ReentrantLock源码学习

学习ReentrantLock之前,先了解一下可重入锁的概念。何为可重入锁,顾名思义,就是可重入的。真是听君一席话,胜听一席话啊。

请添加图片描述

正经点,可重入锁就是能够支持同一个线程对资源的重复加锁。注意两个关键字:同一线程和重复。

像synchronized关键字也实现了可重入。用synchronized修饰的方法,在进行递归调用时,执行线程在获取了锁之后仍然能够连续多次获得该锁,并不会出现阻塞的情况。

再比如说,这篇文章要学习的ReentrantLock,也实现了可重入锁。并且ReentrantLock还支持公平锁和非公平锁(默认是非公平锁)。

1、ReentrantLock源码学习

1.1 构造方法

ReentrantLock的源码比较简单,并且它也是基于AQS实现的。先看看它的构造函数

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

默认就是非公平锁。

1.2 锁的释放

Sync类就是继承自AQS的,FairSync类和NonfairSync类又是继承自Sync。对于公平锁和非公平锁,其释放锁的逻辑都是一样的,所以在Sync类中实现。

    abstract static class Sync extends AbstractQueuedSynchronizer {
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            // 判断当前线程是不是占有锁的线程,如果不是,抛异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 同步变量state的值为0时,才释放锁,返回true
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 设置同步变量的值
            setState(c);
            return free;
        }
    }

可以发现可重入锁的释放逻辑,对于占有锁的线程来说,只有在同步变量state的值为0的时候,才算是释放了锁。

1.3 锁的获取

锁的获取分公平锁和非公平锁。非公平锁的获取逻辑实现在Sync类中

abstract static class Sync extends AbstractQueuedSynchronizer {
        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         * 非公平锁,获取锁
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            // 同步变量为0,说明没有线程占用锁
            if (c == 0) {
                // CAS获取锁,注意这里并没有判断该线程是不是同步队列的队头
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 判断当前线程是不是占有锁的线程
            else if (current == getExclusiveOwnerThread()) {
                // 增加同步变量state的值
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

可以发现Sync类似并没有重写AQS的tryAcquire方法,而是放到了它的子类FairSync类和NonfairSync类中去实现的。

看看NonfairSync类的源码

    // 非公平锁
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        // 上来先CAS获取一下锁,如果获取失败,再调用AQS的acquire方法
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        // 调用Sync类的nonfairTryAcquire方法
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

看看FairSync类的源码

    // 公平锁
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
        // 直接调用AQS的acquire方法
        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            // 同步变量为0,说明没有线程占有锁
            if (c == 0) {
                /**
                 * 判断同步队列中当前节点是否有前驱节点,也就是只有当前节点是头结点并且CAS成功的情况下,当
                 * 前线程才能占有锁
                 */
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 当前线程已经占有锁,则增加state变量的值
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

可以发现公平锁和非公平锁在获取锁的时候,唯一的差别就是公平锁判断了当前节点是不是头结点,只有是头结点的情况下才可能获取到锁。非公平锁就不一样了,上来就直接CAS。

上面的方法都是在ReentrantLock类内部用的,对外提供的接口如下

// 获取锁,在等待获取锁的过程中休眠并禁止一切线程调度
public void lock() {
    sync.lock();
}

// 在等待获取锁的过程中可被中断
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

// 尝试获取锁,获取到锁并返回true;获取不到并返回false
public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}

// 在指定时间内等待获取锁;过程中可被中断
public boolean tryLock(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

// 释放锁
public void unlock() {
    sync.release(1);
}

2、测试

测试一下ReentrantLock的公平锁和非公平锁。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

public class FairAndUnfairTest {

    private static Sync fairLock = new Sync(true);
    private static Sync noFairLock = new Sync(false);

    public static void testLock(Sync lock) {
        // 开启5个线程
        for (int i = 0; i < 5; i++) {
            new Thread(new Job(lock), String.valueOf(i)).start();
        }
    }

    private static class Job extends Thread {
        private Sync lock;
        public Job(Sync lock) {
            this.lock = lock;
        }

        public void run() {
            for (int i = 0; i < 2; i++) {
                lock.lock();
                System.out.println("locked by " + currentThread().getName() + ", waiting by " + lock.getQueueThreads());
                lock.unlock();
            }
        }
    }

    private static class Sync extends ReentrantLock {
        public Sync(boolean fair) {
            super(fair);
        }

        /**
         * 获取等待队列
         * @return
         */
        public List<String> getQueueThreads() {
            List<Thread> arrayList = new ArrayList<Thread>(super. getQueuedThreads());
            Collections.reverse(arrayList);
            List<String> list = new ArrayList<>();
            arrayList.forEach(el -> {
                list.add(el.getName());
            });
            return list;
        }
    }

    public static void main(String[] args) {
        //testLock(fairLock);
        testLock(noFairLock);
    }
}

公平锁输出如下

locked by 0, waiting by [1, 2]
locked by 1, waiting by [2, 4, 3, 0]
locked by 2, waiting by [4, 3, 0, 1]
locked by 4, waiting by [3, 0, 1, 2]
locked by 3, waiting by [0, 1, 2, 4]
locked by 0, waiting by [1, 2, 4, 3]
locked by 1, waiting by [2, 4, 3]
locked by 2, waiting by [4, 3]
locked by 4, waiting by [3]
locked by 3, waiting by []

非公平锁输出如下

locked by 0, waiting by [2]
locked by 0, waiting by [2, 3, 1, 4]
locked by 2, waiting by [3, 1, 4]
locked by 2, waiting by [3, 1, 4]
locked by 3, waiting by [1, 4]
locked by 3, waiting by [1, 4]
locked by 1, waiting by [4]
locked by 1, waiting by [4]
locked by 4, waiting by []
locked by 4, waiting by []

可以发现公平锁总是按照顺序来依次获取锁。而非公平锁却是连续获取。回顾nonfairTryAcquire(int acquires)方法,当一 个线程请求锁时,只要获取了同步状态即成功获取锁。在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。

非公平锁可能会出现线程饥饿的情况,当竞争的线程很多时,后面的线程可能一直都获取不到锁。那为啥ReentrantLock默认是非公平锁呢?经过上面的测试可以发现,在公平锁的情况下,线程进行了10次上下文切换,非公平锁情况下只进行了5次。

线程上下文切换是一个耗费时间和资源的操作,所以在线程竞争激烈的情况下,非公平锁无疑能够节省很多的资源。

总结一下就是:公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

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

推荐阅读更多精彩内容