十四、写时复制的CopyOnWriteArrayList

1. 前言

本节带领大家认识第二个常用的 Java 并发容器类之 CopyOnWriteArrayList

本节先介绍 CopyOnWriteArrayList 工具类表达的概念和最基本用法,接着通过一个生活中的例子为大家解释 CopyOnWriteArrayList 工具类的使用场合,然后通过简单的编码实现此场景。

下面我们正式开始介绍吧。

2. 概念解释

什么是 CopyOnWrite ? 顾名思义,就是 “写数据的时候先拷贝一份副本,在副本上写数据”。为什么需要在写的时候以这种方式执行呢?当然是为了提高效率。

当多个线程同时操作一个 ArrayList 对象时,为了线程安全需要对操作增加线程安全相关的锁控制。采用 CopyOnWrite 方式,可以做到读操作不用加锁,而只对写操作加锁,且可以很方便地反馈写后的结果给到读操作。CopyOnWriteArrayList 就是采用这种优化思想,对 ArrayList 做的线程安全特性增强。我们通过一张图了解其基本原理。
在这里插入图片描述

概念已经了解了,CopyOnWriteArrayList 工具类最基本的用法是怎样的呢?看下面。

3. 基本用法

此工具类和 ArrayList 在使用方式方面很类似。

// 创建一个 CopyOnWriteArrayList 对象
CopyOnWriteArrayList phaser = new CopyOnWriteArrayList();
// 新增
copyOnWriteArrayList.add(1);
// 设置(指定下标)
copyOnWriteArrayList.set(0, 2);
// 获取(查询)
copyOnWriteArrayList.get(0);
// 删除
copyOnWriteArrayList.remove(0);
// 清空
copyOnWriteArrayList.clear();
// 是否为空
copyOnWriteArrayList.isEmpty();
// 是否包含
copyOnWriteArrayList.contains(1);
// 获取元素个数
copyOnWriteArrayList.size();

是不是很简单,那 CopyOnWriteArrayList 应用在哪些场合比较合适呢?下面我们给出最常用的场景说明。

4. 常用场景

CopyOnWriteArrayList 并发容器用于读多写少的并发场景。因为采用了写时复制的实现原理,当存在大量写的时候,内存中会频繁复制原有数据的副本,如果原有数据集很大,则很容易造成内存飙升甚至内存异常。在日常研发中,可用于静态数据字典的缓存场合,如黑白名单过滤判定。

注意,CopyOnWriteArrayList 不能保证写入的数据实时读取到,只保证数据的最终一致。是因为写入时需要复制一份原有内容,以及写入后的新老内容互换都需要一定时间。

我们举一个 IP 黑名单判定的例子:当应用接入外部请求后,为了防范风险,一般会对请求做一些特征判定,如对请求 IP 是否合法的判定就是一种。IP 黑名单偶尔会被系统运维人员做更新。我们使用 CopyOnWriteArrayList 工具类实现此场景,请看下面代码。

5. 场景案例

import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListTest {

    // 创建一个 CountDownLatch 对象,代表黑名单列表
    private static CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
    // 模拟初始化的黑名单数据
    static {
        copyOnWriteArrayList.add("ipAddr0");
        copyOnWriteArrayList.add("ipAddr1");
        copyOnWriteArrayList.add("ipAddr2");
    }

    // 主线程
    public static void main(String[] args) throws InterruptedException {
        Runnable task = new Runnable() {
            public void run() {
                // 模拟接入用时
                try {
                    Thread.sleep(new Random().nextInt(5000));
                } catch (Exception e) {}

                String currentIP = "ipAddr" + new Random().nextInt(5);
                if (copyOnWriteArrayList.contains(currentIP)) {
                    System.out.println(Thread.currentThread().getName() + " IP " + currentIP + "命中黑名单,拒绝接入处理");
                    return;
                }
                System.out.println(Thread.currentThread().getName() + " IP " + currentIP + "接入处理...");
            }
        };
        new Thread(task, "请求1").start();
        new Thread(task, "请求2").start();
        new Thread(task, "请求3").start();

        Runnable updateTask = new Runnable() {
            public void run() {
                // 模拟用时
                try {
                    Thread.sleep(new Random().nextInt(2000));
                } catch (Exception e) {}

                String newBlackIP = "ipAddr3";
                copyOnWriteArrayList.add(newBlackIP);
                System.out.println(Thread.currentThread().getName() + " 添加了新的非法IP " + newBlackIP);
            }
        };
        new Thread(updateTask, "IP黑名单更新").start();

        Thread.sleep(1000000);
    }
}

运行上面代码,我们观察一下运行结果。

请求2 IP ipAddr1命中黑名单,拒绝接入处理
IP黑名单更新 添加了新的非法IP ipAddr3
请求3 IP ipAddr3命中黑名单,拒绝接入处理
请求1 IP ipAddr4接入处理...

观察结果,和我们的预期一致。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jysf98746

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值