问题描述 业务有一个需求,我把问题描述一下: 通过代理IP访问国外某网站N,每个IP对应一个固定的网站N的COOKIE,COOKIE有失效时间。 并发下,取IP是有一定策略的,取到IP之后
顺晟科技
2021-08-28 09:42:46
31
问题描述
生意有需求,我就描述一下问题:
通过代理IP访问国外网站N,每个IP对应网站N的一个固定的COOKIE,COOKIE有过期时间。
在并发下,有一定的取IP策略。获取IP后,获取IP对应的COOKIE。如果COOKIE超过了过期时间,调用脚本访问网站N获取数据一次。
为了防止多线程得到同一个IP,发现该IP对应的COOKIE无效,调用脚本更新COOKIE,IP被锁定。为了确保锁的全局性,在锁的前面添加了一个标识服务的前缀,并将“锁前缀IP”锁定在一个同步(lock){中.}的方式,从而确保多个线程获得相同的IP,并且只有一个IP会更新COOKIE。
不知道这个问题有没有说清楚。没说清楚也没关系。编写测试代码:
复制代码
公共类StringThread实现了Runnable {
私有静态最终字符串LOCK _ PREFIX=' XXX-';
私有字符串IP;
公共字符串读取(字符串IP){ 0
this.ip=ip
}
@覆盖
public void run(){ 0
string lock=BuildLock();
同步(锁定){ 0
系统。out . println('[' jdkutil . getthreadname()']开始运行');
//休眠5秒,模拟脚本调用
jdkutil . sleep(5000);
系统。out . println('[' jdkutil . getthreadname()']已完成运行');
}
}
私有字符串buildLock(){ 0
StringBuilder sb=new StringBuilder();
sb . append(LOCK _ PREFIX);
sb . append(IP);
string lock=sb . ToString();
系统;出去;println('[' jdkutil;getthreadname ()']构建了一个锁[' lock ']');
返回锁;
}
}
复制代码
简单地说,传入一个IP,并尝试构建一个全局的字符串(其原因是,如果字符串的性不强,例如锁的‘192 . 168 . 1 . 1’,如果另一个业务代码也是锁的字符串‘192 . 168 . 1 . 1’,这意味着必须连续执行两个不相关的代码块,代码
预期的结果是并发。例如,如果五个线程传入同一个IP,并且它们构建的锁都是字符串‘XXX-192 . 168 . 1 . 1’,那么这五个线程应该为同步块串联执行,也就是一个在另一个运行之后运行,但事实并非如此。
写一个测试代码,打开5个线程看看效果:
复制代码
公共类StringThreadTest {
私有静态最终int THREAD _ COUNT=5;
@测试
public void testStringThread(){ 0
THREAD[]THREAD=new THREAD[THREAD _ COUNT];
for(int I=0;i THREAD _ COUNTI){ 0
threads[I]=new Thread(new StringThread(' 192 . 168 . 1 . 1 '));
}
for(int I=0;i THREAD _ COUNTI){ 0
线程[i]。start();
}
for(;);
}
}
复制代码
执行结果是:
复制代码
[Thread-1]建了锁[XXX-192.168.1.1]
[线程1]正在运行
[Thread-3]建了锁[XXX-192.168.1.1]
[线程3]正在运行
[线程-4]建立了锁[XXX-192.168.1.1]
[线程-4]正在运行
[Thread-0]建立了锁[XXX-192.168.1.1]
[线程-0]正在运行
[线程-2]建立了锁[XXX-192.168.1.1]
[线程-2]正在运行
[线程1]运行完毕
[线程3]运行完毕
[线程4]运行完毕
[线程-0]运行完毕
[线程2]运行完毕
复制代码
看到线程-0、线程-1、线程-2、线程-3、线程-4五个线程有相同的锁‘XXX-192 . 168 . 1 . 1’,代码是并行执行的,不符合我们的预期。
在这个问题上,一方面我粗心大意,认为代码其他部分的同步控制有问题;另一方面也反映了我对String的理解不够深刻。所以我写了一篇文章记录这个问题,写清楚这个问题的原因以及如何解决。
问题原因
既然出现了这个问题,我们就应该从结果出发,找出问题的原因。让我们看看代码的同步部分:
复制代码
@覆盖
public void run(){ 0
string lock=BuildLock();
同步(锁定){ 0
系统。out . println('[' jdkutil . getthreadname()']开始运行');
//休眠5秒,模拟脚本调用
jdkutil . sleep(5000);
系统。out . println('[' jdkutil . getthreadname()']已完成运行');
}
}
复制代码
因为当synchronized锁定对象时,保证synchronized代码块中的代码执行是串行执行的前提是锁定的对象是相同的,由于在synchronized部分是并行执行多线程的,可以推断在多线程下引入了相同的IP,构造的锁串也不相同。
接下来,让我们看看构建字符串的代码:
复制代码
私有字符串buildLock(){ 0
StringBuilder sb=new StringBuilder();
sb . append(LOCK _ PREFIX);
sb . append(IP);
string lock=sb . ToString();
系统;出去;println('[' jdkutil;getthreadname ()']构建了一个锁[' lock ']');
返回锁;
}
复制代码
锁是由StringBuilder生成的。看看StringBuilder的toString方法:
公共字符串ToString(){ 0
//创建副本,不共享数组
返回新字符串(值,0,计数);
}
那么原因就在这里:虽然buildLock()方法构建的Strings都是‘XXX-192 . 168 . 1 . 1’,但是因为StringBuilder的toString()方法总是会出来一个新的字符串,所以buildLock生成的对象都是不同的对象。
怎么解决?
上面这个问题的原因已经找到了,那就是每次StringBuilder构建的对象都是一个新的对象,我们应该如何解决呢?我在这里给出的个解决方案是sb.toString(),然后是intern()。下一部分我会讲原因,因为我想再次总结String,加深对String的理解。
好的,代码是这样改变的:
复制代码
1公共类StringThread实现了Runnable {
2
3私有静态最终字符串LOCK _ PREFIX=' XXX-';
四
5私有字符串IP;
六
7公共字符串读取(字符串IP){ 0
8 this.ip=ip
9 }
10
11 @覆盖
12公共无效运行(){ 0
13
14字符串锁=buildLock();
15同步(锁定){ 0
16系统。out . println('[' jdkutil . getthreadname()']开始运行');
17 //休眠5秒,模拟脚本调用
18 jdkutil . sleep(5000);
19系统。out . println('[' jdkutil . getthreadname()']已完成运行');
20 }
21 }
22
23私有字符串buildLock(){ 0
24 StringBuilder sb=new StringBuilder();
25 sb . append(LOCK _ PREFIX);
26 sb . append(IP);
27
28字符串锁=sb.toString()。实习生();
29系统。out . println('[' jdkutil . getthreadname()']构建了一个锁[' lock ']');
30
31返回锁定;
32 }
33
34 }
复制代码
查看代码执行结果:
复制代码
[Thread-0]建立了锁[XXX-192.168.1.1]
[线程-0]正在运行
[Thread-3]建了锁[XXX-192.168.1.1]
[线程-4]建立了锁[XXX-192.168.1.1]
[Thread-1]建了锁[XXX-192.168.1.1]
[线程-2]建立了锁[XXX-192.168.1.1]
[线程-0]运行完毕
[线程-2]正在运行
[线程2]运行完毕
[线程1]正在运行
[线程1]运行完毕
[线程-4]正在运行
[线程4]运行完毕
[线程3]正在运行
[线程3]运行完毕
复制代码
不用上面的intern()方法就可以比较执行结果。显而易见,五个线程获取的锁是一样的,只有一个线程执行了同步代码块中的代码,下一个线程才能执行,整个执行是串行的。
再看看字符串
JVM内存区域中有一个常量池。关于常量池的分配:
在JDK6版本中,常量池是在持久生成PermGen中分配的
在JDK7版本中,常量池是在堆堆中分配的
字符串存储在常量池中,常量池中存储有两种类型的字符串数据:
可以在编译时确定的字符串,即由“”引起的字符串,如String a='123 ',String B=' 1 ' B. GetStringdataFromdb()' 2 ' C. GetStringdataFromdb(),这里的' 123 ',' 1 '和' 2 '都是可以在编译时确定的字符串因此,它将被放入常量池,而B . GetStringdataFromdb()和c . GetStringdataFromdb()则在堆上分配,因为它们在编译期间无法确定
由String的inter()方法操作的字符串,如String B=B.getStringDataFromDB()。Inter(),虽然由B.getStringDataFromDB()方法获得的字符串是在堆上分配的,但是由于后来添加了inter(),B. GetStringdataFromdb()方法的结果将被写入常量池
常量池中的字符串数据有一个特点:每次取数据时,如果常量池中有一个,则直接取常量池中的数据;如果不在常量池中,则将数据写入常量池并返回常量池中的数据。
回到我们前面的场景,使用StringBuilder拼接字符串一次返回一个新对象,但是使用intern()方法是不同的:
虽然字符串“XXX-192.168.1.1”是使用StringBuilder的toString()方法创建的,但是个线程发现常量池中没有“XXX-192.168.1.1”,所以它在常量池中放了一个。
XXX-192.168.1.1 ',下面的线程在常量池中找到“XXX-192.168.1.1”,所以直接取常量池中的“XXX-192.168.1.1”。
因此,无论多少线程,只要取‘XXX-192 . 168 . 1 . 1’,就必须取出同一个对象,即常量池中的‘XXX-192 . 168 . 1 . 1’
所有这些都是String的intern()方法的功能
附言
解决了这个问题之后,包括写这篇文章,我特别有一点感触。很多人认为一个Java程序员可以很好的使用框架,写出没有bug的代码流。研究底层原理和虚拟机是没有用的。不知道这个问题能不能给你一些启发:
复制代码
这个业务场景并不复杂,整个代码实现也不是很复杂,但是在运行的时候存在并发问题。
如果你没有扎实的基础:你知道除了常用的方法indexOf、subString、concat之外,在String中还有非常少见的intern()方法
对JVM一无所知:JVM内存分配,尤其是常量池
不要看JDK源代码:字符串生成器的toString()方法
我对并发没有一些了解:当锁定同步的代码块时,我们如何确保多线程连续执行代码块中的代码
这个问题根本解决不了,我们连怎么分析都不知道。
复制代码
因此,不要认为JVM和JDK源代码的底层实现原理是无用的。相反,这些才是技术人员成长最宝贵的东西。
28
2021-08
28
2021-08
16
2021-06
16
2021-06
16
2021-06
16
2021-06