前面我们已经分析过JUC包里面的Lock锁,ReentrantLock锁和semaphore信号量机制。Lock锁实现了比synchronized更灵活的锁机制,Reentrantlock是Lock的实现类,是一种可重入锁,都是每次只有一次线程对资源进行处理;semaphore实现了多个线程同时对一个资源的访问;今天我们要讲的ReadWriteLock锁将实现另外一种很重要的功能:读写分离锁。

假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写,也就是说:读-读能共存,读-写不能共存,写-写不能共存。这就需要一个读/写锁来解决这个问题。

ReadWriteLock简介

我们在JUC包可以看到ReadWriteLock是一个接口,他有一个实现类:ReentrantReadWriteLock,先让我们对读写访问资源的条件做个概述:

- 读取: 没有线程正在做写操作,且没有线程在请求写操作。
- 写入: 没有线程正在做读写操作。

如果某个线程想要读取资源,只要没有线程正在对该资源进行写操作且没有线程请求对该资源的写操作即可。同样当有线程想要写资源,但是此刻有线程正在读取资源,那么此刻写资源的操作是不能继续下去的。

我们来看一个例子:

public class ReadWriteLockTest2 {
public static void main(String[] args) {
final int threadCount = 2;
final ExecutorService exService = Executors.newFixedThreadPool(threadCount);
final ScoreBoard scoreBoard = new ScoreBoard();
exService.execute(new ScoreUpdateThread(scoreBoard));
exService.execute(new ScoreHealthThread(scoreBoard));
exService.shutdown();
}
} class ScoreBoard {
private boolean scoreUpdated = false;
private int score = 0;
String health = "不可用";
final ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock(); public String getMatchHealth() {
rrwl.readLock().lock();
if (scoreUpdated) {
rrwl.readLock().unlock();
rrwl.writeLock().lock();
try {
if (scoreUpdated) {
score = fetchScore();
scoreUpdated = false;
}
rrwl.readLock().lock();
} finally {
rrwl.writeLock().unlock();
}
}
try {
if (score % 2 == 0) {
health = "Bad Score";
} else {
health = "Good Score";
}
} finally {
rrwl.readLock().unlock();
}
return health;
} public void updateScore() {
try {
rrwl.writeLock().lock();
scoreUpdated = true;
} finally {
rrwl.writeLock().unlock();
}
} private int fetchScore() {
Calendar calender = Calendar.getInstance();
return calender.get(Calendar.MILLISECOND);
}
} class ScoreHealthThread implements Runnable {
private ScoreBoard scoreBoard;
public ScoreHealthThread(ScoreBoard scoreTable) {
this.scoreBoard = scoreTable;
}
@Override
public void run() {
for(int i= 0; i< 5; i++) {
System.out.println("Match Health: "+ scoreBoard.getMatchHealth());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} class ScoreUpdateThread implements Runnable {
private ScoreBoard scoreBoard;
public ScoreUpdateThread(ScoreBoard scoreTable) {
this.scoreBoard = scoreTable;
}
@Override
public void run() {
for(int i= 0; i < 5; i++) {
System.out.println("Score Updated.");
scoreBoard.updateScore();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

打印结果:

Score Updated.
Match Health: Good Score
Score Updated.
Match Health: Good Score
Score Updated.
Match Health: Good Score
Score Updated.
Match Health: Good Score
Score Updated.
Match Health: Good Score

基本用法见上例,读写分离锁很好的控制了多个线程对同一个资源的访问。

ReentrantReadWriteLock

由名字我们可以看到读写锁也有可重入的实现类。ReentrantReadWriteLock具有关联的读取和写入锁定,可以重新获取锁定。它可表现为公平和不公平的模式两者。 默认行为是不公平的。 非公平锁的性能更好,虽然有可能读写器或写入器锁可以被推迟许多次,并且持续地尝试锁定。 在公平锁定的情况下,锁定请求按照最长等待的单个写入器锁或读取锁定组请求的顺序来完成,无论谁具有最长等待时间将获得对共享资源的锁定。 在重入ReentrantReadWriteLock可以写入锁定降级读锁。 这意味着如果线程已经获得写锁定,它可以将其锁从写降级到读锁。 顺序将是首先获得写锁定,执行写操作,然后获取读锁,然后解锁写锁,并且在读操作后最终解锁读锁。

ReentrantReadWriteLock 也是基于 AbstractQueuedSynchronizer 实现的,它具有下面这些属性:

  • 获取顺序

此类不会将读取者优先或写入者优先强加给锁访问的排序。但是,它确实支持可选的公平 策略。

1.非公平模式(默认)

当非公平地(默认)构造时,未指定进入读写锁的顺序,受到 reentrancy 约束的限制。连续竞争的非公平锁可能无限期地推迟一个或多个 reader 或 writer 线程,但吞吐量通常要高于公平锁。

2.公平模式

当公平地构造线程时,线程利用一个近似到达顺序的策略来争夺进入。当释放当前保持的锁时,可以为等待时间最长的单个 writer 线程分配写入锁,如果有一组等待时间大于所有正在等待的 writer 线程 的 reader 线程,将为该组分配写入锁。

如果保持写入锁,或者有一个等待的 writer 线程,则试图获得公平读取锁(非重入地)的线程将会阻塞。直到当前最旧的等待 writer 线程已获得并释放了写入锁之后,该线程才会获得读取锁。当然,如果等待 writer 放弃其等待,而保留一个或更多 reader 线程为队列中带有写入锁自由的时间最长的 waiter,则将为那些 reader 分配读取锁。

试图获得公平写入锁的(非重入地)的线程将会阻塞,除非读取锁和写入锁都自由(这意味着没有等待线程)。(注意,非阻塞 ReentrantReadWriteLock.ReadLock.tryLock() 和 ReentrantReadWriteLock.WriteLock.tryLock() 方法不会遵守此公平设置,并将获得锁(如果可能),不考虑等待线程)。

  • 重入

此锁允许 reader 和 writer 按照 ReentrantLock 的样式重新获取读取锁或写入锁。在写入线程保持的所有写入锁都已经释放后,才允许重入 reader 使用它们。

此外,writer 可以获取读取锁,但反过来则不成立。在其他应用程序中,当在调用或回调那些在读取锁状态下执行读取操作的方法期间保持写入锁时,重入很有用。如果 reader 试图获取写入锁,那么将永远不会获得成功。

  • 锁降级

重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不可能的。

  • 锁获取的中断

读取锁和写入锁都支持锁获取期间的中断。

  • Condition 支持

写入锁提供了一个 Condition 实现,对于写入锁来说,该实现的行为与 ReentrantLock.newCondition() 提供的 Condition 实现对 ReentrantLock 所做的行为相同。当然,此 Condition 只能用于写入锁。读取锁不支持 Condition,readLock().newCondition() 会抛出 UnsupportedOperationException。

  • 监测

此类支持一些确定是保持锁还是争用锁的方法。这些方法设计用于监视系统状态,而不是同步控制。

此类行为的序列化方式与内置锁的相同:反序列化的锁处于解除锁状态,无论序列化该锁时其状态如何。

下面的代码展示了如何利用重入来执行升级缓存后的锁降级(为简单起见,省略了异常处理):

class CachedData {
Object data;
volatile boolean cacheValid;
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// 在获得写锁之前必须释放读锁
rwl.readLock().unlock();
rwl.writeLock().lock();
// Recheck state because another thread might have acquired
// write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
//通过在释放写锁之前获得读锁来降级
rwl.readLock().lock();
rwl.writeLock().unlock(); // 解锁写锁,但是任然持有读锁
} use(data);
rwl.readLock().unlock();
}
}

与互斥锁对比

互斥锁一次只允许一个线程访问共享数据,哪怕进行的是只读操作;读写锁允许对共享数据进行更高级别的并发访问:对于写操作,一次只有一个线程(write线程)可以修改共享数据,对于读操作,允许任意数量的线程同时进行读取。

与互斥锁相比,使用读写锁能否提升性能则取决于读写操作期间读取数据相对于修改数据的频率,以及数据的争用——即在同一时间试图对该数据执行读取或写入操作的线程数。

java并发编程(七)----(JUC)ReadWriteLock的更多相关文章

  1. Java并发编程(3) JUC中的锁

    一 前言 前面已经说到JUC中的锁主要是基于AQS实现,而AQS(AQS的内部结构 .AQS的设计与实现)在前面已经简单介绍过了.今天记录下JUC包下的锁是怎么基于AQS上实现的 二 同步锁 同步锁不 ...

  2. 【转】java并发编程系列之ReadWriteLock读写锁的使用

    前面我们讲解了Lock的使用,下面我们来讲解一下ReadWriteLock锁的使用,顾明思义,读写锁在读的时候,上读锁,在写的时候,上写锁,这样就很巧妙的解决synchronized的一个性能问题:读 ...

  3. Java并发编程 (七) J.U.C之AQS

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一. J.U.C之AQS-介绍 1.定义: AbstractQueuedSynchronizer简称AQ ...

  4. java并发编程工具类JUC第七篇:BlockingDeque双端阻塞队列

    在之前的文章中已经为大家介绍了java并发编程的工具:BlockingQueue接口.ArrayBlockingQueue.DelayQueue.LinkedBlockingQueue.Priorit ...

  5. java并发编程笔记(七)——线程池

    java并发编程笔记(七)--线程池 new Thread弊端 每次new Thread新建对象,性能差 线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或者OOM 缺 ...

  6. java并发编程系列七:volatile和sinchronized底层实现原理

    一.线程安全 1.  怎样让多线程下的类安全起来 无状态.加锁.让类不可变.栈封闭.安全的发布对象 2. 死锁 2.1 死锁概念及解决死锁的原则 一定发生在多个线程争夺多个资源里的情况下,发生的原因是 ...

  7. Java并发编程中的设计模式解析(二)一个单例的七种写法

    Java单例模式是最常见的设计模式之一,广泛应用于各种框架.中间件和应用开发中.单例模式实现起来比较简单,基本是每个Java工程师都能信手拈来的,本文将结合多线程.类的加载等知识,系统地介绍一下单例模 ...

  8. Java并发编程(七)ConcurrentLinkedQueue的实现原理和源码分析

    相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Java并发编程(三)volatile域 Java并发编程(四)Java内存模型 Java并发编程(五)Concurr ...

  9. java并发编程工具类JUC第四篇:LinkedBlockingQueue链表队列

    在之前的文章中已经为大家介绍了java并发编程的工具:BlockingQueue接口.ArrayBlockingQueue.DelayQueue. LinkedBlockingQueue 队列是Blo ...

  10. java并发编程工具类JUC第八篇:ConcurrentHashMap

    在之前的文章中已经为大家介绍了java并发编程的工具:BlockingQueue接口.ArrayBlockingQueue.DelayQueue.LinkedBlockingQueue.Priorit ...

随机推荐

  1. Ural 2072:Kirill the Gardener 3(DP)

    http://acm.timus.ru/problem.aspx?space=1&num=2072 题意:有n朵花,每朵花有一个饥渴值.现在浇花,优先浇饥渴值小的(即从小到大浇),浇花需要耗费 ...

  2. orm的操作

    ORM的对应关系 ​ 类 —— > 表 ​ 对象 ——> 记录(数据行) ​ 属性 ——> 字段 class Book(model.Model): title=models.Char ...

  3. 爬虫之抓js教程

    在初学的爬虫过程中,很多人还不知道有些字段是如何生成的,怎样模拟生成这些字段来拼接头部.为了再次纪念[宏彦获水]成语初次面世,特地用[百度登陆]写下一篇登陆百度的教程,以供大家参考. 前面学习了如何在 ...

  4. .Net项目中NLog的配置与使用

    引言: 因为之前在项目开发中一直都是使用的Log4Net作为项目的日志记录框架,最近忽然感觉对它已经有点腻了,所以尝试着使用了NLog作为新项目的日志记录框架(当然作为一名有志向的攻城狮永远都不能只局 ...

  5. c++学习书籍推荐《C++ Templates》下载

    详细讲解C++模板语言的概念. 使用C++模板的常用设计技巧. 应用例证(其中一些是“高 级”应用). 百度云及其他网盘下载地址:点我 名人推荐 如果今年你只打算买一本C++的书,那就选<C++ ...

  6. spring的jar包的下载、说明

    spring的jar包官方下载地址:完整链接:https://repo.spring.io/webapp/#/artifacts/browse/tree/General/libs-release-lo ...

  7. VS2010 winform开发笔记---combox的SelectedIndexChanged事件及级联问题

    DisplayMember绑定需要显示的数据表字段, 而ValueMember绑定需要获取选择的项的值. 然后通过 combo.SelectedValue就可以取得选中项的值了. DisplayMem ...

  8. error: 'commit' is not possible because you have unmerged files.

    解决方案: 1.把修改的文件add下,如:git add bidder_mod/src/common/dragon_bidder_data.cc2.git commit

  9. 【LightOJ - 1370】Bi-shoe and Phi-shoe

    Bi-shoe and Phi-shoe Descriptions: 给出一些数字,对于每个数字找到一个欧拉函数值大于等于这个数的数,求找到的所有数的最小和. Input 输入以整数T(≤100)开始 ...

  10. Vue匿名组件使用keep-alive后动态清除缓存

    在使用Vue开发管理系统项目的时候,为了保存页面的浏览状态,我们可以使用内置组件keep-alive来缓存组件内部状态,避免重新渲染. <keep-alive> <router-vi ...