只有光头才能变强
前一阵子写过一篇COW(Copy On Write)文章,结果阅读量很低啊...COW奶牛!Copy On Write机制了解一下java
可能你们对这个技术比较陌生吧,但这项技术是挺多应用场景的。除了上文所说的Linux、文件系统外,其实在Java也有其身影。git
你们对线程安全容器可能最熟悉的就是ConcurrentHashMap了,由于这个容器常常会在面试的时候考查。github
好比说,一个常见的面试场景:面试
那若是有这样的面试呢?编程
今天主要讲解的是CopyOnWriteArrayList~数组
本文力求简单讲清每一个知识点,但愿你们看完能有所收获安全
咱们知道ArrayList是用于替代Vector的,Vector是线程安全的容器。由于它几乎在每一个方法声明处都加了synchronized关键字来使容器安全。服务器
若是使用Collections.synchronizedList(new ArrayList())
来使ArrayList变成是线程安全的话,也是几乎都是每一个方法都加上synchronized关键字的,只不过它不是加在方法的声明处,而是方法的内部。多线程
在讲解CopyOnWrite容器以前,咱们仍是先来看一下线程安全容器的一些可能没有注意到的地方~并发
下面咱们直接来看一下这段代码:
// 获得Vector最后一个元素 public static Object getLast(Vector list) { int lastIndex = list.size() - 1; return list.get(lastIndex); } // 删除Vector最后一个元素 public static void deleteLast(Vector list) { int lastIndex = list.size() - 1; list.remove(lastIndex); }
以咱们第一反应来分析一下上面两个方法:在多线程环境下,是否有问题?
size()和get()以及remove()
都被synchronized修饰的。答案:从调用者的角度是有问题的
咱们能够写段代码测试一下:
import java.util.Vector; public class UnsafeVectorHelpers { public static void main(String[] args) { // 初始化Vector Vector<String> vector = new Vector(); vector.add("关注公众号"); vector.add("Java3y"); vector.add("买Linux可到我下面的连接,享受最低价"); vector.add("给3y加鸡腿"); new Thread(() -> getLast(vector)).start(); new Thread(() -> deleteLast(vector)).start(); new Thread(() -> getLast(vector)).start(); new Thread(() -> deleteLast(vector)).start(); } // 获得Vector最后一个元素 public static Object getLast(Vector list) { int lastIndex = list.size() - 1; return list.get(lastIndex); } // 删除Vector最后一个元素 public static void deleteLast(Vector list) { int lastIndex = list.size() - 1; list.remove(lastIndex); } }
能够发现的是,有可能会抛出异常的:
缘由也很简单,咱们照着流程走一下就行了:
getLast()
方法,线程B执行deleteLast()
方法int lastIndex = list.size() - 1;
获得lastIndex的值是3。同时,线程B执行int lastIndex = list.size() - 1;
获得的lastIndex的值也是3list.remove(lastIndex)
将下标为3的元素删除了list.get(lastIndex);
,发现已经没有下标为3的元素,抛出异常了.出现这个问题的缘由也很简单:
getLast()
和deleteLast()
这两个方法并非原子性的,即便他们内部的每一步操做是原子性的(被Synchronize修饰就能够实现原子性),可是内部之间仍是能够交替执行。
size()和get()以及remove()
都是原子性的,可是若是并发执行getLast()
和deleteLast()
,方法里面的size()和get()以及remove()
是能够交替执行的。要解决上面这种状况也很简单,由于咱们都是对Vector进行操做的,只要操做Vector前把它锁住就没毛病了!
因此咱们能够改为这样子:
// 获得Vector最后一个元素 public static Object getLast(Vector list) { synchronized (list) { int lastIndex = list.size() - 1; return list.get(lastIndex); } } // 删除Vector最后一个元素 public static void deleteLast(Vector list) { synchronized (list) { int lastIndex = list.size() - 1; list.remove(lastIndex); } }
ps:若是有人去测试一下,发现会抛出异常java.lang.ArrayIndexOutOfBoundsException: -1,这是 没有检查角标的异常,不是并发致使的问题。
通过上面的例子咱们能够看看下面的代码:
public static void main(String[] args) { // 初始化Vector Vector<String> vector = new Vector(); vector.add("关注公众号"); vector.add("Java3y"); vector.add("买Linux可到我下面的连接,享受最低价"); vector.add("给3y加鸡腿"); // 遍历Vector for (int i = 0; i < vector.size(); i++) { // 好比在这执行vector.clear(); //new Thread(() -> vector.clear()).start(); System.out.println(vector.get(i)); } }
一样地:若是在遍历Vector的时候,有别的线程修改了Vector的长度,那仍是会有问题!
vector.size()
时,发现Vector的长度为5clear()
操做vector.get(i)
时,抛出异常在JDK5之后,Java推荐使用for-each
(迭代器)来遍历咱们的集合,好处就是简洁、数组索引的边界值只计算一次。
若是使用for-each
(迭代器)来作上面的操做,会抛出ConcurrentModificationException异常
SynchronizedList在使用迭代器遍历的时候一样会有问题的,源码已经提醒咱们要手动加锁了。
若是想要完美解决上面所讲的问题,咱们能够在遍历前加锁:
// 遍历Vector synchronized (vector) { for (int i = 0; i < vector.size(); i++) { vector.get(i); } }
有经验的同窗就能够知道:哇,遍历一下容器都要我加上锁,这这这不是要慢死了吗.的确是挺慢的..
因此咱们的CopyOnWriteArrayList就登场了!
通常来讲,咱们会认为:CopyOnWriteArrayList是同步List的替代品,CopyOnWriteArraySet是同步Set的替代品。
不管是Hashtable-->ConcurrentHashMap,仍是说Vector-->CopyOnWriteArrayList。JUC下支持并发的容器与老一代的线程安全类相比,总结起来就是加锁粒度的问题
因此通常来讲,咱们都会使用JUC包下给咱们提供的线程安全容器,而不是使用老一代的线程安全容器。
下面咱们来看看CopyOnWriteArrayList是怎么实现的,为何使用迭代器遍历的时候就不用额外加锁,也不会抛出ConcurrentModificationException异常。
咱们仍是先来回顾一下COW:
若是有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取 相同的指针指向相同的资源,直到某个调用者 试图修改资源的内容时,系统才会 真正复制一份专用副本(private copy)给该调用者,而其余调用者所见到的最初的资源仍然保持不变。 优势是若是调用者 没有修改该资源,就不会有副本(private copy)被创建,所以多个调用者只是读取操做时能够 共享同一份资源。
参考自维基百科:https://zh.wikipedia.org/wiki/%E5%AF%AB%E5%85%A5%E6%99%82%E8%A4%87%E8%A3%BD
以前写博客的时候,若是是要看源码,通常会翻译一下源码的注释并用图贴在文章上的。Emmm,发现阅读体验并非很好,因此我这里就 直接归纳一下源码注释说了什么吧。另外,若是使用IDEA的话,能够下一个插件 Translation(免费好用).
归纳一下CopyOnWriteArrayList源码注释介绍了什么:
/** 可重入锁对象 */ final transient ReentrantLock lock = new ReentrantLock(); /** CopyOnWriteArrayList底层由数组实现,volatile修饰 */ private transient volatile Object[] array; /** * 获得数组 */ final Object[] getArray() { return array; } /** * 设置数组 */ final void setArray(Object[] a) { array = a; } /** * 初始化CopyOnWriteArrayList至关于初始化数组 */ public CopyOnWriteArrayList() { setArray(new Object[0]); }
看起来挺简单的,CopyOnWriteArrayList底层就是数组,加锁就交由ReentrantLock来完成。
根据上面的分析咱们知道若是遍历Vector/SynchronizedList
是须要本身手动加锁的。
CopyOnWriteArrayList使用迭代器遍历时不须要显示加锁,看看add()、clear()、remove()
与get()
方法的实现可能就有点眉目了。
首先咱们能够看看add()
方法
public boolean add(E e) { // 加锁 final ReentrantLock lock = this.lock; lock.lock(); try { // 获得原数组的长度和元素 Object[] elements = getArray(); int len = elements.length; // 复制出一个新数组 Object[] newElements = Arrays.copyOf(elements, len + 1); // 添加时,将新元素添加到新数组中 newElements[len] = e; // 将volatile Object[] array 的指向替换成新数组 setArray(newElements); return true; } finally { lock.unlock(); } }
经过代码咱们能够知道:在添加的时候就上锁,并复制一个新数组,增长操做在新数组上完成,将array指向到新数组中,最后解锁。
再来看看size()
方法:
public int size() { // 直接获得array数组的长度 return getArray().length; }
再来看看get()
方法:
public E get(int index) { return get(getArray(), index); } final Object[] getArray() { return array; }
那再来看看set()
方法
public E set(int index, E element) { final ReentrantLock lock = this.lock; lock.lock(); try { // 获得原数组的旧值 Object[] elements = getArray(); E oldValue = get(elements, index); // 判断新值和旧值是否相等 if (oldValue != element) { // 复制新数组,新值在新数组中完成 int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len); newElements[index] = element; // 将array引用指向新数组 setArray(newElements); } else { // Not quite a no-op; enssures volatile write semantics setArray(elements); } return oldValue; } finally { lock.unlock(); } }
对于remove()、clear()
跟set()和add()
是相似的,这里我就再也不贴出代码了。
总结:
经常使用的方法实现咱们已经基本了解了,但仍是不知道为啥可以在容器遍历的时候对其进行修改而不抛出异常。因此,来看一下他的迭代器吧:
// 1. 返回的迭代器是COWIterator public Iterator<E> iterator() { return new COWIterator<E>(getArray(), 0); } // 2. 迭代器的成员属性 private final Object[] snapshot; private int cursor; // 3. 迭代器的构造方法 private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } // 4. 迭代器的方法... public E next() { if (! hasNext()) throw new NoSuchElementException(); return (E) snapshot[cursor++]; } //.... 能够发现的是,迭代器全部的操做都基于snapshot数组,而snapshot是传递进来的array数组
到这里,咱们应该就能够想明白了!CopyOnWriteArrayList在使用迭代器遍历的时候,操做的都是原数组!
看了上面的实现源码,咱们应该也大概能分析出CopyOnWriteArrayList的缺点了。
内存占用:若是CopyOnWriteArrayList常常要增删改里面的数据,常常要执行add()、set()、remove()
的话,那是比较耗费内存的。
add()、set()、remove()
这些增删改操做都要复制一个数组出来。数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
setArray()
了)。可是线程A迭代出来的是原有的数据。CopyOnWriteArraySet的原理就是CopyOnWriteArrayList。
private final CopyOnWriteArrayList<E> al; public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList<E>(); }
如今临近双十一买阿里云服务器就特别省钱!以前我买学生机也要9.8块钱一个月,如今最低价只须要8.3一个月!
若是有要买服务器的同窗可经过个人连接直接享受最低价:https://m.aliyun.com/act/team1111/#/share?params=N.FF7yxCciiM.pfn5xpli
阅读这篇文章可能须要对Java容器和多线程有必定的了解。若是对这些知识还不太了解的同窗们可看我以前写过的文章哦~
若是你们有更好的理解方式或者文章有错误的地方还请你们不吝在评论区留言,你们互相学习交流~~~
参考资料:
扩展阅读:
一个 坚持原创的Java技术公众号:Java3y,欢迎你们关注
3y全部的原创文章: