我们平时写并发测试代码,一般会使用一个for循环,比如启20个线程大概就是这样
for (int i = 0; i < 20; i++) {
TASK_POOL.execute(task);
}
实际来讲这个写法并不是很严格,应为我们使用的是for循环,所有的线程都是按顺序创建的,并不符合我们想要的并发(至少在启动那一刻)
比如我们常用的java.util.ArrayList,这是一个非线程安全的类,比如add方法如下
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
每次新增之前都会判断容量,如果在并发情况下多个线程一起判断应该都能成功,但是下一步可能就会出现下标越界的问题,或者一个线程的数据覆盖另一个线程刚刚新增的数据
假如现在我们想用一段代码,证明在ArrayList多线程情况下存在问题,如下:
@Test
public void doTest() throws InterruptedException {
final List<Integer> datas = new ArrayList<>();
final ExecutorService TASK_POOL = Executors.newFixedThreadPool(20);
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
datas.add(i);
}
};
for (int i = 0; i < 20; i++) {
TASK_POOL.execute(task);
}
TASK_POOL.shutdown();
TASK_POOL.awaitTermination(5, TimeUnit.SECONDS);
System.out.println(datas.size());
}
代码很简单,就是启动20个线程往ArrayList里面增加数据,每个线程增加100个,最后输出这个集合的长度;如果这个类是线程安全的,最后的结果应该是2000,但是ArrayList并非线程安全,我们期待的结果应该是小于2000或出现下标越界的
然而情况是大部分情况下这个方法都能正常结束,输出结果是2000;
偶尔能达到我们想要的结果,出现下标越界或者最终的列表长度小于2000
Exception in thread "pool-1-thread-2" java.lang.ArrayIndexOutOfBoundsException: 10
at java.util.ArrayList.add(ArrayList.java:463)
at com.calix.test.ConcurrentTest.lambda$doTest$0(ConcurrentTest.java:20)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
1895
出现这个问题主要是我们创建的线程并没有我们预期的那样“并行”,没有发生很激烈的竞争。很多时候后一个线程执行时,前一个已经执行完成了。
想要达到更准确的并发,我们可以使用java.util.concurrent.CountDownLatch这个类,这个工具可以让我们的线程在一开始都处于“同一起跑线”,下面对之前的测试类进行调整
@Test
public void doTestWithCountDown() throws InterruptedException {
final List<Integer> datas = new ArrayList<>();
final ExecutorService TASK_POOL = Executors.newFixedThreadPool(20);
final CountDownLatch countDownLatch = new CountDownLatch(20);
Runnable task = () -> {
try {
countDownLatch.await();//这里等待其他线程就绪后开始放行
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 100; i++) {
datas.add(i);
}
};
for (int i = 0; i < 20; i++) {
TASK_POOL.execute(task);
countDownLatch.countDown();//每个任务提交完毕后执行
}
TASK_POOL.shutdown();
TASK_POOL.awaitTermination(5, TimeUnit.SECONDS);
System.out.println(datas.size());
}
CountDownLatch的用法很简单,在倒计时结束前,await将一直阻塞,保证不会有那个线程先执行
改进过的测试代码可以很容易达到我们想要的测试结果