由浅入深ReentrantLock源码阅读

重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

阅读这个可重入锁类之前,可以先阅读我的上两篇文章,对lock以及AbstractQueuedSynchronizer这两个类的作用和设计有一个基础的了解。然后再看着个类的时候,会更好的理解。
AQS(AbstractQueuedSynchronizer)队列同步器源码阅读(二)
https://www.jianshu.com/p/e0066f9349cd
AQS(AbstractQueuedSynchronizer)队列同步器源码阅读(一)
https://www.jianshu.com/p/a41088fc1516

然后我们可以知道一般来说,ReentrantLock是一个可重入锁,所以会实现Lock这个类,并重写该类提供的几个方法:
//获取锁,释放锁
void lock();
void unlock();
//可相应线程中断的获取锁,当线程被中断后,锁会被释放。而一般的lock则不会响应
void lockInterruptibly() throws InterruptedException;
//尝试获取锁,获取不到则返回false
boolean tryLock();
//与上一个一样
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//获取对应的condition,后面会专门研究,现在只要知道,这个可以用来配合完成不同线程的等待/通知机制。
Condition newCondition();

然后一般重写以上几个方法时,需要依赖队列同步器来获取同步状态,获取不到需要加入同步队列等等,所以一般还会设计工具类Sync继承AbstractQueuedSynchronizer

然后根据设计不同的锁去重写AQS提供的一些方法。

继承AQS的子类重写以上AQS提供的方法后,对外ReentrantLock是实现Lock提供的几个方法一般是调用Sync继承的AQS里面的方法。例如:

加锁时:

//调用的是AQS的子类sync的lock
public void lock() {
        sync.lock();
    }
//而lock方法一般会根据具体的锁的设计去实现我们已经重写的acquire方法。大体的锁的设计不会偏差太多。

然后我们来具体看一下:
ReentrantLock 是一个可重入锁。
关于线程的调度策略分为公平调度和非公平调度。
他的设计同样是内部有一个继承AQS的静态内部类Sync,同时为了区分不同的调度策略,有设计了两个子类继承Sync,

static final class NonfairSync extends Sync{.....}
static final class FairSync extends Sync{.....}

看类名可以看出,一个是针对非公平调度策略设计的锁,一个是公平调度的。两种重写同步队列器的方法的实现不同。

再来看构造方法:

public ReentrantLock() {
        sync = new NonfairSync();
    }

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

这回应该不用多解释了吧,看类名。所以说可重入锁是默认非公平调度策略的,就是说线程执行的顺序是不是按执行时间顺序执行的,是非公平的,效率比较高。

可重入性

可重入锁顾名思义就是线程获取到锁之后,可以再次获取该锁,而不会被锁阻塞。该锁实现需要解决两个问题:

1.线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。

2.锁的最终释放。。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。

以默认的非公平调度的锁实现来查看获取同步状态时,如何处理:

 final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //获取当前state,重入数量
            int c = getState();
            //如果还未有线程获取该锁
            if (c == 0) {
            //获取同步状态成功,则设置当前线程为持有锁的线程
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果已有线程获取该锁,且该线程为已持有锁的线程
            else if (current == getExclusiveOwnerThread()) {
              //设置重入的数量。并返回获取锁成功
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。

成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放同步状态时减少同步状态值。

protected final boolean tryRelease(int releases) {
//释放时减去同步状态值
            int c = getState() - releases;
            //如果当前线程与持有线程不一致,则报对象头状态异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
          //如果同步状态值为0,代表该锁已全部释放,需要释放锁,使其他线程能够获取该锁
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。

对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同.

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
//主要区别如下: 即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,
//则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;

非公平调度策略加锁的流程

final void lock() {

//1.如果同步操作state,获取同步状态成功,则设置当前线程为当前独占式获取锁.否则进行获取。

//compareAndSetState(0, 1)  使用的是:
//unsafe.compareAndSwapInt(this, stateOffset, expect, update);  CAS偏移地址来直接修改state的值。
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 2.否则获取同步状态。进去里面看实现
               acquire(1);
        }

//1.产时获取同步状态,(tryAcquire),  
失败则加入队尾tail,状态设置为EXCLUSIVE,(addWaiter(Node.EXCLUSIVE), arg))
同时进入自旋去获取同步状态,直到该节点前一个节点为头节点并获取成功,则出队列,并唤醒下一个节点,并且响应中断。(acquireQueued()
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

tryAcquire()看上面的分析.主要调用我们NonfairSync重写的nonfairTryAcquire。
如果获取非公平的可重入锁失败,则执行下面方法。该方法不做具体分析了,之前已经有具体分析过了。
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 1. 获得当前节点的先驱节点
                final Node p = node.predecessor();
                // 2. 当前节点能否获取独占式锁                  
                // 2.1 如果当前节点的先驱节点是头结点并且成功获取同步状态,即可以获得独占式锁
                if (p == head && tryAcquire(arg)) {
                    //队列头指针用指向当前节点
                    setHead(node);
                    //释放前驱节点
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 2.2 获取锁失败,线程进入等待状态等待获取独占式锁
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}

非公平调度策略释放锁的流程就不再说了,上面有。

然后本来想要再分享下,公平调度获取锁和解锁的过程,其实不用。解锁的过程是一致的,这个前面有说了。然后加锁的区别也是只有一点区别。就是获取同步状态的时候,需要判断前面是否有头节点,如果没有则可以直接获取同步状态,否则继续自旋。因为公平调度是根据时间线程执行的时间顺序获取锁的,所以通过控制这点,来判断是否按时间进行获取同步状态,从而控制获取锁的顺序。

从上面### 标记的那行开始看,以下代码的逻辑与nonfairTryAcquire基本上一直,唯一的不同在于增加了hasQueuedPredecessors的逻辑判断,方法名就可知道该方法用来判断当前节点在同步队列中是否有前驱节点的判断,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。

公平锁 VS 非公平锁

1.公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。

2.公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。

编写不易,给个赞

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容