jdk锁知识(六)—— ReentrantReadWriteLock锁源码

1、前言

开发或者学习中会遇到查看源码的情况,一般我会先找到对应的实现类,然后以此找出它的父接口或者上级抽象类,构建出一个如下的diagram图,查看其整体功能是什么;

然后打开源码,大致浏览下实现类中变量、方法、内部类等信息,了解当前类的主要功能是什么;然后看当前类源码复杂不负责,如果不复杂可以整体看,如果复杂,建议写一个demo,根据实际的使用流程来追踪源码,这样有了主线,读取复杂源码时才不会迷路。另外源码中可能很多方法是重载的,所以在读的时候千万不要被众多的方法吓到,可以根据功能将所有重载方法划成一类,这样可以加快我们对类整体的掌握。

2、Lock锁实现类ReentrantReadWriteLock的demo

通过前面的Lock锁框架图可以知道,ReentrantReadWriteLock和ReentrantLock是Lock较常用的具体锁类,这里我们以ReentrantReadWriteLock为例进行源码的追踪查看,demo如下:

public class LockSource implements Runnable{
    private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); 
    private static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    
    public int threadId = 0; //线程ID,用于记录当前是第几个线程执行

    public LockSource(int threadId) { //构造器
        this.threadId = threadId;
    }

    @Override
    public void run() {
        if (threadId%2==0){ 
            readLock.lock();
            System.out.println("读锁" +threadId+ ":开始处理。。。。");
            try {
                Thread.sleep(1000*5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("读锁" +threadId+ ":处理结束释放锁。。。。");
            readLock.unlock();
        }else {
            writeLock.lock();
            System.out.println("写锁" +threadId+ ":开始处理。。。。");
            try {
                Thread.sleep(1000*5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("写锁" +threadId+ ":处理结束释放锁。。。。");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        for (int i=1;i<5;i++){
            Thread thread = new Thread(new LockSource(i));
            thread.start();
        }
    }
}

demo的功能是启动4个线程,奇数线程号执行写锁功能,偶数线程号执行读锁功能。另外需要注意的是,读锁写锁都是由ReentrantReadWriteLock衍生出来的,而ReentrantReadWriteLock本身则不具有读写锁竞争获取等方法,锁的获取释放等功能需要依赖于读锁和写锁内部类。为了在阅读源码前对ReentrantReadWriteLock有个直观的感受,下面我们再通过两幅图看一下:

右边图可以看到,ReentrantReadWriteLock中并没有锁的竞争释放等方法,所以可以猜测锁的功能应该是由WriteLock和ReadLock内部类实现。再有左边图可以看出,ReentrantReadWriteLock底层是一个继承了AQS的Sync对象,公平锁和非公平锁在其基础上进行的定义。而读锁和写锁则是基于公平锁或非公平锁实现锁的获取与释放。所以可以知道读锁写锁底层共用的是同一个队列系统。

3、源码追踪

通过上述的分析可以得到继承了AQS的Sync是最底层的锁实现对象,上一层是根据用户参数封装的FairSync公平锁或NonfairSync非公平锁,再上一层则是ReadLock和WriteLock根据前一层封装的公平锁或非公平锁实现的读锁和写锁。最上层则是ReentrantReadWriteLock封装的属性和方法供用户使用。这里我们先根据demo查看整个写锁的获取释放,读锁的获取释放流程源码,因为是根据流程讲的,有的地方如果引入比较突兀我会再额外讲。最后如果流程讲完,读写锁还有一些关键知识没涉及到,我会再额外补充这些知识点。下面先来看下写锁的获取:

写锁

public void lock() {
    sync.acquire(1);
}

写锁的获取是通过一个Sync来实现的,这个对象是什么呢,如何构建的呢?

public static class WriteLock implements Lock, java.io.Serializable {
        private final Sync sync;
        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
    。。。。。
}
/**
* 1、可以看到sync在创建写锁时引入,在本demo中,即是创建ReentrantReadWriteLock中的Sync对象,我们再接着到ReentrantReadWriteLock的构造器中看一下:
*/
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}
/**
* 2、可以看到ReentrantReadWriteLock是根据参数创建的公平锁和非公平锁,这两个锁又是如何实现的呢?我们到类定义中看一下:
*/
static final class FairSync extends Sync {
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }

static final class NonfairSync extends Sync {
        final boolean writerShouldBlock() {
            return false; 
        }
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }
    }
/**
* 3、可以看到无论公平锁还是非公平锁,底层都是依赖ReentrantReadWriteLock中的Sync内部类实现锁功能,这里也可以看出公平非公平的区别也很简单,就是writeShouldBlock和readerShouldBlock两个方法的不同,这块可以先留意下,后续源码讲到再介绍具体的功能。
*/

这里我们了解了WriteLock中Sync的由来,下面接着深入看一下sync.acquire(1)方法:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

读过AQS那篇文章的博友是不是感觉有点熟悉,这个就是AQS中获取锁的方法,只不过tryAcquire方法在AQS中只是声明了没有定义。所以这里我们着重看下tryAcquire方法,acquireQueued、addWaiter、selfInterrupt()方法我大致说一下功能,有兴趣的可以看一下前面的AQS文章,这里就不重复画轮子了。

protected final boolean tryAcquire(int acquires) {
            //1、获取当前线程对象、锁状态、排他锁竞争线程数       
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            //2、如果同步状态处于锁定状态,进入如下判断
            if (c != 0) {
                //2.1、如果排它锁线程竞争数为0,或当前线程不是排它锁的拥有者(因为能进入这个判断,说明锁是被某个线程获取了的,如果w=0那么拥有锁的线程肯定是读锁线程,如果w!=0且当前线程不是获取到独占锁的线程,那么返回false)(这里要了解w==0的逻辑判断,还有就是||运算符后面的判断要跟前面的反向结果结合)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //2.2、能到这一步,说明w>0,且当前线程为已经拥有锁的线程,此时接着判断排它锁线程数+1是否超标,超标抛异常
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //2.3、设置线程写锁数+1
                setState(c + acquires);
                return true;
            }
            //3、如果同步状态处于未锁定状态,判断是否要阻塞或能否更新同步状态(注意writerShouldBlock方法,公平和非公平锁对其实现不一致,本文demo是根据公平锁进行源码解析)
            if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
                return false;
            //4、经过第3步的过滤,能到这一步说明同步队列为空,或者当前线程获取到锁。此时设置全局独占锁变量指向当前线程
            setExclusiveOwnerThread(current);
            return true;
}

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

public final boolean hasQueuedPredecessors() {
        //1、获取同步队列尾节点和头节点
        Node t = tail; 
        Node h = head;
        Node s;
        //2、如果头尾节点不同,且头节点后继节点不为当前线程节点,说明同步队列还有节点等待竞争锁,返回true,否则返回false
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
}

可以看到在公平锁的写锁(也可称独占锁)获取锁时:

1)先尝试获取锁:根据当前同步器的同步状态和独占锁竞争的线程数判断能否成功获取锁(这里需要注意的是writerShouldBlock方法,在非公平锁中实现是不同的)

2)尝试获取锁失败,创建独占类型的节点放到同步队列尾部,当节点前驱节点为头节点时再次尝试竞争锁,否则挂起等待。

