基于jdk8分析分析ReentrantReadWriteLock源码

ReentrantReadWriteLock可重入读写锁,同样是基于AQS实现的。与ReentrantLock区别就是读写分离,将锁的粒度都将细化,提升性能。在共享数据读写操作中,读操作远远超过写操作的次数,那么可以理解为共享数据在大部分时间是不变的。synchronized和ReentrantLock作为互斥锁,用于这种场景明显会降低系统的性能。因此,读写分离的重入锁ReentrantReadWriteLock就出现了。

ReentrantLock特点是:读-读并行、读-写互斥、写-写互斥。存在对共享数据修改操作时,自然需要有互斥锁的特点,但是在不需要修改数据时,可以认为是无锁的并行操作。

来个简单的例子:


public class TestReentrantReadWriteLock {
    private Map<String, String> cache = new HashMap<>();
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Lock readLock = readWriteLock.readLock();
    private Lock writeLock = readWriteLock.writeLock();

    public void put(String key, String value) {
        writeLock.lock();
        try {
            cache.put(key, value);
        } finally {
            wirteLock.unlock();
        }
    }
    
    public String get(String key) {
        readLock.lock();
        try {
            return cache.getOrDefault(key, "0");
        } finally {
            readLock.unlock();
        }
    }
}

分析源码:

ReentrantReadWriteLock实现ReadWriteLock接口

public interface ReadWriteLock {
    //返回读锁
    Lock readLock();
    //返回写锁
    Lock writeLock();
}

在ReentrantLock中,提供readLock和writeLock的实现方法。构造函数提供支持公平锁和非公平锁,默认为非公平锁。

    //field
    //ReentrantLock内部类实现的ReadLock、WriteLock
    private final ReentrantReadWriteLock.ReadLock readerLock;
    private final ReentrantReadWriteLock.WriteLock writerLock;
   
    //method
    //返回对应的读写锁
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

    //构造函数
    public ReentrantReadWriteLock() {
        this(false);
    }
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

 读锁(锁资源共享):

public void lock() {
            //sync是基于AQS实现的公平锁或非公平锁
            sync.acquireShared(1);
        }
//AQS提供的获取读锁框架,tryAcquireShared为具体子类实现的获取方法
//根据state状态来识别:锁是否被持有?是否被读锁持有?然后尝试获取锁
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

         protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            //为什么写锁被非当前线程持有时,才会返回呢?读-写不是互斥吗?
            //假如:某个线程获取到写锁后,业务处理仍然需要读取某些共享数据时,同线程写-读互斥不就完了。
            //这行代码体现了ReentrantReadWriteLock支持锁降级特性
            //return -1,则该节点将在AQS获取共享锁框架中入队,如果是head后继则会再次尝试
            //调用acquiredShared获取锁;非head后继则尝试park
            //注意:1、ReentrantReadWriteLock不支持锁升级(拥有读锁申请写锁)
            //     2、在锁升级后,释放写锁,其他线程读锁线程就可以获取读锁
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);
            //readerShouldBlock()在公平锁和非公平锁中提供的策略是不一样的。
            //非公平锁中,head后继节点是EXCLUSIVE类型的,说明存在写锁获取锁,在读写锁中且非公平锁条件下,不阻塞读锁,会导致写锁饥饿,除过这种场景外,其他场景都不会阻塞读锁获取
            //公平锁中,如果双向链表为空、(非空&&head后继==null)、(非空&&head后继是当前线程)时,return false,可以尝试获取锁资源
            //SHARED_UNIT为65536,读写锁是将int state属性值得高16位作为读锁,低16位作为写锁
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                //r==0表示读锁为0,没有线程获取读锁
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                //r!=0且firstReader==current说明firstReaderHoldCount是自己重入次数
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                //r!=0且firstReader!=current则需要获取自身的重入次数
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    //rh!=null&&rh.tid跟cachedHoldeCounter不一样说明上个读锁线程不是自己,则通过ThreadLocal获取自己的重入次数
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    //rh!=null&&rh.tid==current,说明就是自己了
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    //重入次数++
                    rh.count++;
                }
                return 1;
            }
            //当读锁判断后需要阻塞时
            return fullTryAcquireShared(current);
        }
       final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                //持有写锁的线程不是当前线程,返回-1.后续加入双向链表中
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                //没有互斥写锁,但是需要根据公平或非公平锁判断是否需要阻塞
                //1、是公平锁时,前置节点非当前线程,但是正在执行的线程是当前线程,可以尝试获取
                //2、非公平锁时,当head后继节点是互斥锁时,需要阻塞,但是正在执行的线程是当前线程,则可以尝试获取锁。如:写操作时,需要读锁获取某个值,拒绝读锁获取数据岂不是就死锁了
                } else if (readerShouldBlock()) {
                    // Make sure we're not acquiring read lock reentrantly
                    if (firstReader == current) {
                        // assert firstReaderHoldCount > 0;
                    } else {
                        //当读锁不是第一次获取读时,需要尝试获取读锁。可重入。否则就会容易引起死锁。而如果是第一次尝试获取读锁的话,直接退出return -1,进入AQS等待链表
                        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");
                //尝试获取读锁,并return 1
                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;
                }
            }
        }

