Java小强个人技术博客站点    手机版
当前位置: 首页 >> Java >> 线程安全的List之CopyOnWriteArrayList

线程安全的List之CopyOnWriteArrayList

8590 Java | 2023-6-29

ArrayList是线程不安全的,这点毋庸置疑。因为ArrayList的所有方法既没有加锁,也没有进行额外的线程安全处理。

而Vector作为线程安全版的ArrayList,存在感总是比较低。因为无论是add、remove还是get方法都加上了synchronized锁,所以效率低下。

OIP.jpg

无意中看到掘金中有人写了这样一遍文章(我花了两天时间没解决的问题,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 ”的文章

上一篇:dynamic动态数据源集成druid实现多数据源 下一篇:docker可视化管理工具-DockerUI

猜你喜欢

发表评论: