ReentrantReadWriteLock读写锁

目录

一、前言

1、读写锁

2、可重入锁:

3、公平锁和非公平锁

二、接口

三、实现分析

3.1 读写状态的设计

3.1.1读位运算

3.1.2写位运算

3.2 写锁的获取与释放

3.2.1 tryAcquire方法

3.2.2 tryRelease方法

3.3 读锁的获取与释放

3.3.1 tryAcquireShared方法

3.3.2 tryReleaseShared

4、公平锁和非公平锁

4.1 使用原理

4.2 公平锁

4.3 非公平锁


一、前言

ReentrantReadWriteLock中包含三种锁:读写锁、可重入锁、公平/非公平锁

1、读写锁

2、可重入锁:

        又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。

  • 对于Java ReetrantLock而言,从名字就可以看出是一个重入锁,其名字是Re entrant Lock 重新进入锁。
  • 对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
synchronized void setA() throws Exception{
  Thread.sleep(1000);
  setB();
}
 
synchronized void setB() throws Exception{
  Thread.sleep(1000);
}

3、公平锁和非公平锁

  • 公平锁是指多个线程按照申请锁的顺序来获取锁。
  • 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

        对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。 

 对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

二、接口

        ReadWriteLock仅定义了获取读锁和写锁的两个方法,即ReadLock()方法和writeLock()方法,而其实现:ReentranReadWriteLock,除接口方法之外,还提供了一些便于外界监控其内部工作状态的方法:

特性 说明
公平性选择支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
重进入该锁支持重进入。以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁;而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁。
锁降级遵循获取写锁、获取读锁再获取写锁的次序,写锁可以降级成为读锁

三、实现分析

        分析ReentranReadWriteLock的实现,主要包括读写状态的设计、写锁的获取与释放、读锁的获取与释放以及锁降级。

3.1 读写状态的设计

        ReentranReadWriteLock中使用一个int state变量来维护读锁和写锁的状态。int类型为4字节32位,用state的高16位来存储读锁的状态,用低16位来存储写锁的状态。

3.1.1读位运算

位运算代码

//偏移位数
static final int SHARED_SHIFT = 16;
//读锁计数基本单位
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//读锁、写锁可重入最大数量
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//获取低16位的条件
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

//获取读锁重入数
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
//获取写锁重入数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

        因为ReentranReadWriteLock中的int整型变量state要同时维护读锁、写锁两种状态,所以ReentranReadWriteLock的是通过高低位切割来实现。

 int占4个字节,一个字节8个比特位,一共2位,切割一下,高16位表示读,底16位表示写。

读位运算

//偏移位数
static final int SHARED_SHIFT = 16;
//读锁计数基本单位
static final int SHARED_UNIT = (1 << SHARED_SHIFT);

读锁使用高16位,每次获取读锁成功+1,所以读锁计数基本单位是1的高16位,即1左移16位(1 << 16)。

 1左移16位等于65536,每次获取读锁成功都会加上65536,通过以下代码将65536右移16位,实际上每次获取读锁还是只是会加1,就好比你早上去上班去公司,晚上下班回家,最终晚上你还是回家了,左移,右移也是这个道理,是一个过程,最终的状态没变。

//偏移位数
static final int SHARED_SHIFT = 16;
//获取读锁重入数
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

上面shareCount函数通过位运算是做无符号右移16位获取读锁的重入数,为什么可以获取到呢?

 1左移16位为65536,65536再右移16位为1。

3的二进制

0000 0000 0000 0000 0000 0000 0000 0011

3右移16位

0000 0000 0000 0011 0000 0000 0000 0000

        比如我们获取到了3次读锁,就是65536 * 3 = 196608,转换下公式就是3左移16位等于196608196608右移16位等于3

 

虽然我们每次获取到读锁都会+65536,但是获取读锁时会做右移16位,所以效果和+1是一样。

3.1.2写位运算

//偏移位数
static final int SHARED_SHIFT = 16;
//获取低16位的条件
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
//获取写锁重入数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

