系列文章目录
文章目录
- 系列文章目录
- 前言
前言
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。
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比较适合读多写少的场景。