ArrayList是线程不安全的,这点毋庸置疑。因为ArrayList的所有方法既没有加锁,也没有进行额外的线程安全处理。
而Vector作为线程安全版的ArrayList,存在感总是比较低。因为无论是add、remove还是get方法都加上了synchronized锁,所以效率低下。
无意中看到掘金中有人写了这样一遍文章(我花了两天时间没解决的问题,chatgpt用了5秒搞定)
看到最后,终归来说是一个多线程下的并发问题,有些人可能觉得这个问题很弱,但是咋说呢,我也遇上了。
因为我按正常逻辑写了一段代码,需要从数据库查询7次数据,后来发现每次查询的速度特别慢,由于业务原因SQL暂时无法优化。
此时想到一个解决方案,就是多条退走路,以下代码模拟了基本流程
package com.example.springboot; import cn.hutool.core.date.DateUtil; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; public class IpTest { public static void main(String[] args) { List<String> reList = new ArrayList<>(); ThreadPoolExecutor pool = (ThreadPoolExecutor) Executors.newFixedThreadPool(7); CountDownLatch countDownLatch = new CountDownLatch(7); // 一周 for (int i = 0; i < 7 ; i++){ CompletableFuture.supplyAsync(() -> { try { reList.add(DateUtil.now()); }catch (Exception e){ e.printStackTrace(); }finally { countDownLatch.countDown(); // 异步执行结束 } return null; }, pool); } // 同步等待查询结果 try { countDownLatch.await(30, TimeUnit.SECONDS); } catch (InterruptedException e) { } reList.stream().forEach(s -> System.out.println(s)); pool.shutdown(); } }
CountDownLatch用于主线程堵塞等待子任务执行完毕,参考(Future机制实际应用)
CompletableFuture用于异步执行任务,注意务必在finally中释放countDownLatch,否则主线程会一直堵塞30秒。
多次运行这个程序,你会发现一个问题,有时会打印出来空
2023-06-29 09:45:42 null 2023-06-29 09:45:42 2023-06-29 09:45:42 2023-06-29 09:45:42 2023-06-29 09:45:42
原因就是因为ArrayList不是线程安全。
解决方案有两种
(1)使用synchronized关键字
CompletableFuture.supplyAsync(() -> { try { synchronized (reList){ reList.add(DateUtil.now()); } }catch (Exception e){ e.printStackTrace(); }finally { countDownLatch.countDown(); // 异步执行结束 } return null; }, pool);
(2)使用CopyOnWriteArrayList代替
List<String> reList = new CopyOnWriteArrayList<>();
关于CopyOnWriteArrayList的解释,网上一大堆,这里点到为止。
这里简单说下,CopyOnWriteArrayList不管是add也好,还是remove也好。都是通过ReentrantLock + volatile + 数组拷贝来实现线程安全的。
而且每次add/remove操作都会开辟新数组,会占用系统内存。
但是存在肯定也是有好处的,就是get(int index)不需要加锁,因为CopyOnWriteArrayList在add/remove操作时,不会修改原数组,所以读操作不会存在线程安全问题。这其实就是读写分离的思想,只有写入的时候才加锁,复制副本来进行修改。CopyOnWriteArrayList也叫写时复制容器。
而且在迭代过程中,即使数组的结构被改变也不会抛出ConcurrentModificationException异常。因为迭代的始终是原数组,而所有的变化都发生在原数组的副本上。所以对于迭代器来说,迭代的集合结构不会发生改变。
总结:CopyOnWriteArrayList的优点主要有两个:
线程安全
大大的提高了“读”操作的并发度(相比于Vector)
缺点也很明显:
每次“写”操作都会开辟新的数组,浪费空间
无法保证实时性,因为“读”和“写”不在同一个数组,且“读”操作没有加互斥锁,所以不能保证强一致性,只能保证最终一致性
add/remove操作效率低,既要加锁,还要拷贝数组
所以CopyOnWriteArrayList比较适合读多写少的场景。
推荐您阅读更多有关于“ list 多线程 线程安全 CopyOnWriteArrayList ”的文章
Java小强
未曾清贫难成人,不经打击老天真。
自古英雄出炼狱,从来富贵入凡尘。
发表评论: