reentranlock底层原理

[TOC]

类结构

首先ReentrantLock继承自父类Lock,然后有3个内部类,其中中有一个抽象内部类Sync继承AQS,两个内部类NonfairSync和FairSync继承了Sync(Sync重写tryRelease),重写了lock()方法和tryAcquire()方法,分别实现了非公平锁和公平锁。
在这里插入图片描述
ReentrantLock默认为非公平锁,如果想创建公平锁,可给构造方法传入参数true

1
2
3
4
5
6
7
8
public ReentrantLock() {
sync = new NonfairSync();
}

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

ReentrantLock有两个构造方法,无参构造方法默认是创建非公平锁,而传入true为参数的构造方法创建的是公平锁

非公平锁的实现原理

当我们使用无参构造方法构造的时候即ReentrantLock lock = new ReentrantLock(),创建的就是非公平锁。

1
2
3
4
5
6
7
8
9
public ReentrantLock() {
sync = new NonfairSync();
}

//或者传入false参数 创建的也是非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
复制代码

lock方法获取锁

  1. lock方法调用CAS方法设置state的值,如果state等于期望值0(代表锁没有被占用),那么就将state更新为1(代表该线程获取锁成功),然后执行setExclusiveOwnerThread方法直接将该线程设置成锁的所有者。如果CAS设置state的值失败,即state不等于0,代表锁正在被占领着,则执行acquire(1),即下面的步骤。
  2. nonfairTryAcquire方法首先调用getState方法获取state的值,如果state的值为0(之前占领锁的线程刚好释放了锁),那么用CAS设置state的值,设置成功则将该线程设置成锁的所有者,并且返回true。如果state的值不为0,那就调用getExclusiveOwnerThread方法查看占用锁的线程是不是自己,如果是的话那就直接将state + 1,然后返回true。如果state不为0且锁的所有者又不是自己,那就返回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
作者:冠状病毒biss
链接:https://www.nowcoder.com/discuss/355081?type=post&order=time&pos=&page=1&channel=0&source_id=search_post
来源:牛客网

弊端:可能导致排队的线程一直无法得到CPU资源的饥饿现象
public void lock() {
sync.lock();
}
1.调用NofairSync中的lock方法
final void lock() {
if (compareAndSetState(0, 1))//AQS类的方法 使用CAS算法更新state的值
setExclusiveOwnerThread(Thread.currentThread());//若更新成功设置当前线程为独占线程c
else
acquire(1);//若CAS更新失败,执行是AQS类的acquire()方法
}
2.AQS中的acquire()方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&//尝试获取锁 失败则调用addWaiter方法创建结点并追加到队列尾部
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//然后调用acquireQueued阻塞或者自旋尝试获取锁
selfInterrupt();//在 acquireQueued 中,如果线程是因为中断而退出的阻塞状态会返回true
}
3.Nofair中的tryAcquire()重写
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//如果当前state值为0 并CAS操作成功 独占锁 返回true
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}//否则判断当前线程是否是持有锁的那个独占线程
else if (current == getExclusiveOwnerThread()) {//相当于重入锁
int nextc = c + acquires;//是将state值更新
if (nextc < 0) // 假如超过最大可重入次数
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;//state不为0,并且不是那个持有锁的线程 返回false
}

tryRelease锁的释放

  1. 判断当前线程是不是锁的所有者,如果是则进行步骤2,如果不是则抛出异常。
  2. 判断此次释放锁后state的值是否为0,如果是则代表锁没有重入,然后将锁的所有者设置成null且返回true,然后执行步骤3,如果不是则代表锁发生了重入执行步骤4
  3. 现在锁已经释放完,即state=0,唤醒同步队列中的后继节点进行锁的获取。
  4. 锁还没有释放完,即state!=0,不唤醒同步队列。

img

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
public void unlock() {
sync.release(1);
}

