jdk 源码系列之ReentrantLock

Posted by Sinsy on November 10, 2020 About 11 k words and Need 32 min

最近将 ReentrantLock 学了一遍同时也把源码读了一遍,记录下学习的过程

JDK 源码系列

使用

使用锁机制,来保障线程安全

1
2
3
4
5
6
7
8
9
    Lock lock = new ReentrantLock();

    lock.lock();

    try {
        // 受此锁保护的资源块
    } finally {
        lock.unlock();
    }

或者你可以使用 tryLock() 方法,在多线程中,当一个线程释放锁的时候,就尝试去获取锁。

1
2
3
4
5
6
7
8
9
10
    Lock lock = new ReentrantLock();
    if (lock.tryLock()) {
    try {
        // 受此锁保护的资源块
    } finally {
        lock.unlock();
    }
    } else {
    // 进行其他操作,未被锁保护
    }

这种方法,在确保获取锁的时候才会解锁,并且在未获锁时不会尝试去解锁。

如果尝试去获取锁的时候太长,也可以给获取锁的这个过程加上时间,超时则直接中断线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    public static void main(String[] args) throws InterruptedException {
        Lock lock = new ReentrantLock();
        if (lock.tryLock(5, TimeUnit.SECONDS)) {
            try {
                // manipulate protected state
                
            } 
            finally {
                lock.unlock();
            }
        } else {
            // perform alternative actions
        }

    }

}

如果希望当前锁的模块不是立刻执行,也可以调用 await 机制

1
2
3
4
5
6
7
8
9
    Lock lock = new ReentrantLock();
    lock.lock();
    try {
        // manipulate protected state
        lock.newCondition().await(5, TimeUnit.SECONDS);
    }
    finally {
        lock.unlock();
    }

有时候,当遇到过长的业务流程,导致持有的时间太长了,可以考虑打断锁的机制,释放锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    Lock lock = new ReentrantLock();
    lock.lock();
    try {
        // manipulate protected state
        long startTime = System.currentTimeMillis();
        long endTime = System.currentTimeMillis();
        if (endTime - startTime > 10) {
            lock.lockInterruptibly();
            try {
            } finally {
                lock.unlock();
            }
        }
        
        lock.newCondition().await(5, TimeUnit.SECONDS);
    }
    finally {
        lock.unlock();
    }

应用场景比较

方法 说明 适合场景 注意事项
lock 获取锁 保证顺序,同步进行 必须小心以确保通过try-finally或try-catch保护持有锁定时执行的所有代码,以确保在必要时释放锁定
tryLock 仅在调用时释放锁时才获取锁 在没有锁的情况下,可以去做别的事情,充分利用线程 如果加上了获取时间,必须记录情况和异常类型
lockInterruptibly 除非当前线程被中断,否则获取锁 业务时间过长,持有的锁太久,可以直接中断 必须记录情况和异常类型
newCondition 绑定条件在锁上 实现一个延迟加锁机制 必须记录情况
unlock 释放锁 只要使用到了锁,最终都要必须释放锁 只要使用上了锁,必须使用到该方法

源码

先看类

1
2
public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;

向上继承了 Lock 接口,以及 Serializable,都是实现了 Lock 的方法。

  • void lock() 获取锁
  • void lockInterruptibly() 中断锁机制
  • boolean tryLock() 其他线程有释放锁,才调用获取锁
  • boolean tryLock(long time, TimeUnit unit) 给定时间内线程是空闲时间,且其他线程有释放锁,才调用获取锁
  • void unlock() 释放锁
  • Condition newCondition() 给锁绑定条件

实例化锁

1
2
3
4
5
Lock lock = new ReentrantLock();

Lock lock1 = new ReentrantLock(false);

Lock lock2 = new ReentrantLock(true);

