读多写少的场景下引发的问题?

假设现在我们的内存里有一个 ArrayList,这个 ArrayList 默认情况下肯定是线程不安全的,要是多个线程并发读和写这个 ArrayList 可能会有问题。

那么,问题来了,我们应该怎么让这个 ArrayList 变成线程安全的呢?

有一个非常简单的办法,对这个 ArrayList 的访问都加上线程同步的控制,比如说一定要在 Synchronized 代码段来对这个 ArrayList 进行访问,这样的话,就能同一时间就让一个线程来操作它了,或者是用 ReadWriteLock 读写锁的方式来控制,都可以。

我们假设就是用 ReadWriteLock 读写锁的方式来控制对这个 ArrayList 的访问,这样多个读请求可以同时执行从 ArrayList 里读取数据,但是读请求和写请求之间互斥,写请求和写请求也是互斥的。

代码大概就是类似下面这样:

public Object  read() {
lock.readLock().lock();
// 对ArrayList读取
lock.readLock().unlock();
}
public void write() {
lock.writeLock().lock();
// 对ArrayList写
lock.writeLock().unlock();
}

类似上面的代码有什么问题呢?

最大的问题,其实就在于写锁和读锁的互斥。假设写操作频率很低,读操作频率很高,是写少读多的场景。那么偶尔执行一个写操作的时候,是不是会加上写锁,此时大量的读操作过来是不是就会被阻塞住,无法执行?这个就是读写锁可能遇到的最大的问题。

引入 CopyOnWrite 思想解决问题

这个时候就要引入 CopyOnWrite 思想来解决问题了。它的思想就是,不用加什么读写锁,把锁统统去掉,有锁就有问题,有锁就有互斥,有锁就可能导致性能低下,会阻塞请求,导致别的请求都卡着不能执行。

那么它怎么保证多线程并发的安全性呢?

很简单,顾名思义,利用“CopyOnWrite”的方式,这个英语翻译成中文,大概就是“写数据的时候利用拷贝的副本来执行”。你在读数据的时候,其实不加锁也没关系,大家左右都是一个读罢了,互相没影响。问题主要是在写的时候,写的时候你既然不能加锁了,那么就得采用一个策略。假如说你的 ArrayList 底层是一个数组来存放你的列表数据,那么这时比如你要修改这个数组里的数据,你就必须先拷贝这个数组的一个副本。然后你可以在这个数组的副本里写入你要修改的数据,但是在这个过程中实际上你都是在操作一个副本而已。

这样的话,读操作是不是可以同时正常的执行?这个写操作对读操作是没有任何的影响的吧!

看下面的图,来体会一下这个过程:

关键问题来了,那那个写线程现在把副本数组给修改完了,现在怎么才能让读线程感知到这个变化呢?

这里要配合上 Volatile 关键字的使用, Volatile 关键字的核心就是让一个变量被写线程给修改之后,立马让其他线程可以读到这个变量引用的最近的值,这就是 Volatile 最核心的作用。

所以一旦写线程搞定了副本数组的修改之后,那么就可以用 Volatile 写的方式,把这个副本数组赋值给 Volatile 修饰的那个数组的引用变量了。只要一赋值给那个 Volatile 修饰的变量,立马就会对读线程可见,大家都能看到最新的数组了。

下面是 JDK 里的 CopyOnWriteArrayList 的源码:

// 这个数组是核心的,因为用volatile修饰了
// 只要把最新的数组对他赋值,其他线程立马可以看到最新的数组
private transient volatile Object[] array; public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 对数组拷贝一个副本出来
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 对副本数组进行修改,比如在里面加入一个元素
newElements[len] = e;
// 然后把副本数组赋值给volatile修饰的变量
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

我们可以看看写数据的时候,它是怎么拷贝一个数组副本,然后修改副本,接着通过 Volatile 变量赋值的方式,把修改好的数组副本给更新回去,立马让其他线程可见的。

因为是通过副本来进行更新的,万一要是多个线程都要同时更新呢?那搞出来多个副本会不会有问题?

当然不能多个线程同时更新了,这个时候就是看上面源码里,加入了 Lock 锁的机制,也就是同一时间只有一个线程可以更新。

那么更新的时候,会对读操作有任何的影响吗?

绝对不会,因为读操作就是非常简单的对那个数组进行读而已,不涉及任何的锁。而且只要他更新完毕对 Volatile 修饰的变量赋值,那么读线程立马可以看到最新修改后的数组,这是 Volatile 保证的:

private E get(Object[] a, int index) {
// 最简单的对数组进行读取
return (E) a[index];
}

这样就完美解决了我们之前说的读多写少的问题。如果用读写锁互斥的话,会导致写锁阻塞大量读操作,影响并发性能。
但是如果用了 CopyOnWriteArrayList,就是用空间换时间,更新的时候基于副本更新,避免锁,然后最后用 Volatile 变量来赋值保证可见性,更新的时候对读线程没有任何的影响!