3)当等待竞争锁时,检测到中断状态,则置位当前线程的中断状态

当我们了解了AQS之后,在看ReentrantReadWriteLock源码是不是简单了很多,所以AQS是很重要的一块内容,这些是放在该系列文章前面介绍的原因。下面看一下写锁释放的源码流程(这里释放调用的也是Sync对象中的方法,这块的解析和本文开头类似,这里不在详细讲解Sync和AQS相关的部分,直接上核心源码):

public void unlock() {
            sync.release(1);
}

public final boolean release(int arg) {
        //1、尝试释放锁
        if (tryRelease(arg)) {
            //1.1、锁释放成功,唤醒头节点的后继节点
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
}

protected final boolean tryRelease(int releases) {
            //1、释放锁前,确认当前线程是否已经获得锁,如果未获得锁而执行释放操作,直接抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //2、释放一个线程,则同步状态减1,nextc为新的同步状态
            int nextc = getState() - releases;
            //3、判断占有写锁的线程数是否为0(注意这里统计的是占用数,而不是写锁的竞争线程数)
            boolean free = exclusiveCount(nextc) == 0;
            //4、如果写锁当前没有被占用,设置全局排它锁拥有者变量为空
            if (free)
                setExclusiveOwnerThread(null);
            //5、更新同步队列状态
            setState(nextc);
            return free;
        }

可以看到写锁的释放比较简单,流程是:

1)尝试释放锁:当前线程释放锁,并设置同步状态变量减去释放参数,如果当前同步状态表明没有线程再持有排它锁,表明释放成功

2)写锁释放成功后:唤醒头节点的后继节点去竞争锁

读锁

上面是写锁的获取和释放,是不是很简单,简单的前提是了解AQS里面的同步状态获取释放机制,所以这里再次提醒下AQS十分重要,如果直接看锁源码看不懂,建议先尝试了解AQS,下面看下读锁(和写锁类似Sync和AQS相关部分不再重复讲述,有兴趣可以翻看之前的AQS文章和本文的开头部分):

public void lock() {
            sync.acquireShared(1);
}

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
}

protected final int tryAcquireShared(int unused) {
            //1、获取当前线程和同步状态
            Thread current = Thread.currentThread();
            int c = getState();
            //2、如果当前拥有锁的线程为独占锁且不是当前线程,返回-1
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            //3、根据同步状态计算拥有共享锁线程数
            int r = sharedCount(c);
            //4、如果读不被阻塞且拥有共享锁线程数小于最大值且CAS增加当前拥有共享锁线程数成功
            if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
                //4.1、如果拥有共享锁线程数为0,则赋值首个获取读锁的线程为当前线程,且首个读锁线程计数为1
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                //4.2、能到这一步,说明拥有共享锁线程数大于0,如果当前线程等于首歌获取读锁线程,则直接则对应的首个读锁线程技术+1。
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                //4.3、如果当前拥有读锁的线程数不为0,且当前线程不是首个获取读锁的线程,进入下列判断
                } else {
                    //4.3.1、从缓存(全局变量)获取上一次获取锁的线程
                    HoldCounter rh = cachedHoldCounter;
                    //4.3.2、如果缓存为空,或缓存记录上一次获取锁的线程不是当前线程,则从线程局部变量中获取,如果线程局部变量中没有则会以当前线程初始化创建一个HoldCounter对象(这一块最难的是get的理解,这个要深入查看才行,当我们get为空时会调用一个初始化方法,这个方法正好被ThreadLocalHoldCounter重写,后面会有源码解析)
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    //4.3.3、能到这一步说明缓存中有当前线程信息,如果当前线程获取锁的次数为0,说明readHolds线程局部变量中已经没有了它的相关信息(根据readHolds的特性所知道的),次受将其重新放入线程局部变量。
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    //4.3.4、当前线程获取锁计数加一
                    rh.count++;
                }
                //4.4、能进当前逻辑,说明是符合获取锁条件的,所以返回1
                return 1;
            }
            //5、如果当前读锁竞争应该阻塞或者CAS设置失败,则进入全量版本的锁获取方法
            return fullTryAcquireShared(current);
}

这里先简单说一下get的获取逻辑,方便大家理解,然后再介绍readerShouldBlock和全量版本的获取锁方法

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
//可以看到如果取不到对应的信息,会调用setInitialValue方法
private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
//这里要留意的是ThreadLocal里的initialValue方法,当取不到数据时会执行这个方法初始化创建,而在ThreadLocalHoldCounter中继承了ThreadLocal后重写了该方法
static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }
//到这是不是就清晰了
static final class HoldCounter {
            int count = 0;
            // Use id, not reference, to avoid garbage retention
            final long tid = getThreadId(Thread.currentThread());
        }

下面我们再接着说readerShouldBlock和全量版本的锁获取

//与公平锁下写锁判读相同,都是验证同步队列中是否还有前驱节点
final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
}

