解决方案三(并发容器:CopyOnWriteArrayList)
package com.mmall.concurrency.example.concurrent; import com.mmall.concurrency.annoations.ThreadSafe; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; @Slf4j @ThreadSafe public class CopyOnWriteArrayListExample { // 请求总数 public static int clientTotal = 5000; // 同时并发执行的线程数 public static int threadTotal = 200; private static List<Integer> list = new CopyOnWriteArrayList<>(); public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal; i++) { final int count = i; executorService.execute(() -> { try { semaphore.acquire(); update(count); semaphore.release(); } catch (Exception e) { log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); log.info("size:{}", list.size()); } private static void update(int i) { list.add(i); } } // 输出 size:5000
分析
CopyOnWriteArrayList 写操作时复制,当有新元素添加到集合中时,从原有的数组中拷贝一份出来,然后在新的数组上作写操作,然后将原来的数组指向新的数组。整个数组的add操作都是在锁的保护下进行的,防止并发时复制多份副本。读操作是在原数组中进行,不需要加锁。
缺点
写操作时复制消耗内存
不能用于实时读的场景
由于复制和add操作等需要时间,故读取时可能读到旧值。
能做到最终一致性,但无法满足实时性的要求,更适合读多写少的场景。
如果无法知道数组有多大,或者add、set操作有多少,慎用此类。
设计思想
读写分离
最终一致性
使用时另外开辟空间,防止并发冲突
附1:Vector 线程不安全情景
package com.mmall.concurrency.example.syncContainer; import com.mmall.concurrency.annoations.NotThreadSafe; import java.util.Vector; @NotThreadSafe public class VectorExample2 { private static Vector<Integer> vector = new Vector<>(); public static void main(String[] args) { while (true) { for (int i = 0; i < 10; i++) { vector.add(i); } Thread thread1 = new Thread() { public void run() { for (int i = 0; i < vector.size(); i++) { vector.remove(i); } } }; Thread thread2 = new Thread() { public void run() { for (int i = 0; i < vector.size(); i++) { vector.get(i); } } }; thread1.start(); thread2.start(); } } }
- 输出结果报错,因为 remove 和 get 分别是不同对象调用的不同 sync 方法,很有可能出现 remove 在 get 之前操作。
附2:Vector for & foerach & iterator
package com.mmall.concurrency.example.syncContainer; import java.util.Iterator; import java.util.Vector; public class VectorExample3 { // java.util.ConcurrentModificationException private static void test1(Vector<Integer> v1) { // foreach for(Integer i : v1) { if (i.equals(3)) { v1.remove(i); } } } // java.util.ConcurrentModificationException private static void test2(Vector<Integer> v1) { // iterator Iterator<Integer> iterator = v1.iterator(); while (iterator.hasNext()) { Integer i = iterator.next(); if (i.equals(3)) { v1.remove(i); } } } // success private static void test3(Vector<Integer> v1) { // for for (int i = 0; i < v1.size(); i++) { if (v1.get(i).equals(3)) { v1.remove(i); } } } public static void main(String[] args) { Vector<Integer> vector = new Vector<>(); vector.add(1); vector.add(2); vector.add(3); test1(vector); } }
结论
使用foreach,iterator以及for循环对集合类遍历的同时进行修改
在foreach、iterator迭代器循环集合的时候,在遍历过程中尽量不要做更新操作。
如果一定要做的话,在遍历过程中,只做标记,遍历完成后再更新。
遍历过程中(foeach,iterator)更新会导致ConcurrentModificationException
可以用for循环做遍历过程中的增删操作