概念

image-20211225174538717

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

LockSupport中的park()unpark()的作用分别是阻塞线程和解除阻塞线程。

使用

三种让线程等待和唤醒的方法

  1. synchronized+wait+notify
  2. lock+await+signal
  3. LockSupport+park+unpark

JUC之synchronized和Lock | Kylin (codekylin.cn)

synchronized和lock

  1. 使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程。
  2. 使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程。
  3. LockSupport类可以阻塞当前线程已经唤醒指定被阻塞的线程。

synchronized

image-20211225180951299

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
package com.kylin;

/**
* @author kylin
*/
public class SynchronizedDemo {

static Object objectLock = new Object();

public static void main(String[] args) {
new Thread(() -> {
synchronized (objectLock) {
System.out.println(Thread.currentThread().getName() + "---------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"---------被唤醒");
}
}, "A").start();

new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
System.out.println(Thread.currentThread().getName() + "---------通知");
}
}, "B").start();
}
}

A线程让线程暂停,B线程唤醒A线程执行。

image-20211225181056398

此时一切正常。接着把同步代码快注释

image-20211225181252637

运行抛出java.lang.IllegalMonitorStateException异常。说明waitnotify是无法单独脱离synchronized使用的。

接着我们让A线程运行先暂停3秒钟,确保B线程先运行。也就是notify和wait先后过程调换,先notify再wait。

image-20211225181928899

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
package com.kylin;

import java.util.concurrent.TimeUnit;

/**
* @author kylin
*/
public class SynchronizedDemo {

static Object objectLock = new Object();

public static void main(String[] args) {
new Thread(() -> {
//暂停3秒钟
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectLock) {
System.out.println(Thread.currentThread().getName() + "---------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"---------被唤醒");
}
}, "A").start();

new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
System.out.println(Thread.currentThread().getName() + "---------通知");
}
}, "B").start();
}
}

A线程暂停3秒钟,确保B线程先执行notify方法。B线程先notify(此时没有线程被暂停,没有唤醒任何线程),3秒钟过后,A线程继续执行,阻塞等待。一直没有线程将其唤醒。程序一直运行….

总结

  1. wait和notify方法必选要再同步代码快或者同步方法里面而且成对出现使用
  2. 遵循先wait后notify

Lock

image-20211225200739715

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
package com.kylin;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
* @author kylin
*/
public class LockDemo {

static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();

public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "---------come in");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + "---------被唤醒");

}, "A").start();

new Thread(() -> {
lock.lock();
try {
condition.signal();
System.out.println(Thread.currentThread().getName() + "---------通知");
}finally {
lock.unlock();
}
}, "B").start();
}
}

将lock的lockunlock操作注释。运行代码

image-20211225200915312

image-20211225200952866

同样运行抛出java.lang.IllegalMonitorStateException异常

接着我们同样是A线程先暂停3秒钟,确保B线程先运行,通知唤醒。

image-20211225201235308

同样程序处于一直运行状态,A线程没有被唤醒。

这和使用synchronized是一样的问题,只不过对应Api有所区分。

传统的synchronized和Lock实现等待唤醒通知的约束

  • 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
  • 必须要先等待后唤醒,线程才能够被唤醒

LockSupport

通过park()unpark(thread)方法来实现阻塞和唤醒线程的操作

LockSupport类使用一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和0,默认是零。

可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。

image-20211225204613398

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author kylin
*/
public class LockSupportDemo {

public static void main(String[] args) {
Thread a = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "---------come in");
//被阻塞...等地啊通知等待放行,它要通过需要许可证
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "---------被唤醒");
}, "A");
a.start();

Thread b = new Thread(() -> {
//唤醒A线程
LockSupport.unpark(a);
System.out.println(Thread.currentThread().getName() + "---------通知");
}, "B");
b.start();
}
}

接着同样是A线程暂停3秒钟,确保B线程先执行唤醒操作。

image-20211225204804986

并没有出现抛出异常的情况,B线程运行3秒钟后,A线程正常运行,并被正常唤醒。

通过以上代码可以看出,LockSupport不需要同步代码块之类的前提,同时也支持先唤醒后等待这种操作。(先唤醒,后面的等待操作就没有用了,相当于抵消了)

详解

LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。总之,LockSupport调用的Unsafe中的native代码。

image-20211225205711360

1
2
3
public static void park() {
UNSAFE.park(false, 0L);
}

image-20211225205750274

1
2
3
4
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}

LockSupport提供park()和unpark()方法来实现阻塞线程和解除线程阻塞的过程。

LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0。

调用一次unpark就加1,变成1。

调用一次park会消费permit,也就是将1变成0,同时park立即返回。

如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。

每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证。

image-20211225210948916

  1. A线程运行首先使用park(),将线程阻塞,permit为0。
  2. B线程调用unpark(a)两次,permit由0+1=1变成了1,不能积累。
  3. A线程不唤醒,再次调用park(),将线程阻塞,permit为0。没有唤醒操作,程序一直运行…
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

/**
* @author kylin
*/
public class LockSupportDemo {

public static void main(String[] args) {
Thread a = new Thread(() -> {
//try {
// TimeUnit.SECONDS.sleep(3);
//} catch (InterruptedException e) {
// e.printStackTrace();
//}
System.out.println(Thread.currentThread().getName() + "---------come in");
//被阻塞...等地啊通知等待放行,它要通过需要许可证
LockSupport.park();
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "---------被唤醒");
}, "A");
a.start();

Thread b = new Thread(() -> {
//唤醒A线程
LockSupport.unpark(a);
LockSupport.unpark(a);
System.out.println(Thread.currentThread().getName() + "---------通知");
}, "B");
b.start();
}
}

线程阻塞需要消耗凭证(permit),这个凭证最多只有一个。

当调用park方法时

  • 如果有凭证,则会直接消耗掉这个凭证然后正常退出
  • 如果无凭证,就必选阻塞等待凭证可用

而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。

为什么可以先唤醒线程后阻塞线程?

因为unpark获得一个凭证,之后调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证。而调用两次park却需要消耗两个凭证,证不够,不能放行。