7. ReadWriteLock接口及其实现ReentrantReadWriteLock

Java并发包的locks包里的锁基本上已经介绍的差不多了,ReentrantLock重入锁是个关键,在清楚的了解了同步器AQS的运行机制后,实际上再分析这些锁就会显得容易的多,这章节主讲另外一个重要的锁---ReentrantReadWriteLock读写锁。

ReentrantLock是一个独占锁,也就是说只能由一个线程获取锁,但如果场景是线程只做读的操作呢?这样ReentrantLock就不是很合适,读的线程并不需要保证其线程的安全性,任何一个线程都能去获取锁,只有这样才能尽可能地保证性能和效率。ReentrantReadWriteLock就是这样的一个锁,在其内部分为读锁和写锁,可以有N个读操作线程获取到读锁,但是只能有1个写操作线程获取到写锁,那么可以预见的是读锁是共享锁(AQS中的共享模式),写锁是独占所(AQS中的独占模式)。首先来看读写锁的接口类:

public interface ReadWriteLock {    
    Lock readLock();        //获取读锁
    Lock writeLock();        //获取写锁
 }

可以看到ReadWriteLock接口只定义了两个方法,获取读锁和获取写锁的方法。下面是ReadWriteLock的实现类---ReentrantReadWriteLock。

和ReentrantLock类似,ReentrantReadWriteLock在其内部也是通过一个内部类Sync实现同步器AQS,同样也是通过实现Sync实现公平锁和非公平锁,这一点的思路和ReentrantLock类似。在ReadWriteLock接口中获取的读锁和写锁是怎么实现的呢?

//ReentrantReadWriteLock
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
public ReentrantReadWriteLock(){
    this(false);    //默认非公平锁
}
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();    //锁类型(公平/非公平)
    readerLock = new ReadLock(this);    //构造读锁
    writerLock = new WriteLock(this);    //构造写锁
}
……
public ReentrantReadWriteLock.WriteLock writeLock0{return writerLock;}
public ReentrantReadWriteLock.ReadLock readLock0{return ReaderLock;}
//ReentrantReadWriteLock$ReadLock
public static class ReadLock implements Lock {
    protected ReadLock(ReentrantReadwritLock lock) {
        sync = lock.sync;        //最后还是通过Sync内部类实现锁
  }
    ……    //它实现的是Lock接口,其余的实现可以和ReentrantLock作对比,获取锁、释放锁等等
}
//ReentrantReadWriteLock$WriteLock
public static class WriteLock implemnts Lock {
    protected WriteLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
  }
……    //它实现的是Lock接口,其余的实现可以和ReentrantLock作对比,获取锁、释放锁等等
}

上面是对ReentrantReadWriteLock做了一个大致的介绍,可以看到在其内部有好几个内部类,实际上读写锁有两个锁---ReadLock、WriteLock,这两个锁都是实现自Lock接口,可以和ReentrantLock对比,而这两个锁的内部实现则通过Sync,也就是同步器AQS实现的,这也可以和ReentrantLock中的Sync对比。

回顾一下AQS,其内部有两个重要的数据结构---一个是同步队列、一个则是同步状态,这个同步状态应用到读写锁中也就是读写状态,但AQS中只有一个state整形来标识同步状态,读写锁中则有读、写两个同步状态需要记录。所以,读写锁将AQS中的state整型做了一个处理,它是一个int型变量,一共4个字节32位,那么可以读写状态就各占16位----高16位标识度,低16位标识写。 image

先在有一个疑问如果state的值为5,二进制为(00000000000000000000000000000101),如何快速确定读和写各自的状态呢?这就要用到位移运算了。计算方式为:写状态=state & 0x0000FFFF,读状态=stae >>> 16。写状态增加1:state + 1,读状态增加1=state + (1 << 16)。有关位移运算可以参考《<<、>>、>>>位移操作

1. 写锁的获取与释放

根据我们之前的经验可以得知:AQS已经将获取锁的算法骨架搭好了,只需子类实现tryAcquire(独占锁),故我们只需要看tryAcquire。

//ReentrantReadWriteLock$Sync
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread;
    int c = getState();    //获取state状态
    int w = exclusiveCount(c);    //获取写状态,即 state & 0x00001111
    if (c != 0) {    //存在同步状态(读或写),作下一步判断
        if (w == 0 || current != getExclusiveOwnerThread())     //写状态为0,但同步状态不为0表示有读状态,此时获取锁失败,或者当前已经有其他写线程获取了锁此时也获取锁失败
            return false;
        if (w + exclusiveCount(acquire) > MAX_COUNT)    //锁重入是否超过限制
            throw new Error(“Maxium lock count exceeded”);
        setState(c + acquire);    //记录锁状态
        return true;
  }
  if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
      return false;        //writerShouldBlock对于非公平锁总是返回false,对于公平锁则判断同步队列中是否有前驱节点
  setExclusiveOwnerThread(current);
  return true;
}

上面是写锁的状态获取,不好理解的是writeShouleBlock方法,此方法上面有描述,非公平锁直接返回false,而对于公平锁则是调用hasQueuedPredecessors,方法如下:

//ReentrantReadWriteLock$FairSync
final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
}

原因是为什么呢?这就要回到非公平锁和公平锁的区别上来了。对于非公平锁,每次线程获取锁时,首先会强行进行锁获取操作而不管同步队列中是否有线程,当获取不到锁才会将线程构造至队尾;对于公平锁来讲,只要同步队列中存在线程,就不会去获取锁,而是将线程构造至队尾。所以重新回到写状态的获取上,tryAcquire方法里,前面发现没有线程持有锁,但是此时会根据锁的不同做响应操作,对于非公平锁---抢锁,对公平锁---同步队列中有线程,不抢锁,添加至队尾排队。

写锁的释放和ReentrantLock的释放过程基本类似,毕竟都是独占锁,每次释放减少写的状态,直道减小到0就表示写锁已经完全释放。

读锁的获取与释放

同理,根据我们之前的经验可以得知:AQS已经将获取锁的算法骨架搭好了,只需子类实现tryAcquireShared(共享锁),故我们只需要查看tryAcquireShared。我们知道对于共享模式下的锁,他能狗被多个线程同时获取,现在问题来了,T1线程获取了锁,同步状态state=1,此时T2也获取了锁,state=2,接着T1线程重入state=3,也就是说读状态是所有线程读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护,所以这个地方要做一些复杂处理,源码有点长,但复杂就在于每个线程保存自身获取读锁的次数,具体参照源码的tryAcquireShared,仔细阅读并结合上面对写锁获取锁的分析不难读懂。

读锁的释放值得注意的地方在于自身维护的获取锁的次数,以及通过移位操作减少状态state-(1 << 16)。

转载于:https://my.oschina.net/waterfu/blog/2245992

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值