Java并发(十一):读写锁——写锁

概述

ReentrantLock和Mutex都是一个排他锁,也就是说,同一时刻只有一个线程可以去获取这把锁

但读写锁并不完全是排他锁,同一时刻是允许多个读线程来进行访问的,读写锁实际上指的是一对锁,读锁和写锁

读锁可以被共享,但只要写锁被获取了,那么读锁和写锁都将被阻塞。

读写锁不仅可以保证写操作对读操作的可见性外,还可以简化读写交互场景,比如对于一个缓存的结构来说,缓存一般是以读服务为主,读写锁可以保证,写操作对于后续的读操作是可见的(因为写的时候不允许读,只要写完才可以读)

读写锁是在Java5之后才拥有的,在没有读写锁之前,Java采用的是等待通知机制来实现上面的操作的,即写操作对于后续的读操作是可见的,具体是当写操作开始的时候,所有晚于该写操作的读操作均会进入等待状态,只有等写操作完成并进行通知之后,所有正在等待的读操作才能继续进行,而对于写操作而言,写操作之间是使用Synchronized来进行同步的,保证了只能写操作之间是互斥的,使得读操作可以读取到正确得数据,不会出现脏读

读写锁其实也就是针对上面的场景得出的,因为使用Synchronized为重量级锁,效率会比较慢,最重要的是采用等待通知机制去让读操作进入等待状态会比较麻烦,改用读写锁去实现的话,只需要在读操作时获取读锁,写操作时获取写锁即可,当写锁被获取到时,后续的读写操作都会被阻塞,等写锁释放了之后,后续所有的读写操作才能继续进行

ReentrantReadWriteLock

特性

  • ReentrantReadWriteLock支持非公平和公平的获取锁方式,吞吐量依然是非公平优于公平,并且默认的方式是非公平
  • ReentrantReadWriteLock支持可重入,当读线程获取了读锁之后,其能继续去获得读锁;当写线程获得了写锁之后,其能继续去获得写锁,也能去获得读锁(写线程能自己读,但其他线程不能读,也不能写)
  • 支持锁降级,当获取了写锁、再获取读锁,之后释放了写锁之后,会自动降级为读锁

使用方法

在这里插入图片描述
使用的方法也比较简单,对于读写锁都是

  • 创建一个ReentrantReadWriteLock
  • 利用ReentrantReadWriteLock来获取读锁或者写锁
  • 调用读锁或者写锁的lock方法进行上锁
  • 调用读锁或者写锁的unlock方法进行解锁

读写锁的实现分析

之前学习ReentrantLock的时候认识了同步状态这个概念,同步状态就是指锁被一个线程重复获取的次数,在ReentrantLock就是state这个变量

但对于读写锁来说,不仅需要维护同步状态,还需要去维护多个读线程和一个写线程的状态(写线程只可能有一个,一旦出现写线程,其余读线程全部停止)

同步状态是一个整形变量,而一个整形变量上去维护多种状态,就需要按位切割使用这个变量,而去维护这个变量是关键所在,总的来说就是怎样使用一个变量来区分读写锁的状态,为什么不使用多个变量去记录两种锁的同步状态呢?

读写锁对于该整形变量进行了按位切割使用,高16位表示读而低16位表示写

在这里插入图片描述

如何使用位拆分去表示两种锁的重入状态呢?

对于写锁的低16位,只要采用与运算,与0x0000FFFF进行与运算,就可以把高16位给去掉了,然后对于重入次数只要正常加减即可

对于读锁的高16位,不能像写锁一样简单地使用与运算就可以算出,因为简单的与运算只能保留高16位,但重入的次数不能通过简单的加减来得到了,不过其实也差不多,采用的是无符号右移16位的做法,因为是一个正数,所以只要无符号右移16位就可以将高16位移去低16位,并且高16位会变成全为0,然后重入的时候再进行同样的加减即可

如何判断读锁和写锁获取

当低16位不为0的时候,即代表读锁被获取,当高16位不为0的时候,即代表写锁去获取,但去判断写锁的时候,要先去移位才可以判断

写锁

写锁的使用如下

    public void test(){
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        writeLock.lock();
        writeLock.unlock();
    }

从构造方法上看

在这里插入图片描述
可以看到,其调用的是另外一个构造方法,并且fair变量设为false,这也证明了前面提到的,默认的读写锁实现是一个非公平锁

    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

在这里插入图片描述
在这里插入图片描述
可以看到

  • 公平锁的实现由sync成员变量来决定
  • 构造方法直接实例化了ReadLock和WriteLock
  • 并且,读写锁的获取其实就是直接返回构造实例化的ReadLock和WriteLock
  • 并且,ReentrantReadWriteLock里面的内部类有5哥
    • ReadLock:读锁
    • WriteLock:写锁
    • Sync:抽象的读写锁的同步功能实现(线程安全的功能主要由Sync来保证)
      • FariSync:公平的同步功能
      • UnFairSync:不公平的同步功能
    • 并且Sync是否公平,取决于构造方法,默认是不公平的

写锁的获取

lock方法

写锁的获取是直接调用WriteLock的lock方法

在这里插入图片描述
从代码上可以看到,写锁的获取直接依赖于Sync同步类来实现