从上述代码可以看出,不管是否是公平锁,有一个问题是例外。如果当前正在执行的已拥有读锁或写锁的线程再次获取读锁时,都需要循环获取读锁,否则读锁一直获取不到读锁加入AQS链表中,那么该线程将会阻塞,严重影响性能。

private void doAcquireShared(int arg) {
        //将node节点加入AQS链表
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            //线程没有被阻塞或没有获取到锁,一直循环
            for (;;) {
                //加入到链表后,前驱接点是head,说明它是第一个元素,则尝试获取锁
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    //获取成功,因为是读锁所有需要直接唤醒下一个节点,让下一个节点也开始获取锁
                    //否则就不是读-读互斥了
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

读锁到这块差不多结束了,看得头昏脑涨的……

写锁结构跟ReentrantLock完全一样啊,当然tryAcquire需要子类自己实现了。首先尝试获取锁,获取失败进入链表,再判断是否为head后继,如果是则再次tryAcquire尝试获取锁,如果不是则看前继节点是否能安全的park,并检测中断属性值。

//写锁结构跟ReentrantLock完全一样,只需要关注tryAcquire(arg)即可
           public final void acquire(int arg) {
               if (!tryAcquire(arg) &&
                 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                 selfInterrupt();
          }
          protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            //锁的状态
            int c = getState();
            //互斥锁的状态
            int w = exclusiveCount(c);
            //state!=0说明,写锁前有读锁或写锁申请了锁资源
            if (c != 0) {
                //有锁,w==0说明,上次申请锁的是读锁;current!=线程持有锁不是当前线程
                //线程持有锁只会是写锁设置的
                if (w == 0 || current != getExclusiveOwnerThread())
                    //获取失败时,又到了进入双向链表,是否为head的后继尝试获取锁的流程
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //w!=0&&current==getExclusveOwnerThread就是写锁重入了
                setState(c + acquires);
                return true;
            }
            //此处state==0,没有任何线程持有锁
            //writerShouldBlock是公平锁和非公平锁都实现的方法
            //非公平锁直接返回false,公平锁则判断空链表或head后继是当前线程返回false;
            //cas设置state值
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            //设置当前线程独占锁
            setExclusiveOwnerThread(current);
            return true;
        }

Condition:

只有写锁支持Condition,读锁会抛出UnsupportedOperationException异常,读锁不支持Condition的原因,个人理解跟读写锁的应用场景有关,支持高并发的读。读之间都要支持Condition的话,读写锁实现会非常复杂,而且又丢了读写锁的应用场景。

读锁和写锁释放锁的逻辑就不细读了,大致就是:一直尝试释放锁,然后唤醒head后继节点,接着让它尝试获取锁的过程。

 

未完待续,去玩会再说……

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章