Java并发包提供了不少线程安全的集合,有了他们的存在,使得咱们在多线程开发下,能够和单线程同样去编写代码,大大简化了多线程开发的难度,可是若是不知道其中的原理,可能会引起意想不到的问题,因此知道其中的原理仍是颇有必要的。数组
今天咱们来看下Java并发包中提供的线程安全的List,即CopyOnWriteArrayList。安全
刚接触CopyOnWriteArrayList的时候,我总感受这个集合的名称有点奇怪:在写的时候复制?后来才知道它就是在写的时候进行了复制,因此这个命名仍是至关严谨的。固然,翻译成 写时复制 会更好一些。bash
咱们在研究源码的时候,能够带着问题去研究,这样可能效果会更好,把问题一个一个攻破,也更有成就感,因此在这里,我先抛出几个问题:微信
咱们先来看下CopyOnWriteArrayList的UML图: 多线程
咱们能够经过add方法添加一个元素并发
public boolean add(E e) {
//1.得到独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();//2.得到Object[]
int len = elements.length;//3.得到elements的长度
Object[] newElements = Arrays.copyOf(elements, len + 1);//4.复制到新的数组
newElements[len] = e;//5.将add的元素添加到新元素
setArray(newElements);//6.替换以前的数据
return true;
} finally {
lock.unlock();//7.释放独占锁
}
}
复制代码
final Object[] getArray() {
return array;
}
复制代码
当调用add方法,代码会跑到(1)去得到独占锁,由于独占锁的特性,致使若是有多个线程同时跑到(1),只能有一个线程成功得到独占锁,而且执行下面的代码,其他的线程只能在外面等着,直到独占锁被释放。源码分析
线程得到到独占锁后,执行(2),得到array,而且赋值给elements ,(3)得到elements的长度,而且赋值给len,(4)复制elements数组,在此基础上长度+1,赋值给newElements,(5)将咱们须要新增的元素添加到newElements,(6)替换以前的数组,最后跑到(7)释放独占锁。ui
解析源码后,咱们明白了this
public E get(int index) {
return get(getArray(), index);
}
复制代码
final Object[] getArray() {
return array;
}
复制代码
咱们能够经过调用get方法,来得到指定下标的元素。spa
首先得到array,而后得到指定下标的元素,看起来没有任何问题,可是其实这是存在问题的。别忘了,咱们如今是多线程的开发环境,否则也没有必要去使用JUC下面的东西了。
试想这样的场景,当咱们得到了array后,把array捧在手内心,如获珍宝。。。因为整个get方法没有独占锁,因此另一个线程还能够继续执行修改的操做,好比执行了remove的操做,remove和add同样,也会申请独占锁,而且复制出新的数组,删除元素后,替换掉旧的数组。而这一切get方法是不知道的,它不知道array数组已经发生了天翻地覆的变化,它仍是傻乎乎的,看着捧在手内心的array。。。这就是弱一致性。
就像微信同样,虽然对方已经把你给删了,可是你不知道,你仍是天天打开和她的聊天框,准备说些什么。。。
咱们能够经过set方法修改指定下标元素的值。
public E set(int index, E element) {
//(1)得到独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();//(2)得到array
E oldValue = get(elements, index);//(3)根据下标,得到旧的元素
if (oldValue != element) {//(4)若是旧的元素不等于新的元素
int len = elements.length;//(5)得到旧数组的长度
Object[] newElements = Arrays.copyOf(elements, len);//(6)复制出新的数组
newElements[index] = element;//(7)修改
setArray(newElements);//(8)替换
} else {
//(9)为了保证volatile 语义,即便没有修改,也要替换成新的数组
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();//(10)释放独占锁
}
}
复制代码
当咱们调用set方法后:
经过源码解析,咱们应该更有体会:
咱们能够经过remove删除指定坐标的元素。
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
复制代码
能够看到,remove方法和add,set方法是同样的,第一步仍是先获取独占锁,来保证线程安全性,若是要删除的元素是最后一个,则复制出一个长度为【旧数组的长度-1】的新数组,随之替换,这样就巧妙的把最后一个元素给删除了,若是要删除的元素不是最后一个,则分两次复制,随之替换。
在解析源码前,咱们先看下迭代器的基本使用:
public class Main {public static void main(String[] args) {
CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
copyOnWriteArrayList.add("Hello");
copyOnWriteArrayList.add("copyOnWriteArrayList");
Iterator<String>iterator=copyOnWriteArrayList.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
复制代码
运行结果:
代码很简单,这里就再也不解释了,咱们直接来看迭代器的源码:
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
复制代码
static final class COWIterator<E> implements ListIterator<E> {
private final Object[] snapshot;
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
// 判断是否还有下一个元素
public boolean hasNext() {
return cursor < snapshot.length;
}
//获取下个元素
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
复制代码
当咱们调用iterator方法获取迭代器,内部会调用COWIterator的构造方法,此构造方法有两个参数,第一个参数就是array数组,第二个参数是下标,就是0。随后构造方法中会把array数组赋值给snapshot变量。 snapshot是“快照”的意思,若是Java基础尚可的话,应该知道数组是引用类型,传递的是指针,若是有其余地方修改了数组,这里应该立刻就能够反应出来,那为何又会是snapshot这样的命名呢?没错,若是其余线程没有对CopyOnWriteArrayList进行增删改的操做,那么snapshot就是自己的array,可是若是其余线程对CopyOnWriteArrayList进行了增删改的操做,旧的数组会被新的数组给替换掉,可是snapshot仍是原来旧的数组的引用。也就是说 当咱们使用迭代器便利CopyOnWriteArrayList的时候,不能保证拿到的数据是最新的,这也是弱一致性问题。
什么?你不信?那咱们经过一个demo来证明下:
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
copyOnWriteArrayList.add("Hello");
copyOnWriteArrayList.add("CopyOnWriteArrayList");
copyOnWriteArrayList.add("2019");
copyOnWriteArrayList.add("good good study");
copyOnWriteArrayList.add("day day up");
new Thread(()->{
copyOnWriteArrayList.remove(1);
copyOnWriteArrayList.remove(3);
}).start();
TimeUnit.SECONDS.sleep(3);
Iterator<String> iterator = copyOnWriteArrayList.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
复制代码
运行结果:
而后咱们换一种写法:
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
copyOnWriteArrayList.add("Hello");
copyOnWriteArrayList.add("CopyOnWriteArrayList");
copyOnWriteArrayList.add("2019");
copyOnWriteArrayList.add("good good study");
copyOnWriteArrayList.add("day day up");
Iterator<String> iterator = copyOnWriteArrayList.iterator();
new Thread(()->{
copyOnWriteArrayList.remove(1);
copyOnWriteArrayList.remove(3);
}).start();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
复制代码
此次咱们改变了代码的顺序,先是获取迭代器,而后是执行删除线程的操做,最后遍历迭代器。 运行结果:
若是咱们没有分析源码,不知道其中的原理,不知道弱一致性,当在多线程中用到CopyOnWriteArrayList的时候,可能会痛不欲生,想砸电脑,不知道为何获取的数据有时候就不是正确的数据,而有时候又是。因此探究原理,仍是挺有必要的,不论是经过源码分析,仍是经过看博客,甚至是直接看JDK中的注释,都是能够的。
在Java并发包提供的集合中,CopyOnWriteArrayList应该是最简单的一个,但愿经过源码分析,让你们有一个信心,原来JDK源码也是能够读懂的。