一、读写锁

1、初识读写锁

  a)Java中的锁——Lock和synchronized中介绍的ReentrantLock和synchronized基本上都是排它锁,意味着这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,在写线程访问的时候其他的读线程和写线程都会被阻塞。读写锁维护一对锁(读锁和写锁),通过锁的分离,使得并发性提高。

  b)关于读写锁的基本使用:在不使用读写锁的时候,一般情况下我们需要使用synchronized搭配等待通知机制完成并发控制(写操作开始的时候,所有晚于写操作的读操作都会进入等待状态),只有写操作完成并通知后才会将等待的线程唤醒继续执行。

  如果改用读写锁实现,只需要在读操作的时候获取读锁,写操作的时候获取写锁。当写锁被获取到的时候,后续操作(读写)都会被阻塞,只有在写锁释放之后才会执行后续操作。并发包中对ReadWriteLock接口的实现类是ReentrantReadWriteLock,这个实现类具有下面三个特点

  ①具有与ReentrantLock类似的公平锁和非公平锁的实现:默认的支持非公平锁,对于二者而言,非公平锁的吞吐量由于公平锁;

  ②支持重入:读线程获取读锁之后能够再次获取读锁,写线程获取写锁之后能再次获取写锁,也可以获取读锁。

  ③锁能降级:遵循获取写锁、获取读锁在释放写锁的顺序,即写锁能够降级为读锁

2、读写锁源码分析

a)ReadWriteLock接口中只有两个方法,分别是readLock和writeLock

  1. public interface ReadWriteLock {
  2. /**
  3. * 返回读锁
  4. */
  5. Lock readLock();
  6.  
  7. /**
  8. * 返回写锁
  9. */
  10. Lock writeLock();
  11. }

b)关于读写读写状态的设计

  ①作为已经实现的同步组件,读写锁同样是需要实现同步器来实现同步功能,同步器的同步状态就是读写锁的读写状态,只是读写锁的同步器需要在同步状态上维护多个读线程和写线程的状态。使用按位切割的方式将一个整形变量按照高低16位切割成两个部分。对比下图,低位值表示当前获取写锁的线程重入两次,高位的值表示当前获取读锁的线程重入一次。读写锁的获取伴随着读写状态值的更新。当低位为0000_0000_0000_0000的时候表示写锁已经释放,当高位为0000_0000_0000_0000的时候表示读锁已经释放。

  ②从下面的划分得到:当state值不等于0的时候,如果写状态(state & 0x0000FFFF)等于0的话,读状态是大于0的,表示读锁被获取;如果写状态不等于0的话,读锁没有被获取。这个特点也在源码中实现。

