1.汇合类不平安之并发批改异样
2.汇合类不平安之加锁和写时复制
3.汇合类不平安之SET
4.汇合类不平安之MAP
1.汇合类不平安之并发批改异样
本次咱们来解说汇合类线程不平安操作的问题,咱们先来看一看个别状况下,咱们是如何操作汇合类的:
List<String> list = new ArrayList<>();; list.add("a"); list.add("b"); list.add("c");
一般来说,没有什么简单的高并发业务逻辑场景的话,咱们都是简略地对list进行add操作,然而,在高并发场景下呢?接下来咱们模仿一下多线程操作一个list,会呈现什么样的状况:
//创立了三千个线程,对一个list进行随机减少字符串 List<String> list = new ArrayList<>();; for (int i = 0; i < 3000; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0, 8)); System.out.println(list); }, String.valueOf(i)).start(); }
运行一下,后果报了异样:
!
java.util.ConcurrentModificationException
显示JAVA并发批改异样,这是因为多线程同时在操作一个对象。
(举个例子,一个班有三千名学生,同时在往一签到表上写名字进行签到,然而如果不遵循先来后到准则的话,就会产生哄抢景象,签到表会坏掉。)
2.汇合类不平安之加锁和写时复制
此时,有两个解决办法:
办法一:
List<String> list = Collections.synchronizedList(new ArrayList<>());
咱们先来尝试一下:
List<String> list = Collections.synchronizedList(new ArrayList<>()); for (int i = 0; i < 3000; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0, 8)); System.out.println(list); }, String.valueOf(i)).start(); }
运行后果没有问题:
为什么应用Collections.synchronizedList()办法咱们就能够将这个list变为线程平安的list呢,咱们来看一下源码:
public static <T> List<T> synchronizedList(List<T> list) { return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list) : new SynchronizedList<>(list)); }
把ArrayList类传进去后,会进行类型判断,因为ArrayList实现了RandomAccess接口(是一个标记接口,表明实现这个这个接口的 List 汇合是反对疾速随机拜访的,具体咱们不开展),所以会调用
new SynchronizedRandomAccessList<>(list)
而后就会调用
SynchronizedList(List<E> list) { super(list); this.list = list; }
此时,list是一个final对象
final List<E> list;
final就代表这个list被赋值一次就不能再被赋值了,然而list里的内容还是可能add和remove的。
接下来,咱们看一下最重要的办法add:
public void add(int index, E element) { synchronized (mutex) {list.add(index, element);} }
mutex是一个互斥变量,学过操作系统的人都晓得,谁领有mutex,谁就能进入这个办法进行操作,而别的线程只能在里面期待。
Collections.synchronizedList(new ArrayList<>());
下面这个类对list的办法进行了再封装,进行同步的add操作。
办法二:
List<String> list = new CopyOnWriteArrayList<>()
他的add办法在多线程的状况下也是不会报错的,在这里咱们就不演示了,接下来咱们来看一下源码,首先是构造方法:
new CopyOnWriteArrayList<>()
点进去之后
public CopyOnWriteArrayList() { setArray(new Object[0]); }
final void setArray(Object[] a) { array = a; }
private transient volatile Object[] array;
他会先设置一个长度为0的object数组,并且这个array是volatile的Object数组,留神一下volatile的个性(可见性,禁止指令重排,然而不保障原子性)。
接下来咱们看一下,CopyOnWriteArrayList最重要的add办法是怎么保障线程平安的。
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { // 先获取当初的数组对象 Object[] elements = getArray(); int len = elements.length; //将当初的数组对象进行扩容+1后,取的新的数组对象 Object[] newElements = Arrays.copyOf(elements, len + 1); //将新元素放在数组最初的地位上 newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
由此源码,咱们能够看到,add办法先后进行了
复制->扩容->放入最初一个地位 这三个操作。
并且还应用了加锁机制,每次只能有一个线程能进入这个办法。
还有volatile关键字:
每次应用array数组的时候,确保是从内存中取到的最新的数值。
3.汇合类不平安之SET
因为list,set,map都是线程不平安的,多线程操作都会引起异样,所以咱们就不演示了。
创立汇合平安的类,有两种办法
//这种形式的add办法和synchronizedList形式并没有区别Set<String> set = Collections.synchronizedSet(new HashSet<>());//咱们重点看第二种Set<String> set = new CopyOnWriteArraySet();
咱们先看构造方法
/** * Creates an empty set. */ public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList<E>(); }
private final CopyOnWriteArrayList<E> al;
可见CopyOnWriteArraySet的底层是arrayList(这里不要被我误导了,HashSet的底层是HashMap,只不过value都是Object而已,只是咱们当初看到的CopyOnWriteArraySet,底层是ArrayList)
咱们来看一下最重要的add办法:
public boolean add(E e) { return al.addIfAbsent(e); }
/** * Appends the element, if not present. * * @param e element to be added to this list, if absent * @return {@code true} if the element was added */ public boolean addIfAbsent(E e) { Object[] snapshot = getArray(); return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false : addIfAbsent(e, snapshot); }
咱们能够看到注解说,如果没有这个元素,就增加,否则就不增加。
indexOf是判断这个元素是否在set内存在并返回索引的,如果大于等于0就不存在,接着会调用addIfAbsent办法:
/** * A version of addIfAbsent using the strong hint that given * recent snapshot does not contain e. */ private boolean addIfAbsent(E e, Object[] snapshot) { //加锁操作 final ReentrantLock lock = this.lock; lock.lock(); try { Object[] current = getArray(); int len = current.length; //如果传进来的值和本地数组援用不是一个地址 //咱们就要进行比对,如果发现了未增加的元素曾经在数组里了,就返回false if (snapshot != current) { // Optimize for lost race to another addXXX operation int common = Math.min(snapshot.length, len); for (int i = 0; i < common; i++) if (current[i] != snapshot[i] && eq(e, current[i])) return false; if (indexOf(e, current, common, len) >= 0) return false; } //也是采取了写时复制办法 Object[] newElements = Arrays.copyOf(current, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
咱们由此看出,set的add办法,也是用了扩容+写时复制的办法了。
4.汇合类不平安之MAP
最初咱们来看一下map
map的线程安全类为:
Map<String,String> map = new ConcurrentHashMap<>();
concurrentHashMap采纳的原理是cas,具体我会另外开一篇文章,因为很简短,就不在这儿赘述了。
总结:
并发状况下操作一个数组会出现异常,咱们大部分都会采纳加锁和写时复制进行解决。