public final boolean release(int arg) {
//子类重写的tryRelease方法,需要等锁的state=0,即tryRelease返回true的时候,才会去唤醒其
//它线程进行尝试获取锁。
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

protected final boolean tryRelease(int releases) {
//状态的state减去releases
int c = getState() - releases;
//判断锁的所有者是不是该线程
if (Thread.currentThread() != getExclusiveOwnerThread())
//如果所的所有者不是该线程 则抛出异常 也就是锁释放的前提是线程拥有这个锁,
throw new IllegalMonitorStateException();
boolean free = false;
//如果该线程释放锁之后 状态state=0,即锁没有重入,那么直接将将锁的所有者设置成null
//并且返回true,即代表可以唤醒其他线程去获取锁了。如果该线程释放锁之后state不等于0,
//那么代表锁重入了,返回false,代表锁还未正在释放,不用去唤醒其他线程。
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
复制代码

公平锁的实现原理

lock方法获取锁

  1. 获取状态的state的值,如果state=0即代表锁没有被其它线程占用(但是并不代表同步队列没有线程在等待),执行步骤2。如果state!=0则代表锁正在被其它线程占用,执行步骤3
  2. 判断同步队列是否存在线程(节点),如果不存在则直接将锁的所有者设置成当前线程,且更新状态state,然后返回true。
  3. 判断锁的所有者是不是当前线程,如果是则更新状态state的值,然后返回true,如果不是,那么返回false,即线程会被加入到同步队列中

通过步骤2实现了锁获取的公平性,即锁的获取按照先来先得的顺序,后来的不能抢先获取锁,非公平锁和公平锁也正是通过这个区别来实现了锁的公平性。

img

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
final void lock() {
acquire(1);
}

public final void acquire(int arg) {
//同步队列中有线程 且 锁的所有者不是当前线程那么将线程加入到同步队列的尾部,
//保证了公平性,也就是先来的线程先获得锁,后来的不能抢先获取。
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//判断状态state是否等于0,等于0代表锁没有被占用,不等于0则代表锁被占用着。
if (c == 0) {
//调用hasQueuedPredecessors方法判断同步队列中是否有线程在等待,如果同步队列中没有
//线程在等待 则当前线程成为锁的所有者,如果同步队列中有线程在等待,则继续往下执行
//这个机制就是公平锁的机制,也就是先让先来的线程获取锁,后来的不能抢先获取。
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//判断当前线程是否为锁的所有者,如果是,那么直接更新状态state,然后返回true。
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//如果同步队列中有线程存在 且 锁的所有者不是当前线程,则返回false。
return false;
}
复制代码

tryRelease锁的释放

公平锁的释放和非公平锁的释放一样,这里就不重复。
公平锁和非公平锁的公平性是在获取锁的时候体现出来的,释放的时候都是一样释放的。

ReentrantLock的等待/通知机制

我们知道关键字Synchronized + ObjectwaitnotifynotifyAll方法能实现等待/通知机制,那么ReentrantLock是否也能实现这样的等待/通知机制,答案是:可以。
ReentrantLock通过Condition对象,也就是条件队列实现了和waitnotifynotifyAll相同的语义。 线程执行condition.await()方法,将节点1从同步队列转移到条件队列中。

img

线程执行condition.signal()方法,将节点1从条件队列中转移到同步队列。

img

因为只有在同步队列中的线程才能去获取锁,所以通过Condition对象的waitsignal方法能实现等待/通知机制。
代码示例:

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
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void await() {
lock.lock();
try {
System.out.println("线程获取锁----" + Thread.currentThread().getName());
condition.await(); //调用await()方法 会释放锁,和Object.wait()效果一样。
System.out.println("线程被唤醒----" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("线程释放锁----" + Thread.currentThread().getName());
}
}

public void signal() {
try {
Thread.sleep(1000); //休眠1秒钟 等等一个线程先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try {
System.out.println("另外一个线程获取到锁----" + Thread.currentThread().getName());
condition.signal();
System.out.println("唤醒线程----" + Thread.currentThread().getName());
} finally {
lock.unlock();
System.out.println("另外一个线程释放锁----" + Thread.currentThread().getName());
}
}

public static void main(String[] args) {
Test t = new Test();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
t.await();
}
});

Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
t.signal();
}
});

t1.start();
t2.start();
}
复制代码

运行输出:

1
2
3
4
5
6
7
线程获取锁----Thread-0
另外一个线程获取到锁----Thread-1
唤醒线程----Thread-1
另外一个线程释放锁----Thread-1
线程被唤醒----Thread-0
线程释放锁----Thread-0
复制代码

执行的流程大概是这样,线程t1先获取到锁,输出了”线程获取锁—-Thread-0”,然后线程t1调用await方法,调用这个方法的结果就是线程t1释放了锁进入等待状态,等待唤醒,接下来线程t2获取到锁,然输出了”另外一个线程获取到锁—-Thread-1”,同时线程t2调用signal方法,调用这个方法的结果就是唤醒一个在条件队列(Condition)的线程,然后线程t1被唤醒,而这个时候线程t2并没有释放锁,线程t1也就没法获得锁,等线程t2继续执行输出”唤醒线程—-Thread-1”之后线程t2释放锁且输出”另外一个线程释放锁—-Thread-1”,这时候线程t1获得锁,继续往下执行输出了线程被唤醒----Thread-0,然后释放锁输出”线程释放锁—-Thread-0”

如果想单独唤醒部分线程应该怎么做呢?这时就有必要使用多个Condition对象了,因为ReentrantLock支持创建多个Condition对象,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//为了减少篇幅 仅给出伪代码
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Condition condition1 = lock.newCondition();

//线程1 调用condition.await() 线程进入到条件队列
condition.await();

//线程2 调用condition1.await() 线程进入到条件队列
condition1.await();

//线程32 调用condition.signal() 仅唤醒调用condition中的线程,不会影响到调用condition1。
condition1.await();
复制代码

这样就实现了部分唤醒的功能。

Condition

和synchronized的对比

  1. 锁的实现
    synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
  2. 性能
    新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
  3. 等待可中断
    当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
    ReentrantLock 可中断,而 synchronized 不行。
  4. 公平锁
    公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
    synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
  5. 锁绑定多个条件
    一个 ReentrantLock 可以同时绑定多个 Condition 对象。
    使用选择
    除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一
    种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没
    有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

可中断

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
public class hhh {
static class ThreadDemo implements Runnable {
Lock firstLock;
Lock secondLock;
public ThreadDemo(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
firstLock.lockInterruptibly();
TimeUnit.MILLISECONDS.sleep(50);
secondLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()
+"获取到了资源,正常结束!");
}
}
}
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args)
throws InterruptedException {
Thread thread = new Thread(new ThreadDemo(lock1, lock2));
Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));
thread.start();
thread1.start();
thread.interrupt();//是第一个线程中断
}

}

作者:薛8
链接:https://juejin.im/post/5c95df97e51d4551d06d8e8e
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

https://www.javadoop.com/post/AbstractQueuedSynchronizer