c)写锁writeLock

  ①上面说到过,读写锁是支持重入的锁,而对于写锁而言还是排他的,这样避免多个线程同时去修改临界资源导致程序出现错误。如果当前线程已经获取了写锁,则按照上面读写状态的设计增加写锁状态的值;如果当前线程在获取写锁的时候,读锁已经被获取或者该线程之前已经有别的线程获取到写锁,当前线程就会进入等待状态。

  1. static final int SHARED_SHIFT = 16;
  2. static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; //1左移16位减1=>0000_0000_0000_0000_1111_1111_1111_1111
  3. static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } //返回读状态的值
  4. protected final boolean tryAcquire(int acquires) {
  5. /*
  6. * Walkthrough:
  7. * 1. 如果读状态不为0或者写状态不为0并且写线程不是自己,返回false
  8. * 2. 如果已经超过了可重入的计数值MAX_COUNT,就会返回false
  9. * 3. 如果该线程是可重入获取或队列策略允许,则该线程有资格获得锁定;同时更新所有者和写锁状态值
  10. */
  11. Thread current = Thread.currentThread(); //获取当前线程
  12. int c = getState(); //获取当前写锁状态值
  13. int w = exclusiveCount(c); //获取写状态的值
  14. //当同步状态state值不等于0的时候,如果写状态(state & 0x0000FFFF)等于0的话,读状态是大于0的,表示读锁被获取
  15. if (c != 0) {
  16. if (w == 0 || current != getExclusiveOwnerThread())
  17. return false;
  18. if (w + exclusiveCount(acquires) > MAX_COUNT) //如果已经超过了可重入的计数值MAX_COUNT,就会返回false
  19. throw new Error("Maximum lock count exceeded");
  20. // 重入锁:更新状态值
  21. setState(c + acquires);
  22. return true;
  23. }
  24. if (writerShouldBlock() ||
  25. !compareAndSetState(c, c + acquires))
  26. return false;
  27. setExclusiveOwnerThread(current);
  28. return true;
  29. }

  ②分析一下上面的写锁获取源码

  tryAcquire中线程获取写锁的条件:读锁没有线程获取,写锁被获取并且被获取的线程是自己,那么该线程可以重入的获取锁,而判断读锁是否被获取的条件就是(当同步状态state值不等于0的时候,如果写状态(state & 0x0000FFFF)等于0的话,读状态是大于0的,表示读锁被获取)。对于读写锁而言,需要保证写锁的更新结果操作对读操作是可见的,这样的话写锁的获取就需要保证其他的读线程没有获取到读锁。

  ③写锁的释放源码

  写锁的释放和ReentrantLock的锁释放思路基本相同,从源码中可以看出来,每次释放都是减少写状态,直到写状态值为0(exclusiveCount(nextc) == 0)的时候释放写锁,后续阻塞等待的读写线程可以继续竞争锁。

  1. static final int SHARED_SHIFT = 16;
  2. static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; //1左移16位减1=>0000_0000_0000_0000_1111_1111_1111_1111
  3. static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } //返回读状态的值
  4. protected final boolean tryRelease(int releases) {
  5. if (!isHeldExclusively())
  6. throw new IllegalMonitorStateException();
  7. int nextc = getState() - releases;
  8. boolean free = exclusiveCount(nextc) == 0; //写状态值为0,就释放写锁,并将同步状态的线程持有者置为null,然后更新状态值
  9. if (free)
  10. setExclusiveOwnerThread(null);
  11. setState(nextc);
  12. return free;
  13. }

d)读锁readLock

  ①读锁是同样是支持重入的,除此之外也是共享式的,能够被多个线程获取。在同一时刻的竞争队列中,如果没有写线程想要获取读写锁,那么读锁总会被读线程获取到(然后更新读状态的值)。每个读线程都可以重入的获取读锁,而对应的获取次数保存在本地线程中,由线程自身维护该值。

  ②获取读锁的条件:其他线程已经获取了写锁,则当前线程获取读锁会失败而进入等待状态;如果当前线程获取了写锁或者写锁没有被获取,那么就可以获取到读锁,并更细同步状态(读状态值)。

  ③读锁的每次释放都是减少读状态,

f)锁的降级

  锁降级的概念:如果当先线程是写锁的持有者,并保持获得写锁的状态,同时又获取到读锁,然后释放写锁的过程。(注意不同于这样的分段过程:当前线程拥有写锁,释放掉写锁之后再获取读锁的过程,这种分段过程不能称为锁降级)。

  1. class CachedData {
  2. Object data;
  3. volatile boolean cacheValid;
  4. final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
  5.  
  6. void processCachedData() {
  7. // 获取读锁
  8. rwl.readLock().lock();
  9. if (!cacheValid) {
  10. // 在获取写锁之前必须释放读锁,不释放的话下面写锁会获取不成功,造成死锁
  11. rwl.readLock().unlock();
  12. // 获取写锁
  13. rwl.writeLock().lock();
  14. try {
  15. // 重新检查state,因为在获取写锁之前其他线程可能已经获取写锁并且更改了state
  16. if (!cacheValid) {
  17. data = ...
  18. cacheValid = true;
  19. }
  20. // 通过在释放写锁定之前获取读锁定来降级
  21. // 这里再次获取读锁,如果不获取,那么当写锁释放后可能其他写线程再次获得写锁,导致下方`use(data)`时出现不一致的现象
  22. // 这个操作就是降级
  23. rwl.readLock().lock();
  24. } finally {
  25. rwl.writeLock().unlock(); // 释放写锁,由于在释放之前读锁已经被获取,所以现在是读锁获取状态
  26. }
  27. }
  28.  
  29. try {
  30. // 使用完后释放读锁
  31. use(data);
  32. } finally {
  33. rwl.readLock().unlock(); //释放读锁
  34. }
  35. }
  36. }}

