概念

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程。
使用
三种让线程等待和唤醒的方法
- synchronized+wait+notify
- lock+await+signal
- LockSupport+park+unpark
JUC之synchronized和Lock | Kylin (codekylin.cn)

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

| 12
 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;
 
 
 
 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线程执行。

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

运行抛出java.lang.IllegalMonitorStateException异常。说明wait和notify是无法单独脱离synchronized使用的。
接着我们让A线程运行先暂停3秒钟,确保B线程先运行。也就是notify和wait先后过程调换,先notify再wait。

| 12
 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;
 
 
 
 
 public class SynchronizedDemo {
 
 static Object objectLock = new Object();
 
 public static void main(String[] args) {
 new Thread(() -> {
 
 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线程继续执行,阻塞等待。一直没有线程将其唤醒。程序一直运行….
总结
- wait和notify方法必选要再同步代码快或者同步方法里面而且成对出现使用
- 遵循先wait后notify
Lock

| 12
 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;
 
 
 
 
 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的lock和unlock操作注释。运行代码


同样运行抛出java.lang.IllegalMonitorStateException异常
接着我们同样是A线程先暂停3秒钟,确保B线程先运行,通知唤醒。

同样程序处于一直运行状态,A线程没有被唤醒。
这和使用synchronized是一样的问题,只不过对应Api有所区分。
传统的synchronized和Lock实现等待唤醒通知的约束
- 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
- 必须要先等待后唤醒,线程才能够被唤醒
LockSupport
通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
LockSupport类使用一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和0,默认是零。
可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。

| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | 
 
 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(() -> {
 
 LockSupport.unpark(a);
 System.out.println(Thread.currentThread().getName() + "---------通知");
 }, "B");
 b.start();
 }
 }
 
 | 
接着同样是A线程暂停3秒钟,确保B线程先执行唤醒操作。

并没有出现抛出异常的情况,B线程运行3秒钟后,A线程正常运行,并被正常唤醒。
通过以上代码可以看出,LockSupport不需要同步代码块之类的前提,同时也支持先唤醒后等待这种操作。(先唤醒,后面的等待操作就没有用了,相当于抵消了)
详解
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。总之,LockSupport调用的Unsafe中的native代码。

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

| 12
 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也不会积累凭证。

- A线程运行首先使用park(),将线程阻塞,permit为0。
- B线程调用unpark(a)两次,permit由0+1=1变成了1,不能积累。
- A线程不唤醒,再次调用park(),将线程阻塞,permit为0。没有唤醒操作,程序一直运行…
| 12
 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
 
 | 
 
 
 public class LockSupportDemo {
 
 public static void main(String[] args) {
 Thread a = new Thread(() -> {
 
 
 
 
 
 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(() -> {
 
 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却需要消耗两个凭证,证不够,不能放行。