在这里插入图片描述
再深入,发现其底层实现就是AQS,可以看到很多锁的底层实现都是AQS,下面就来看看这段代码是干什么的

    public final void acquire(int arg) {
        //尝试去获取锁
        //如果获取锁失败就去进行入队等待
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //如果获取锁失败,并且入队成功
            //将自己设为中断状态???
            selfInterrupt();
    }
tryAcquire

这个方法就是去获取锁,可以看到进来

其由ReentrantReadWriteLock去实现的

protected final boolean tryAcquire(int acquires) {
    /*
     * Walkthrough:
     * 1. If read count nonzero or write count nonzero
     *    and owner is a different thread, fail.
     * 2. If count would saturate, fail. (This can only
     *    happen if count is already nonzero.)
     * 3. Otherwise, this thread is eligible for lock if
     *    it is either a reentrant acquire or
     *    queue policy allows it. If so, update state
     *    and set owner.
     */
     //从注释上我们可以看到三种情况
     //1.如果有人获取了读锁或者有人获取了写锁,那就获取锁失败
     //2.如果计数达到饱和也会停止,也就是可重入次数达到上线了
     //3.其余状态都可以进行获取锁了
    
    //获取当前线程
    Thread current = Thread.currentThread();
    //获取state变量,也就是获取同步状态
    int c = getState();
    //计算wrieteLock的同步状态
    int w = exclusiveCount(c);
    //如果state不为0,代表有其他线程获取读锁或者写锁
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        //如果w为0.代表有人获取了读锁,return false
        //如果w不为0,但当前线程并不是已经获取了写锁的线程,return false
        //以上两种情况都不能获取锁了,因为读锁有人获取了,或者没人获取读锁,但写锁的拥有者不是自己
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        //如果w不为0,且写锁的拥有者是自己,那就代表发生重入锁了
        //改变writeLock的同步状态
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            //如果超过了最大值
            //最大值为16左移一位并且减一(刚好6位)
            //抛出异常
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        //重新设置同步状态
        setState(c + acquires);
        //返回true代表加锁成功
        return true;
    }
    //如果状态栏为0
    //判断写线程是否需要阻塞
    //如果不需要阻塞,使用CAS来重新设置同步状态
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    //不需要阻塞且CAS重新设置同步状态成功
    //就将当前写锁的拥有线程设为自己!!!
    //相当于贴上自己的标签
    setExclusiveOwnerThread(current);
    //返回true
    return true;
}

整个加锁的过程很简单

  • 获取当前线程

  • 获取当前锁的同步状态

  • 如果同步状态不为0,代表写锁或者读锁被获取了

    • 判断是不是被人获取了写锁

      • 如果没人获取写锁,那就代表读锁被获取了,返回false,加锁失败
      • 如果有人获取写锁,判断获取写锁的是不是当前线程,如果不是,也返回false,代表加锁失败(这里是比较巧妙的,重要的是判断当前线程有没有获取写锁,如果获取了,那就代表是重入情况而已,如果其他线程获取了写锁,更加不需要考虑了)
    • 如果当前线程已经获取了写锁,那就代表是可重入情况

      • 修改写锁的同步状态

      • 如果修改写锁之后的同步状态没有大于最大值(2^6-1)

      • 加锁成功

  • 如果同步状态为0,代表没有线程获取锁,但不排除当前会存在竞争情况

    • 判断写操作是否需要进行阻塞
      • 如果不需要进行阻塞,使用CAS去更换写锁的状态量,此时如果发生竞争,谁CAS成功就谁获取锁成功
      • CAS更换同步状态量成功后,就将写锁的拥有者改为自己,相当于贴上自己的标签!!!
getExclusiveOwnerThread

在这里插入图片描述
可以看到,这个变量来自AbstractOwnableSynchronizer的,专门用来存储不需要进行同步的线程

exclusive

从上面代码可以看到,这个方法是用来重新计算写锁的状态量的

因为写锁规定了,重入次数不能超过2 ^ 6,最大值就是2 ^ 6-1,而且,写锁的同步状态量是位于前16位的,所以要进行与运算来取出前6位

在这里插入图片描述
这个运算也很简单,直接跟11111进行与运算即可

setState

这一步没什么好说的,就是对状态量进行+1操作,因为写锁是低16位的,直接加1即可

写锁的释放

对应的就是写锁的unLock方法,其实写锁的释放方法跟ReentrantLock基本一致,每次释放均会减少写状态,当写状态为0时表示写锁已经被释放了,从而等待的所有读写线程才能够继续访问读写锁,实现了前面写线程的修改对于后面的读线程是可见的

在这里插入图片描述

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

关键的方法在于tryRelease

tryRelease

源码如下

        protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //对状态量进行减一
            int nextc = getState() - releases;
            //如果状态量的前5位减为0了
            //代表全部写锁释放完了
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                //将写锁的拥有者改回为Null
                setExclusiveOwnerThread(null);
            //重新修改状态量
            setState(nextc);
            //返回释放结果
            return free;
        }

步骤如下

  • 对状态量进行减一
  • 取前5位判断是不是减为0了
    • 如果减为0,代表写锁全部释放了,包括重入的
      • 将写锁的标志拥有位改为Null,让其他线程可以获取写锁
      • 重新修改状态量(只有重新修改了状态量,其他锁才能获取写锁,所以这里并不会有并发问题)
  • 返回释放结果
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值