剩下的写锁就非常简单,获取低16位不用左右移动,只要把高16位全部补0即可。

         反推一下,因为不需要左右移动,其实就和正常的数字一样,只不过因为高16位补0,导致数值范围在0~65535,也就是说写锁获取成功直接+1就好了。

         EXCLUSIVE_MASK变量,1右移16位后-1,得到6553565535的二进制就是111111111111111

现在来看exclusiveCount函数,该函数内做了位运算&&又称""运算。

""运算是两个二进制,每位数运算,运算规则如下

0&0=0
0&1=0
1&0=0
1&1=1

如果相对应位都是1,则结果为1,否则为0

可能有些读者大大还是不太明白,下面放张图16位二进制""运算图

         我们发现""运算时,只要有一方为0,那结果一定是0,所以为了切割低16位,可以使用&来完成。

 

        从上图可以看出,EXCLUSIVE_MASK16位都是0,低16位都是1,和它&的变量,高16位全部会变成0,低16位全部保留下来,最终达到获取低16位效果。

c & EXCLUSIVE_MASK,假设c1&的过程如下图

 这样看可能没太大感觉,我们把数值调大点,假设c6553665537&的过程如下图

现在有感觉了吧,c的高16位都会变成0,低16位会原样保留,最终达到获取低16位效果。

  EXCLUSIVE_MASK范围在0~65535,所以c的范围也不会超过0~65535,因为超过了也会通过& EXCLUSIVE_MASK回到0~65535

3.2 写锁的获取与释放

        ReentranReadWriteLock的写锁的获取与释放实现基于同步器AbstractQueuedSynchronizer,与其它独占锁的差异就在于对同步器中tryAcquire()和tryRelease()的重写上,所以下面仅讨论这两个方法。AbstractQueuedSynchronizer独占锁释放获取实现见:AbstractQueuedSynchronizer独占式同步状态获取与释放。

3.2.1 tryAcquire方法

官方给出的注释如下:

  1. 若读锁数或写锁数非空,并且当前线程不是读锁或写锁的持有者,获取失败。
  2. 若当前线程获取写锁的数量已经达到最大值,失败。
  3. 否则,当前线程获得锁。
        protected final boolean tryAcquire(int acquires) {
                /*
             * Walkthrough:
             * 1. 若读锁数或写锁数非空,并且当前线程不是读锁或写锁的持有者,获取失败。
             * 2. 若当前线程获取写锁的数量已经达到最大值,失败。
             * 3. 否则,当前线程获得锁
             */
            Thread current = Thread.currentThread(); //返回当前线程对象
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

3.2.2 tryRelease方法

释放方法很简单,就是对锁的状态进行判断,无误则对状态进行修改。

        protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

isHeldExclusively()方法

读锁是否被当前线程持有

        protected final boolean isHeldExclusively() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

getExclusiveOwnerThread()方法

获取锁持有者线程

    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }

3.3 读锁的获取与释放

        读锁是一个支持重进入的共享锁,他能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功的获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。