CopyOnWrite 思想及在 Java 并发包中的具体体现的更多相关文章

  1. Java 并发包中的读写锁及其实现分析

    1. 前言 在Java并发包中常用的锁(如:ReentrantLock),基本上都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时 刻可以允许多个读线程访问,但是在写线程访问时,所有 ...

  2. Java并发编程(您不知道的线程池操作), 最受欢迎的 8 位 Java 大师,Java并发包中的同步队列SynchronousQueue实现原理

    Java_并发编程培训 java并发程序设计教程 JUC Exchanger 一.概述 Exchanger 可以在对中对元素进行配对和交换的线程的同步点.每个线程将条目上的某个方法呈现给 exchan ...

  3. Java 并发包中的高级同步工具

    Java 并发包中的高级同步工具 Java 中的并发包指的是 java.util.concurrent(简称 JUC)包和其子包下的类和接口,它为 Java 的并发提供了各种功能支持,比如: 提供了线 ...

  4. Java并发包中CopyOnWrite容器相关类简介

    简介: 本文是主要介绍,并发容器CopyOnWriteArrayList和CopyOnWriteArraySet(不含重复元素的并发容器)的基本原理和使用示例. 欢迎探讨,如有错误敬请指正 如需转载, ...

  5. Java并发包中Semaphore的工作原理、源码分析及使用示例

    1. 信号量Semaphore的介绍 我们以一个停车场运作为例来说明信号量的作用.假设停车场只有三个车位,一开始三个车位都是空的.这时如果同时来了三辆车,看门人允许其中它们进入进入,然后放下车拦.以后 ...

  6. Java并发包中Lock的实现原理

    1. Lock 的简介及使用 Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制.本质上Lock仅仅是一个接口(位于源码包中的java\util\concurrent\l ...

  7. Java并发包中常用类小结(一)

    从JDK1.5以后,Java为我们引入了一个并发包,用于解决实际开发中经常用到的并发问题,那我们今天就来简单看一下相关的一些常见类的使用情况. 1.ConcurrentHashMap Concurre ...

  8. Java并发包中线程池的种类和特点介绍

    Java并发包提供了包括原子量.并发集合.同步器.可重入锁.线程池等强大工具这里学习一下线程池的种类和特性介绍. 如果每项任务都分配一个线程,当任务特别多的时候,可能会超出系统承载能力.而且线程的创建 ...

  9. Java并发包中CountDownLatch的工作原理、使用示例

    1. CountDownLatch的介绍 CountDownLatch是一个同步工具,它主要用线程执行之间的协作.CountDownLatch 的作用和 Thread.join() 方法类似,让一些线 ...

随机推荐

  1. Graphviz install the Windows for Scyther

    1.在Pycharm 中使用Scyther工具的时候需要导入 graphviz 直接在 Interpreter 上安装的售后会报错,如果在 IDE上无法支架安装的库可以试图在控制台上安装,控制台上无法 ...

  2. [ipsec][strongswan] VirtualPN隧道网络加速FEC(forward error correction)

    引用 跟一个网友就有关IPsec的网络加速以及降低延迟等问题进行了一些讨论,并总结了一写粗浅的看法. 因为FEC的资料并不多,所以分享出来,希望能被有需要的人看见:) 先说一下FEC. 我们使用ips ...

  3. 文件操作之stat()函数

    作用: 返回一个文件的详细信息 头文件: #include <sys/types.h> #include <sys/stat.h> #include <unistd.h& ...

  4. onclick与click的区别

    用法: Obj.click(function(){ }); Obj.onclick=function(){ } 相同:效果一样. 区别: 用户或浏览器执行的某种动作,例如click load,mous ...

  5. 4.1 vue-resource

    全局拦截器.配置全局地址等:

  6. nginx配置静态资源:配置绝对路径

    nginx配置静态资源:配置绝对路径 项目都是html格式的文件,我的项目路径:E:\javaservice\nginx-1.15.7\html assets:静态资源 html:站点文件 uploa ...

  7. kafka2.12 集群搭建

    前提: 1.下载 kafka http://kafka.apache.org/downloads 2.下载配置zookeeper http://www.cnblogs.com/eggplantpro/ ...

  8. learning java BigDecimal类

    使用BiDecimal类是了为防止运算时精度丢失: var f1 = new BigDecimal("0.05"); var f2 = BigDecimal.valueOf(0.0 ...

  9. cube.js 学习(四)cube.js cube 说明

      cube 是cube.js data schema 的核心,里面定义了生成sql 的说明 一个比较全的schema 例子 cube(`Users`, { sql: `select * from u ...

  10. 开始编写Makefile

    1.Makefile 的编写规则一 目标列表:关联性列表 命令列表 目标列表:可以是多个以空格隔开多个目标文件 关联列表页称为先决条件:同样是用个或多个空格分开的目标文件 命令列表:用<tab& ...