JUC之读写锁

前言

很多时候,我们使用重入锁,是为了锁住共有资源保证每次只有一个线程进行修改,但是我们很多时候只是去读这些资源,如果每次都用reentrantLock锁的话,反而会降低吞吐量。
锁的细粒度可以增加吞吐量,读写锁通过16位低位去写锁,高16位去读锁还是挺有意思的。

写锁

在进行查看源码的时候先进行思考一下,写锁 和ReentrantLock 感觉差不多的底层实现应该是差不多的。也分公平锁,和非公平锁。
下面看看写锁的源码:


protected final boolean tryAcquire(int acquires) {
            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;
                //超过了记录的65535位    
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire 重入增加state
                setState(c + acquires);
                return true;
            }
            // writerShouldBlock这个代码挺关键的,这个方法是公平和非公平的关键所在,如果是非公平锁,直接返回false,公平锁就去调用一下hasQueuedPredecessors() 这个方法去判断同步队列中有没有等待的线程,有一个模版模式的体现。讲到这里基本上公平锁和非公平锁就讲到了。
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

exclusiveCount(acquires)

这个方法还是很有意思的,官方的翻译是返回count中表示的独占保持的数量,感觉这里还要画一下图进行解释一下了。写锁拿的是前16位低位锁。读锁拿的是16位 高位锁,还是有意思的这样设计,因为想要读写锁共用,势必要用到state,所以通过高位和地位进行区分,分别获取了哪些锁

1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
如果state为1 两者之间进行按位与运算的话得到的结果为 1

static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int exclusiveCount(int c) { 
    return c & EXCLUSIVE_MASK; 
}

tryRelease(int releases)

这个方法应该是和可重入锁的释放接口,差不多,都是state释放完后,返回true,只不过这个是16位低位释放,应该有这个点,下面看代码分析

   protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            // 直接拿低位16位做与运算,如果都是0,肯定说明释放完了。
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

注意点:这里的释放和可重入锁的释放还不一样,这里是只释放一次,写下面的那种代码就会出现问题,当时我看到这里还挺奇怪的,为什么要这样写。就会出现线程一直挂在同步队列上面

        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//        reentrantReadWriteLock.readLock().lock();
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

        for (int i = 1; i <= 3; i++) {
            writeLock.lock();
        }
        writeLock.unlock();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            writeLock.lock();
            System.out.println();
            writeLock.unlock();
        }).start();

读锁

为什么要设计读锁呢?因为业务中其实大部分的都是读的情况,写的情况少。之前已经提到了读锁的state是高16位开始的,那么65536 就是最基本的增加计量单位了,对吧。看源码实现,还有之前写锁分析的时候,有段代码是如果有state,但是有没有线程占领写资源的时候,有个写线程进入的话,是会加入到同步队列中等待读锁执行完的。
tryAcquire中有个这样的判断

 if (w == 0 || current != getExclusiveOwnerThread())
                    return false;

通过以下代码可以复现:

  public static void main(String[] args) throws Exception {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        reentrantReadWriteLock.readLock().lock();
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

        for (int i = 1; i <= 3; i++) {
            writeLock.lock();
        }
        writeLock.unlock();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            writeLock.lock();
            System.out.println();
            writeLock.unlock();
        }).start();

    }

下面开始看读锁的源码

        protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            // 如果独占锁持有的不为0,并且持有独占锁的线程不是当前线程,读锁加入到同步队列中去,
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            // 共享锁持有的数量   
            int r = sharedCount(c);
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                // 如果是第一次读,就加一,如果是同一个第一个线程则++
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    // 这个cacheHoldCounter 记录了上一个线程获取锁的次数,以及线程id
                    HoldCounter rh = cachedHoldCounter;
                    // 如果是null的话获取一个初始值,如果不是同一个线程了,记录新的线程 
                    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);
        }

非公平共享锁的处理方法
重点讲解一下这里的方法
这只是一种启发式地避免写锁无限等待的做法,它在遇到同步队列的head后继为写锁节点时,会让readerShouldBlock返回true代表新来的读锁(new reader)需要阻塞等待这个head后继。但只是一定概率下能起到作用,如果同步队列的head后继是一个读锁,之后才是写锁的话,readerShouldBlock就肯定会返回false了

final boolean readerShouldBlock() {
              Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
        }

公平共享锁的处理方法

final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }

ThreadLocalHoldCounter重写了ThreadLocal的 initialValue 方法,使得HoldCounter 进行了初始化,所以第一次获取的时候是有值的

//readHolds 这个记录了每个线程持有锁的次数
private transient ThreadLocalHoldCounter readHolds;
   static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

如果 if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) 这个判断为false,则会进行下面的方法
fullTryAcquireShared(Thread current) 这个方法很关键
总结如下:

  1. 判断是否有写锁持锁
  2. 判断 onSyncQueue 有无等待者之类
  3. 高位不能超过 65535
  4. 进行 CAS 操作
    final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                    // else we hold the exclusive lock; blocking here
                    // would cause deadlock.
                    
                } else if (readerShouldBlock()) {
                    // Make sure we're not acquiring read lock reentrantly
                    if (firstReader == current) {
                        // assert firstReaderHoldCount > 0;
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            return -1;
                    }
                }
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

读锁的释放锁资源
先思考一下加锁的过程,写锁是对state进行一个递加来表示重入,但是读锁不行,因为我可能是很多的线程,所以读锁引入了HoldCounter 这个对象来存储线程重入的次数,多个线程用的threadlocal,ThreadLocalHoldCounter进行存储

  protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    return nextc == 0;
            }
        }

读锁套写锁会写锁

当执行下面这段代码的时候就会导致死锁

  public static void main(String[] args) throws Exception {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        readLock.lock();
        System.out.println("写锁重入开始");
        writeLock.lock();
    }

为什么呢?首先读锁获取到锁资源,然后state此时应该为65536
然后写锁去获取资源的时候会进入下面这段代码

 Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;

然后就会返回false,进入到acquirdQueued方法,然后挂起。读锁就永远不能进行unlock了。
但是先执行写锁lock,就不会导致死锁了,因为此时已经拿到了独占资源了。

锁降级

关于jdk对于锁降级的定义如下:
锁降级:
重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不可能的。其实就是线程先获取到了写锁,还没有释放写锁的资源的时候,再去获取读锁,是可以的。具体的代码在tryAcquireShared(int unused) 方法和fullTryAcquireShared(Thread current)方法中都有体现

if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;

获取读锁的时候会去判断是不是有写锁拿到独占资源了,如果有独占资源是不是当前线程的,如果不是当前线程的则去加入到同步队列中,如果是当前的线程则直接可以获取到读锁,这样做的好处就是避免了拿到独占锁后,释放才能获取读锁,因为释放掉才能获取读锁,又要和别的线程进行竞争,读到的数据并不是最开始修改的值。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值