3.3.1 tryAcquireShared方法

        当写锁被另外一个线程持有时,获取读锁失败;否则调用readerShouldBlock()来判断这次获取读锁的操作应不应该阻塞,然后判断读锁数是否达到上限,如果上面两次判断都通过,就调用compareAndSetState()来更新锁状态。

        readerShouldBlock()方法主要是为了避免尝试获取写锁的线程陷入无限阻塞,如果队列的头结点是一个等待的writer节点,那么该方法就会返回true。

        通过下面代码可以看到,在调用compareAndSetState()更新锁状态成功后,会进入if方法体。这段代码的作用主要是:更新第一个获取读锁的线程指针和获取次数(firstReader 和firstReaderHoldCount),更新上一个获取读锁的线程指针(cachedHoldCounter),最后更新当前线程获取读锁的数量(通过ThreadLocal存在自己线程的容器中)。若读锁数量为0,表示当前线程是第一个,执行下面1处代码;若当前线程是第一个,然后现在再次获取了,执行下面2处代码;否则更新当前线程的获取读锁数和cachedHoldCounter,最后获取读锁数都+1。最后返回1。

        protected final int tryAcquireShared(int unused) {
            /*
             * Walkthrough:
             * 1. 如果写锁被另一个线程持有,则获取读锁失败。
             * 2. 否则调用readerShouldBlock()来判断这次获取读锁的操作
             *    应不应该阻塞,然后判断读锁数是否达到上限。
             * 3. 如果上面两次判断都通过,就调用compareAndSetState()来更新锁状态。
             */
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current) // 写锁被其它线程持有
                return -1;
            int r = sharedCount(c); // 读锁数量                   
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) { // 读锁+1
                if (r == 0) {
                    firstReader = current; 
                    //1、当前线程是第一个获取读锁的线程
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    //2、当前线程是第一个,然后现在再次获取了
                    firstReaderHoldCount++;
                } else {
                    //3、更新当前线程的获取读锁数,然后更新cachedHoldCounter
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

3.3.2 tryReleaseShared

        首先更新firstReader,然后更新cachedHoldCounter和当前线程的锁数量,最后更新锁的状态。同时若读写锁数量都为0,则返回true,这是为了上层方法唤醒阻塞的writer线程;否则返回false。

     protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();//获取当前线程
            if (firstReader == current) {
                // 当前线程是第一个获取读锁的线程
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;    // 上一个获取读锁的线程
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();        // rh指向当前线程获取读锁的数量对象
                int count = rh.count;            // 当前线程获取读锁的数量
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;        // 若当前线程读锁数>1,则数量-1
            }
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;         // 读写锁的数量   
                if (compareAndSetState(c, nextc))    // 更新锁状态
                    // 若读写锁数量都为0,则返回true,这里是为了唤醒阻塞的writer线程
                    return nextc == 0;
            }
        }

4、公平锁和非公平锁

        与ReentrantLock一样,ReentrantReadWriteLock也分为公平锁和非公平锁,可以在构造方法中指定,不指定默认就是非公平锁。公平锁和非公平锁都继承自内部同步器Sync,它们的区别仅在writerShouldBlock()和readerShouldBlock()这两个方法的实现上。writerShouldBlock()在tryAcquire()方法中即获取写锁的时候会调用,readerShouldBlock()在tryAcquireShared()方法中即获取写锁的时候会调用。

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

4.1 使用原理

        就是在构造方法的时候给sync属性传入FairLock或NonFairLock对象,然后接下来就用sync来进行锁的操作了。

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    final Sync sync;    // 构造方法中初始化成FairLock或NonFairLock,进行lock和unlock的主要对象
 
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
 
    public ReentrantReadWriteLock() {    this(false);    }
 
    // 自定义同步器,tryAcquire()、tryRelease()等主要方法都在里面实现了
    abstract static class Sync extends AbstractQueuedSynchronizer {...}
 
    // 公平锁,就是一个同步器,特别实现readerShouldBlock()和writerShouldBlock()来实现公平锁
    static final class FairSync extends Sync {...}
 
    // 非公平锁,就是一个同步器,特别实现readerShouldBlock()和writerShouldBlock()来实现非公平锁
    static final class NonfairSync extends Sync {...}

4.2 公平锁

        writerShouldBlock()和readerShouldBlock()都是判断当前节点有没有不是头结点的前驱节点,即是否有线程比当前线程等待更久的时间,有的话返回false,否则true。

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();    // 判断当前节点有没有不是头结点的前驱节点
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }
 
    public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

4.3 非公平锁

        获取写锁的线程一定不会阻塞,因为始终返回false。获取读锁的线程,若队列中第一个节点是独占节点,则该线程获取读锁失败,这是为了避免获取写锁一直阻塞。

   static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();    // 队列中第一个节点是否是writer节点
        }
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值