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

假设现在我们的内存里有一个 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 变量来赋值保证可见性,更新的时候对读线程没有任何的影响!

最新文章

  1. sql语句executeQuery和executeUpdate之间的区别
  2. [Effective JavaScript 笔记]第41条:将原型视为实现细节
  3. bzoj4216 Pig
  4. CENTOS LINUX查询内存大小、频率
  5. Javascript 面向对象编程
  6. 类型<T> where T:class的用法
  7. Chrome开发者工具详解(3):Timeline面板
  8. MySQL----alter table modify | change的不同
  9. $in的方法总结
  10. Server from URL
  11. UI自动化测试(三)对页面中定位到的元素对象做相应操作
  12. Android开发技巧——PagerAdapter实现类的封装
  13. 安卓TV开发(七) 移动智能终端多媒体之在线解析网页视频源
  14. 秋名山老司机(BS4与正则的比拼)
  15. JarvisOJ Basic Help!!
  16. org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'catchFromPBomService': Cannot create inner bean '(inner bean)#302efb82' of type [com.thinkgem.jeesite.modules.fd
  17. c# 自动计算字符串的宽度
  18. Sublime Text 2 - 性感无比的代码编辑器!程序员必备神器!
  19. linux初始
  20. Securecrt emacs/vi 代码无法高亮、无颜色

热门文章

  1. [ipsec][strongswan] strongswan源码分析--(一)SA整体分析
  2. Minio对象存储
  3. Linux 反弹shell(二)反弹shell的本质
  4. 3.XPath
  5. idea添加tomcat和服务添加项目
  6. JAVA中使用Dom解析XML
  7. es 启动问题
  8. PostgreSQL物理坏块和文件损坏案例分享
  9. Qt文件读写操作
  10. Bash基本功能:输入输出重定向