JUC(一)——Locks
JUC(二)——深入理解锁机制
JUC(三)——线程安全类
JUC(四)——强大的辅助类讲解
JUC(五)——Callable
JUC(六)——阻塞队列
JUC(七)——线程池简单使用
JUC(八)——线程池深入讲解
首先我们都知道ArrayList是线程不安全的,但在以往写代码的时候也没有遇到过多线程操作集合的情况,也没有考虑过不安全这个问题,反正就是拿到就用。
那么到了多线程的时候会出什么问题呢?
- 先来个多线程操作线程的小例子看看结果吧
public class ListMultiThread {
public static void main(String[] args) {
List list = new ArrayList();
for (int i = 0; i < 30; i++) {
//线程里面规定必须用final
final int tempI = i;
new Thread(() -> {
list.add(tempI+"");
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
哦,报错了!!!
ConcurrentModificationException:并发修改异常
为什么会报这个错呢?
- 我们来看一下ArrayList中add()方法的源码吧
-
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
- 没加锁
-
- 那么是在哪里抛出的这个异常呢?
-
public String toString() { Iterator<E> it = iterator(); if (! it.hasNext()) return "[]"; StringBuilder sb = new StringBuilder(); sb.append('['); for (;;) { //这里 E e = it.next(); sb.append(e == this ? "(this Collection)" : e); if (! it.hasNext()) return sb.append(']').toString(); sb.append(',').append(' '); } }
- next()方法中有一个
checkForComodification();
- 异常就是从这里抛出来的
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
- 简单来说就是:没加锁导致toString遍历读写不一致if (modCount != expectedModCount)
-
接下来我们来解决这个问题
- 1、先说最简单的vector吧
- 在学习集合的时候我们应该知道List集合有三个实现类
- vector、ArrayList、LinkedList
- 在我们知道ArrayList线程不安全的同时肯定也听说了vector是线程安全的
- 我们先来看一下vector吧
-
public class ListMultiThread { public static void main(String[] args) { List list = new Vector(); for (int i = 0; i < 30; i++) { //线程里面规定必须用final final int tempI = i; new Thread(() -> { list.add(tempI+""); System.out.println(list); }, String.valueOf(i)).start(); } } }
- 果然,vector并没有报错
- 我们来看一下vector的add()方法
-
public void add(int index, E element) { insertElementAt(element, index); }
-
- ???,竟然没加锁,怎么可能???
- 不要急,add()里面并没有什么实质性的代码,我们再来看insertElementAt
-
public synchronized void insertElementAt(E obj, int index) { modCount++; if (index > elementCount) { throw new ArrayIndexOutOfBoundsException(index + " > " + elementCount); } ensureCapacityHelper(elementCount + 1); System.arraycopy(elementData, index, elementData, index + 1, elementCount - index); elementData[index] = obj; elementCount++; }
-
- 哦,果然还是加锁了
-
- 但是我们都知道vector已经被淘汰了,难不成你真的让我用vector???
- 当然不会,我们还有更好的选择
- 在学习集合的时候我们应该知道List集合有三个实现类
- 2、Collections
- 我们都知道Collections是集合的工具类,那有没有给我们提供好的解决办法呢?
- 哇哦!果然给我们提供了解决方法,不光有list的还有set和map的
- 那我们来试试 Collections.synchronizedList() 吧
-
public class ListMultiThread { public static void main(String[] args) { List list = Collections.synchronizedList(new ArrayList<>()); for (int i = 0; i < 30; i++) { //线程里面规定必须用final final int tempI = i; new Thread(() -> { list.add(tempI+""); System.out.println(list); }, String.valueOf(i)).start(); } } }
- 试完之后发现果然没报错,从字面意思上来看是Collections工具类为我们的集合加了锁
-
- 我们都知道Collections是集合的工具类,那有没有给我们提供好的解决办法呢?
- 3、来开始今天的正式话题:JUC
- JUC为我们提供了更好的方法,不仅仅是为写操作加了锁,还实现了写时复制(也就是我们平时说的读写分离)
- 写时复制
- CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,
- 而是先将当前容器Object[]进行Copy,复制出一个新的容器Object[] newElements,然后向新的容器Object[] newElements里添加元素。
- 添加元素后,再将原容器的引用指向新的容器setArray(newElements)。
- 这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
- 所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
- 写时复制
- 我们来看看如何使用吧
- 这里也包含了其他集合的方法,我们就还是以ArrayList为例
-
public class ListMultiThread { public static void main(String[] args) { List list = new CopyOnWriteArrayList(); for (int i = 0; i < 30; i++) { //线程里面规定必须用final final int tempI = i; new Thread(() -> { list.add(tempI+""); System.out.println(list); }, String.valueOf(i)).start(); } } }
- 结果没有出现问题
-
- JUC为我们提供了更好的方法,不仅仅是为写操作加了锁,还实现了写时复制(也就是我们平时说的读写分离)