//为了解决共享锁获取失败或CAS设置共享线程数失败的问题
final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            //1、无限循环,直至获取到锁或获取失败或抛异常
            for (;;) {
                //1.2、如果同步状态被写锁线程获取且获取锁的线程不是当前线程,返回-1
                int c = getState();
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                //1.3、到这一步,说明没有写锁线程竞争同步状态,此时判断同步队列中是否还有其他竞争节点。如果有则进入下面的判断
                } else if (readerShouldBlock()) {
                    //1.3.1、如果当前线程为首个获取读锁的线程,则不作处理
                    if (firstReader == current) {
                        
                    } else {
                    //1.3.2、到这一步说明当前线程不是首个获取读锁的线程,且同步队列中有不止一个节点等待竞争锁
                        if (rh == null) {
                            //1.3.2.1、获取缓存中上一次获取读锁的线程
                            rh = cachedHoldCounter;
                            //1.3.2.2、如果缓存信息为空或者缓存的最后一个获取读锁线程不是当前线程,则重新构建缓存对象(注意,get方法获取不到时会调用初始化创建方法,源码在前面讲过)
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                //如果线程局部变量中记录线程拿到锁次数为0,则调用remove方法避免ThreadLoack弱引用导致的内存泄漏(ThreadLoack相关知识可自行查阅)
                                if (rh.count == 0)
                                    readHolds.remove();
                            }
                        }
                        //1.3.2.3、经过上一步过滤,rh.count为0的局部变量应该计数大于0,此处等于0表明线程已经获取到锁并执行结束,所以此时不再需要竞争锁,直接返回-1
                        if (rh.count == 0)
                            return -1;
                    }
                }
                //1.4、如果共享锁持有数大于最大值,抛异常
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //1.5、CAS方式设置读锁数加一,成功后更新首个读线程全局变量以及最后一次读线程的缓存信息。并在最后返回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;
                }
            }
        }

上面是读锁的获取源码,大致流程是:尝试获取同步状态,如果同步状态未被修改,或者可重入则获取锁成功。如果获取失败则创建节点放入同步队列,然后等待唤醒竞争锁,由于要获取的是共享锁,所以此时在拿到锁之后还有个共享传播的流程。

最后来看一下共享锁的释放:

public void unlock() {
            sync.releaseShared(1);
}

public final boolean releaseShared(int arg) {
        //1、尝试释放锁
        if (tryReleaseShared(arg)) {
            //2、共享锁释放成功,通知后继节点以及保证共享锁传播(这里在AQS中介绍过了,此处也不在重新介绍)
            doReleaseShared();
            return true;
        }
        return false;
}

protected final boolean tryReleaseShared(int unused) {
            //1、获取当前线程,并判断是否是首个获取读锁的线程
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                //1.1、如果当前线程是首个获取读锁的线程,且拿到锁次数为1,此时锁释放后其不在持有锁,故直接将对应变量赋值空。否则只是将重入次数减一
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
            //2、如果当前线程不是首个获取读锁线程,则获取缓存中最后一次获取读锁的线程信息
                HoldCounter rh = cachedHoldCounter;
                //2.1、如果缓存为空或最后一次获取读锁线程不是当前线程,则以当前线程初始化创建一个,get方法的解析在前面有,这里不重复介绍
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                //2.2、获取当前线程拿取锁的次数(即锁重入次数)
                int count = rh.count;
                if (count <= 1) {
                    //2.2.1、如果当前线程锁重入次数为1,在释放后则是为空,所以这里从线程局部变量中移除,便于垃圾回收和避免ThreadLocal内存泄漏
                    readHolds.remove();
                    //2.2.2、如果当前线程没有持有锁,进入锁释放方法要抛异常
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                //2.3、缓存中锁重入次数减一
                --rh.count;
            }
            //2、for循环设置同步状态中读锁持有线程数减一,直至成功才退出
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
}

上面是共享锁释放的源码,大致流程是先尝试释放,释放成功后开始唤醒后继节点以及保证共享锁的传播。

到此为止排它锁、共享锁的基本使用源码已经查看完毕,还有一些相关的知识点,由于篇幅原因,这个在下一节讲解。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值