zoukankan      html  css  js  c++  java
  • [图解Java]读写锁ReentrantReadWriteLock

    图解ReentrantReadWriteLock

    如果之前使用过读写锁, 那么可以直接看本篇文章. 如果之前未使用过, 那么请配合我的另一篇文章一起看: [源码分析]读写锁ReentrantReadWriteLock

    0. demo

    我先给出一个demo, 这样大家就可以根据我给的这段代码, 边调试边看源码了. 还是那句话: 注意"My" , 我把ReentrantReadWriteLock类 改名为了 "MyReentrantReadWriteLock"类 , "Lock"类 改名为了"MyLock"类. 大家粘贴我的代码的时候, 把相应的"My"都去掉就好了, 否则会编译报错哦.

    demo里是一个公平读写锁

    import java.util.HashMap;
    import java.util.Map;
    import java.util.Scanner;
    import java.util.concurrent.locks.Lock;
    import java.util.function.Supplier;
    
    public class ReentrantReadWriteLockTest2 {
        static final Scanner scanner = new Scanner(System.in);
        static volatile String cmd = "";
        private static MyReentrantReadWriteLock lock = new MyReentrantReadWriteLock(true);
        private static MyReentrantReadWriteLock.ReadLock readLock = lock.readLock();
        private static MyReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    
        public static void main(String[] args) {
            for (Map.Entry<String, Lock> entry : new HashMap<String, Lock>() {{
                for (int i = 0; i < 10; i++) {
                    put("r" + i, readLock);
                    put("w" + i, writeLock);
                }
            }}.entrySet()) {
                new Thread(() -> func(entry::getValue, entry.getKey())).start();
            }
    
            while (scanner.hasNext()) {
                cmd = scanner.nextLine();
            }
        }
    
        public static void func(Supplier<Lock> myLockSupplier, String name) {
            String en_type = myLockSupplier.get().getClass().getSimpleName().toLowerCase().split("lock")[0];
            String zn_type = (en_type.equals("read") ? "读" : "写");
            blockUntilEquals(() -> cmd, "lock " + en_type + " " + name);
            myLockSupplier.get().lock();
            System.out.println(name + "获取了" + zn_type + "锁");
            blockUntilEquals(() -> cmd, "unlock " + en_type + " " + name);
            myLockSupplier.get().unlock();
            System.out.println(name + "释放了" + zn_type + "锁");
        }
    
        private static void blockUntilEquals(Supplier<String> cmdSupplier, final String expect) {
            while (!cmdSupplier.get().equals(expect))
                quietSleep(1000);
        }
    
        private static void quietSleep(int mills) {
            try {
                Thread.sleep(mills);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    使用例子在下面. 

    我们可以看到r1持有了读锁之后, r2来申请读锁, 也可以成功. 说明读锁是可以共享的.

    接下来申请写锁. 申请的写锁会进入到`等待队列`.

    然后咱们再申请读锁r3, r4. 由于咱们的demo是公平的读写锁. `等待队列`中有线程在等待写锁时, 后续的申请读锁的线程也都会直接进入`等待队列`. 不会和先来的写锁线程争抢.

    大致就是这样.更详细的请看下面小节的详解.

                            (图1)

    1. 开始图解 (公平读写锁)

    -----------2018.07.29 下午---更新-----关于图片不清楚的问题------------------------

    本文下面评论中提出这篇博客的图片看着很不舒服. 我以后会注意的. 之前没想到显示图片会有差别. 

    不过这里临时先给出一个解决办法. 如果想看仔细看某一个图片, 那么请这样做(可以拿上面的图1来试试, 本篇中除了图2和图3都可以的 ):

    在图片上右键 -> 新标签中打开图片 - > 在这个新标签中鼠标指针是一个放大镜, 左键点击一下, 让图片放大.然后你会发现, 图片非常非常非常清晰....

    大概就是这样的区别:

    (图2)

    (图3)

    ----------------------下面咱们回到本篇主题---------------------------

    咱们实例化一个读写锁后, 锁的状态大致如下图:

    此时锁是空闲状态.

    如果这个时候r1来申请读锁.那么就可以直接成功, 变化如下的黑色阴影部分.

    firstReader 是线程的引用. 读锁是共享的, 可以有很多线程来获取读锁. 而firstReader是记录这些持有读锁线程中第一个获得读锁的线程的.

    firstReaderHoldCount是 firstReader引用的线程的读锁获得次数(也就是firstReader重入的次数)

     接下来如果r2来申请读锁, 会发生什么?

    r2会申请成功, 而且变化如下:

     其中cacheHoldCounter是一个引用, 总是指向最后一个获得读锁的线程的计数器.

    接下来让w1线程申请写锁. 写锁和读锁是互斥的, 所以写锁无法申请成功, 于是会进入到`等待队列`.

    由于等待队列是懒初始化, 所以这个时候才会产生等待队列的头结点:

     然后就是把w1对应的Node尾插到`等待队列`中了:

     然后再把当前节点的前驱节点的waitStatus置为-1.  -1表示后继节点在等待线程被激活. 

    然后线程w1就放心地挂起了:

    接下来咱们再让r3线程获取读锁会怎么样呢?

    (咱们现在演示的是公平锁, 如果有线程在队列里等待的话, 后续申请读锁的线程就不会直接拿到读锁, 而是进入到等待队列中. 毕竟写锁先来的嘛, 不能插队.) 

    线程r3进入到了`等待队列`中.然后线程r3挂起了. 变化如上图的黑色阴影部分所示.

    接下来咱们让r4申请读锁, 最终结果和r3一样, 就是进入到了`等待队列`的最末尾. (但是这个r4在后续的讲解中有用)

    所以r4就不用讲了, 和r3一样:

    接下来咱们释放r1的读锁:

    然后释放r2的读锁:

    (cachedHoldCounter我没有加阴影, 是因为, 他其实并不是真的变为null了, 还是指向原来的那个元素, 但是这个已经不重要了.)

    当线程r2释放读锁的时候发现读锁已经被完全释放了, 所以会激活`等待队列` 里的第一个线程.

    并且让第一个线程对应的Node作为新的Head. 淘汰掉原先的Head.

    释放w1的写锁:

    线程w1释放了读锁后, 激活了自己的后继节点r3.

    r3被激活后,开始准备获取读锁.

     把firstReader指向自己后, 把自己替换为新的Head节点:

     线程r3申请完读锁后, 查看后继节点的nextWaiter是否等于Node.SHARED. 如果是, 那么就会唤醒这个后继节点.

    所以接下来会唤醒r4: 

     现在r4被激活了, r4开始申请读锁了:

     然后r4即将成为新的Head节点:

    到这里, demo里的演示部分就完成了.

    最后, 咱们依次把r3 和 r4 也都释放了吧. 反正也剩的不多了.

    释放了r3的时候, 变化如下:

    最后咱们释放掉r4:

     (其中的cachedHoldCounter并不是真正地变为了null, 而是还在指向着原来的元素. 只是在这里显得没用了, 所以那部分没话)

    线程r4执行完之后, 所有的线程就都释放了. 锁的状态如下: 

     到这里, 整个公平读写锁的申请锁, 释放锁的过程, 就都演示完了. 

  • 相关阅读:
    Hibernate之必须导入jar包
    浏览器兼容性问题
    CSS中的浮动清除
    CSS的三种手段让元素脱离标准本文档流——浮动、绝对定位、固定定位
    块级元素和行内元素
    网页设计前端——盒子模型
    CSS的继承性和层叠性
    网站前端设计——选择器
    网站中图片的相对路径与绝对路径
    网站隐藏文件夹
  • 原文地址:https://www.cnblogs.com/noKing/p/9384442.html
Copyright © 2011-2022 走看看