Java|锁--JAVA成长之路( 七 )


独享锁也叫排他锁 , 是指该锁一次只能被一个线程所持有 。 如果线程T对数据A加上排它锁后 , 则其他线程不能再对A加任何类型的锁 。 获得排它锁的线程即能读数据又能修改数据 。 JDK中的synchronized和JUC中Lock的实现类就是互斥锁 。
共享锁是指该锁可被多个线程所持有 。 如果线程T对数据A加上共享锁后 , 则其他线程只能对A再加共享锁 , 不能加排它锁 。 获得共享锁的线程只能读数据 , 不能修改数据 。
独享锁与共享锁也是通过AQS来实现的 , 通过实现不同的方法 , 来实现独享或者共享 。
【Java|锁--JAVA成长之路】下图为ReentrantReadWriteLock的部分源码:
我们看到ReentrantReadWriteLock有两把锁:ReadLock和WriteLock , 由词知意 , 一个读锁一个写锁 , 合称“读写锁” 。 再进一步观察可以发现ReadLock和WriteLock是靠内部类Sync实现的锁 。 Sync是AQS的一个子类 , 这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在 。
在ReentrantReadWriteLock里面 , 读锁和写锁的锁主体都是Sync , 但读锁和写锁的加锁方式不一样 。 读锁是共享锁 , 写锁是独享锁 。 读锁的共享锁可保证并发读非常高效 , 而读写、写读、写写的过程互斥 , 因为读锁和写锁是分离的 。 所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升 。
那读锁和写锁的具体加锁方式有什么区别呢?在了解源码之前我们需要回顾一下其他知识 。
在最开始提及AQS的时候我们也提到了state字段(int类型 , 32位) , 该字段用来描述有多少线程获持有锁 。
在独享锁中这个值通常是0或者1(如果是重入锁的话state值就是重入的次数) , 在共享锁中state就是持有锁的数量 。 但是在ReentrantReadWriteLock中有读、写两把锁 , 所以需要在一个整型变量state上分别描述读锁和写锁的数量(或者也可以叫状态) 。 于是将state变量“按位切割”切分成了两个部分 , 高16位表示读锁状态(读锁个数) , 低16位表示写锁状态(写锁个数) 。 如下图所示:
了解了概念之后我们再来看代码 , 先看写锁的加锁源码:

  • 这段代码首先取到当前锁的个数c , 然后再通过c来获取写锁的个数w 。 因为写锁是低16位 , 所以取低16位的最大值与当前的c做与运算( int w = exclusiveCount(c); ) , 高16位和0与运算后是0 , 剩下的就是低位运算的值 , 同时也是持有写锁的线程数目 。
  • 在取到写锁线程的数目后 , 首先判断是否已经有线程持有了锁 。 如果已经有线程持有了锁(c!=0) , 则查看当前写锁线程的数目 , 如果写线程数为0(即此时存在读锁)或者持有锁的线程不是当前线程就返回失败(涉及到公平锁和非公平锁的实现) 。
  • 如果写入锁的数量大于最大数(65535 , 2的16次方-1)就抛出一个Error 。
  • 如果当且写线程数为0(那么读线程也应该为0 , 因为上面已经处理c!=0的情况) , 并且当前线程需要阻塞那么就返回失败;如果通过CAS增加写线程数失败也返回失败 。
  • 如果c=0 , w=0或者c>0 , w>0(重入) , 则设置当前线程或锁的拥有者 , 返回成功!
tryAcquire()除了重入条件(当前线程为获取了写锁的线程)之外 , 增加了一个读锁是否存在的判断 。 如果存在读锁 , 则写锁不能被获取 , 原因在于:必须确保写锁的操作对读锁可见 , 如果允许读锁在已被获取的情况下对写锁的获取 , 那么正在运行的其他读线程就无法感知到当前写线程的操作 。
因此 , 只有等待其他读线程都释放了读锁 , 写锁才能被当前线程获取 , 而写锁一旦被获取 , 则其他读写线程的后续访问均被阻塞 。 写锁的释放与ReentrantLock的释放过程基本类似 , 每次释放均减少写状态 , 当写状态为0时表示写锁已被释放 , 然后等待的读写线程才能够继续访问读写锁 , 同时前次写线程的修改对后续的读写线程可见 。


推荐阅读