Java中的读写锁的更多相关文章

  1. java中ReentrantReadWriteLock读写锁的使用

    Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象.两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象. 读写锁:分为读 ...

  2. java中的读/写锁

    读写锁接口:ReadWriteLock,它的具体实现类为:ReentrantReadWriteLock 使用场景: 对于一个资源,读读能共存,读写不能共存,写写不能共存. 锁降级:从写锁变成读锁: 锁 ...

  3. 22、Java并发性和多线程-Java中的读/写锁

    以下内容转自http://ifeve.com/read-write-locks/: 相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些.假设你的程序中涉及到对一些共享资源 ...

  4. 【练习】Java中的读文件,文件的创建,写文件

    前言 大家好,给大家带来Java中的读文件,文件的创建,写文件的概述,希望你们喜欢 读文件 public static void read(String path,String filename){ ...

  5. Java中的读文件,文件的创建,写文件

    前言 大家好,我是 Vic,今天给大家带来Java中的读文件,文件的创建,写文件的概述,希望你们喜欢 示意图 读文件 public static void read(String path,Strin ...

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

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

  7. java多线程:并发包中ReentrantReadWriteLock读写锁的原理

    一:读写锁解决的场景问题--->数据的读取频率远远大于写的频率的场景,就可以使用读写锁.二:读写锁的结构--->用state一个变量.将其转化成二进制,前16位为高位,标记读线程获取锁的次 ...

  8. java多线程:并发包中ReentrantReadWriteLock读写锁的锁降级模板

    写锁降级为读锁,但读锁不可升级或降级为写锁. 锁降级是为了让当前线程感知到数据的变化. //读写锁 private ReentrantReadWriteLock lock=new ReentrantR ...

  9. java并发编程-读写锁

    最近项目中需要用到读写锁 读写锁适用于读操作多,写操作少的场景,假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁.在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以 ...

随机推荐

  1. vue 部署404

    https://www.cnblogs.com/kevingrace/p/6126762.html 在nginx部署https://www.jianshu.com/p/7017143e3f7a 在ap ...

  2. 【DOS】Win7系统文件夹名太长无法删除问题的解决

    一个测试工具产生了几个坑爹文件夹名为n个“x” ,系统提示删除不掉. 网上百度,说什么压缩.写bat文件...统统没用. 猛地看到右击菜单中安装了git客户端工具,想试试看.在该文件夹目录下Git B ...

  3. C#发送带附件的邮件的代码

    如下的代码是关于C#发送带附件的邮件的代码. MailMessage m = new MailMessage();m.Subject = "File attachment!";m. ...

  4. neutron--ml2 plugin

    ml2 plugin 对 plugin 的功能进行抽象和封装,有 ml2 plugin ,各种 network 无需开发自己的 plugin,只需开发 ml2 plugin 相对应的 driver , ...

  5. 2018-2019-2 网络对抗技术 20165206 Exp6 信息搜集与漏洞扫描

    - 2018-2019-2 网络对抗技术 20165206 Exp6 信息搜集与漏洞扫描 - 实验任务 (1)各种搜索技巧的应用 (2)DNS IP注册信息的查询 (3)基本的扫描技术:主机发现.端口 ...

  6. cmake让add_subdirectory()的所有target生成到同一目录

    问题描述和解决办法 整个项目包括: 库.测试程序,都是基于源码生成:测试程序肯定是executable类型了,而如果生成的库是SHARED类型,在windows下就是.dll(以及对应的.lib)文件 ...

  7. Python2.7和3.7区别

    区别一:print语法使用 Python2.7   print语法使用   >>> print "Hello Python" Python3.7   print语 ...

  8. lxml库

    lxml库 lxml是一个HTML/XML的解析器,主要的功能是如何解析和提取 HTML/XML数据. 基本使用: 1.我们可以利用他来解析HTML代码,并且在解析HTML代码的时候,如果HTML代码 ...

  9. C语言--第五次作业--指针

    1.本章学习总结 1.1 思维导图 1.2本章学习体会及代码量学习体会 1.2.1学习体会 没想到都已经学习完C语言的灵魂-指针的内容了(当然也是C里面最难学习的内容了).虽然在之前就有听学习进度比较 ...

  10. twig模板的进一步学习以及在symfony当中的使用

    首先,twig可以理解为用于输出html代码的,虽然用PHP等其他语言也可以输出,但是twig更为简洁高效,同时twig模板被编译成原生的php类缓存起来,所以才会这么快, 其实twig跟php类差不 ...