这里,有两个构造,一个是无参构造,一个是传入布尔值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    /**
     * 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) {
        // true 公平锁 false 非公平锁
        sync = fair ? new FairSync() : new NonfairSync();
    }

这里默认是构造一个非公平锁,也可以直接设置创建什么类型锁,比如公平锁、非公平锁。

我们先看看公平锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            // 当使用 lock.lock() 的时候调到 同时传入1
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
         // 这里 acquires = 1指的是 上锁的状态 0则是未上锁
        protected final boolean tryAcquire(int acquires) {
            // 获取当前线程对象
            final Thread current = Thread.currentThread();
            // 是否上锁,首次默认是0
            int c = getState();
            if (c == 0) {
                // hasQueuedPredecessors 是否是当前线程,使用了双向链表的数据结构,做一个 queue,这里的意思大概是到了这个线程可以加锁了。不用排队了
                if (!hasQueuedPredecessors() &&
                    // CAS 比较后修改成功
                    compareAndSetState(0, acquires)) {
                    // 将当前线程设置成独占线程
                    setExclusiveOwnerThread(current);
                    // 获取锁成功
                    return true;
                }
            }
            // 独占线程
            else if (current == getExclusiveOwnerThread()) {
                // nextc = 1
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // 同步锁的状态 = 1
                setState(nextc);
                return true;
            }
            // 其他线程则未获取到锁
            return false;
        }
    }

    /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        // 尝试上锁
        if (!tryAcquire(arg) &&
            // 先将线程当前线程添加进队列最后一个,然后在判断是否到他上锁了
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 没有则直接打断线程
            selfInterrupt();
    }


    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
     // 持有锁的队列里面的线程
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 一个死循环你判断里面当前谁获得锁
            for (;;) {
                // 指向下一个节点
                final Node p = node.predecessor();
                // 队列的第一个,且获取锁,将当前线程变成独占线程
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    // 如果获得到锁,则不需要打断当前线程返回一个 false
                    return interrupted;
                }
                // 线程阻塞了
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 中断获取锁的机制
                    interrupted = true;
            }
        } finally {
            // 如果获得到锁 则不需要取消加锁操作、反之则取消
            if (failed)
                cancelAcquire(node);
        }
    }

公平锁,当前的线程会进入一个队列中最后一个,等待到他的上锁。如果当前的线程表示获取到锁,且也是队列的第一个位置。会将自己变成锁的独占线程,只有自己才持有这个锁的对象。同时这里进行了 CAS 的原子交换,设置状态为1。其中 0 为 未上锁状态,1为上锁状态。

非公平锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
    /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            // 先进行抢占锁,如果如可以修改状态,则直接变成上锁状态
            if (compareAndSetState(0, 1))
                // 设置独占线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 加锁
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

        /**
         * 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();
            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;
        }

非公平锁,先进行抢占锁,先查看线程是否释放了锁,如果释放了,当前线程则直接上锁,这样的好处就是减少了线程之间的等待,加快了上锁的机制,避免了排队时竞争所导致的延时,提高了性能,如果没有释放锁,则进入到队列的最后一个等待上锁。

小结一下。

锁的类型 机制 效率
公平锁 将线程放进队列的最后一个,等待上锁 一般
非公平 先进行抢占锁,获取到锁则直接上锁,没有则将线程放进队列的最后一个等待上锁 较高

释放锁机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
    /**
     * Attempts to release this lock.
     *
     * <p>If the current thread is the holder of this lock then the hold
     * count is decremented.  If the hold count is now zero then the lock
     * is released.  If the current thread is not the holder of this
     * lock then {@link IllegalMonitorStateException} is thrown.
     *
     * @throws IllegalMonitorStateException if the current thread does not
     *         hold this lock
     */
    public void unlock() {
        // 携带 1
        sync.release(1);
    }


    public final boolean release(int arg) {
        // 释放成功则为 true 否则是 false
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                // 唤醒队列后面的线程
                unparkSuccessor(h);
            // 释放成功
            return true;
        }
        // 释放失败
        return false;
    }



    protected final boolean tryRelease(int releases) {

        // c = 0
        int c = getState() - releases;
        // 如果不是当前线程和不是独占线程
        if (Thread.currentThread() != getExclusiveOwnerThread())
            // 直接抛出错
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            // 释放锁
            free = true;
            // 独占线程设置null
            setExclusiveOwnerThread(null);
        }
        // 0
        setState(c);
        return free;
    }

锁的释放,比较简单。将锁状态重新设置回 0,同时独占线程也设置null,之后唤醒后面的队列里面的线程,完成释放。

源码总结

ReentrantLock 创建的时候,默认是非公平锁,不过你也可以在构造的时候,也可以创建一个公平锁。 其中通过 CAS 改变 state 的状态来改变锁的数值, 0 表示有锁可以获取,1 表示锁已被获取,来设置锁的独占线程。

在公平锁的机制中,请求锁的线程会直接排到一个队列中(通过一个双向链表来模拟的队列)的最后一个,去获取锁。

非公平锁的机制中,请求锁的线程首先会先通过 CAS 来改变 state 的锁状态,如果可以改变(0 -> 1),则直接获取到锁,将自身设置成独占锁。这样的好处就减少了一些进队列、加载队列、唤醒线程等性能消耗。如果未能修改到 state 的状态,也会变成公平锁的机制,进入到队列的最后一个,等待到它去获取锁。

锁的释放,将 state 重新设置回 0,同时独占线程(你也可以认为这是持有锁的线程对象)设置null,之后唤醒排在它下个的线程。这一系列步骤做完,则宣告锁的释放。

优缺点

非公平锁,确实性能比较高。不过也有一个显而易见的缺点,我们可以想象一下,当你在排队吃饭的时候,轮到你吃饭的时候,这时候突然来一个人插在你前面,提前打饭了,导致你打饭时间变长了,如果这时候在有几个人在也突然插到你前打饭,又会继续导致你打饭时间变得更长。那如果放到线程里面,突然其他线程提前获取到了锁,那会导致当前线程获取到锁时间变长,而导致线程阻塞,迟迟未获取到锁。

所以得根据业务去选择合适的锁类型,进行上锁,尽可能的避免有一些重要的业务因为上锁而阻塞到。

声明

作者: Sinsy
本文链接:https://blog.sincehub.cn/2020/11/10/jdk-reentrantLock/
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文声明。
如您有任何商业合作或者授权方面的协商,请给我留言:550569627@qq.com

引用