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

假设现在我们的内存里有一个 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. 数据库PDO简介

    php简介,php历史,php后端工程师职业前景,php技术方向,php后端工程师职业体系介绍. php是世界上使用最广泛的web开发语言,是超文本预处理器,是一种通用的开源脚本语言,语法吸收了c语言 ...

  2. linux系统编程之信号(四)

    今天继续探讨信号相关的东东,话不多说,正入正题: 信号在内核中的表示: 下面用图来进一步描述这种信号从产生到递达之间的状态(信号阻塞与未诀):   那是怎么来决定的呢?下面慢慢来举例分解: 所以,通过 ...

  3. 小程序框架之视图层 View~基础组件

    框架为开发者提供了一系列基础组件,开发者可以通过组合这些基础组件进行快速开发.详细介绍请参考组件文档. 什么是组件: 组件是视图层的基本组成单元. 组件自带一些功能与微信风格一致的样式. 一个组件通常 ...

  4. java.lang.NoClassDefFoundError: org/apache/zookeeper/proto/SetWatches

    Session 0x16b21fa441900b6 for server 192.168.240.126/192.168.240.126:2181, unexpected error, closing ...

  5. HDU2870 Largest Submatrix

    Largest Submatrix Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others ...

  6. CSS 相邻元素选择器

    相邻兄弟选择器(Adjacent sibling selector)可选择紧接在另一元素后的元素,且二者有相同父元素.选择相邻兄弟 如果需要选择紧接在另一个元素后的元素,而且二者有相同的父元素,可以使 ...

  7. 黄杉杉 --java第八次作业

    题目:编写一个应用程序,创建一个矩形类,类中具有长.宽两个成员变量和求周长的方法.再创建一个矩形类的子类——正方形类,类中定义求面积方法.重写求周长的方法.在主类中,输入一个正方形边长,创建正方形对象 ...

  8. tensorflow LSTM+CTC使用详解

    最近用tensorflow写了个OCR的程序,在实现的过程中,发现自己还是跳了不少坑,在这里做一个记录,便于以后回忆.主要的内容有lstm+ctc具体的输入输出,以及TF中的CTC和百度开源的warp ...

  9. 用1 x 2的多米诺骨牌填满M x N矩形的方案数(完美覆盖)

    题意 用 $1 \times 2$ 的多米诺骨牌填满 $M \times N$ 的矩形有多少种方案,$M \leq 5,N < 2^{31}$,输出答案模 $p$. 分析 当 $M=3$时,假设 ...

  10. 09 webpack的介绍

    webpack干嘛的?:  模块打包机,分析目录结构,找到js模块(包括浏览器不能直接识别的代码 typscript sass...),打包成合适的格式供浏览器访问 webpack是一款模块加载器兼打 ...