Java多线程并发编程中并发容器第二篇之List的并发类讲解
Java多线程并发编程中并发容器第二篇之List的并发类讲解
概述
本文我们将详细讲解list对应的并发容器以及用代码来测试ArrayList、vector以及CopyOnWriteArrayList在100个线程向list中添加1000个数据后的比较
本文是《凯哥分享Java并发编程之J.U.C包讲解》系列教程中的第六篇。如果想系统学习,凯哥(kaigejava)建议从第一篇开始看。
从本篇开始,我们就来讲解讲解Java的并发容器。大致思路:先介绍什么是并发容器。然后讲解list相关的、map相关的以及队列相关的。这个系列会有好几篇文章。大家最好跟着一篇一篇学。
联系凯哥:
个人博客:www.kaigejava.com
公众号:凯哥Java(kaigejava)
正文开始
并发容器分类讲解
CopyOneWriteArrayList
Copy-One-Write:即写入时候复制。
我们知道在原来List子类中vactor是同步容器线程安全的。这个CopyOneWriteArrayList可以理解为是他的并发替代品。
其底层数据结构也是数值。和ArrayList的不同之处就在于:在list对象中新增或者是删除元素的时候会把原来的集合copy一份,增删操作是在新的对象中操作的。操作完成之后,会将新的数组替换原来的数组。
我们来看看CopyOnWriteArrayList源码中的add方法:
我们来看看setArray方法:
发现了吗?变量使用的是transient和volatile两个关键之来修饰的。
在之前文章中,我们知道了volatile关键字是内存可见性。那么transient关键字是干嘛的呢?我们来看下百科解释:
关键的一句话:用transient关键字修饰的成员变量不用参与序列化过程。
添加注释后的源码:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//获取到锁
lock.lock();
try {
//获取到原集合
Object[] elements = getArray();
int len = elements.length;
//将原集合copy一份到新的集合中。并设置新的集合的长度为原集合长度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//将需要新增的元数添加到新的素组中
newElements[len] = e;
//将新数组替换原来数据。 使用transient和volatitle关键字修饰的
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
代码很简单,大致流程如下:
先从ReentrantLock中获取到锁(这样在多线程下可以防止其他线程来修改容器list里面内容了);
通过arrays.copyOf方法copy出一份原有数组长度+1;
将要添加的元素赋值给copy出来的数组;
使用setArray方法将新得数组替换原有素组。
因为都是List集合。我们就拿ArrayList、vector以及CopyOnWriteArrayList进行比较:
ArrayList、vector以及CopyOnWriteArrayList比较
业务场景描述:
启动100个线程,向对象中添加1000个数据。查看各自运行结果耗时及插入数据总数。代码在文章最后凯哥会贴出来。
先用线程非安全的arrayList执行效果:
执行arryList时间为 : 112毫秒!
List.size() : 93266
我们发现list的长度不对。正确的应该是100*1000.从结果来看,arrayList丢数据了。
使用Vector执行后的效果:
执行vector时间为 : 98毫秒!
List.size() : 100000
执行的总数对,说下同步锁没有丢数据。
在来看看copyOnWriteArrayList执行效果:
执行copyOnWriteArrayList时间为 : 5951毫秒!
List.size() : 100000
运行后数据比较:
运行的类 |
运行时长 |
运行结果 |
ArrayList |
112毫秒 |
93266 |
Vector |
98毫秒 |
100000 |
copyOnWriteArrayList |
5951毫秒 |
100000 |
从上面表格中我们可以看出非安全线程的容器会丢数据。使用copyOneWriteArrayList耗时很长。那是因为每次运行都要copyof一份。
总结
copyArrayList(这里凯哥就简写了):是读写分离的。在写的时候会复制一个新的数组来完成插入和修改或者删除操作之后,再将新的数组给array.读取的时候直接读取最新的数据。
因为在写的时候需要向主内存申请控件,导致写操作的时候,效率非常低的(虽然在操作时候比较慢得,但是在删除或者修改数组的头和尾的时候还是很快的。因为其数据结构决定查找头和尾快,而且执行不需要同步锁)
从上面表中,可以看出copyArrayList虽然保证了线程的安全性,但是写操作效率太low了。但是相比Vector来说,在并发安全方面的性能要比vector好;
CopyArrayList和Vector相比改进的地方:
Vector是在新增、删除、修改以及查询的时候都使用了Synchronized关键字来保证同步的。但是每个方法在执行的时候,都需要获取到锁,在获取锁等待的过程中性能就会大大的降低的。
CopyArrayList的改进:只是在新增和删除的方法上使用了ReentrantLock锁进行(这里凯哥就不截图源码了,自己可以看看源码)。在读的时候不加锁的。所以在读的方面性能要不vector性能要好。
所以,CopyArrayList支持读多写少的并发情况
CopyOnWriteArrayList的使用场景:
由于读操作不加锁,增删改三个操作加锁的,因此适用于读多写少的场景,
局限性:因为读的时候不加锁的,读的效率和普通的arrayList是一样的。但是请看读操作:
在get的时候array使用的是volatile修饰的。是内存可见的。所以可以说copyArrayList在读的时候不会出现arrayList读取到脏数据的问题。
Get(i)方法比较如下:
附件:arrayList、vector、copyOnwriteArrayList比较的代码:
public static void main(String[] args) {
//使用线程不安全的arrayList
// List<String> arryList = new ArrayList<>();
//使用vector
// List<String> arryList = new Vector<>();
//使用copyOnWriteArrayList
List<String> arryList = new CopyOnWriteArrayList<>();
Random random = new Random();
Thread [] threadArr = new Thread[100];
CountDownLatch latch = new CountDownLatch(threadArr.length);
Long beginTime = System.currentTimeMillis();
for(int i = 0;i<threadArr.length;i++){
threadArr[i] = new Thread(new Runnable() {
@Override
public void run() {
for(int j = 0; j < 1000; j++){
try {
arryList.add("value" + random.nextInt(100000));
} catch (Exception e) {
}
}
latch.countDown();
}
});
}
for(Thread t : threadArr){
t.start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("执行copyOnWriteArrayList时间为 : " + (endTime-beginTime) + "毫秒!");
System.out.println("List.size() : " + arryList.size());
}