JUC(6)LockSupport
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
线程等待唤醒机制
3种让线程等待和唤醒的方法
- 使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
- 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
- LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
wait()和notify()存在的不足
public class LockSupportDemo {
public static void main(String[] args) {
Object objectLock = new Object();
new Thread(() -> {
synchronized (objectLock) {
try {objectLock.wait();} catch (InterruptedException e) {e.printStackTrace();}
}
System.out.println(Thread.currentThread().getName() + "\t" + "被唤醒了");
}, "t1").start();
//暂停几秒钟线程
try {TimeUnit.SECONDS.sleep(3L);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
}
}, "t2").start();
}
}
这段代码运行完全没有任何问题,一个很正常的等待-唤醒流程。
但是,wait()和notify()方法能否脱离同步代码块呢?当我们作出这个尝试运行后发现,代码报了java.lang.IllegalMonitorStateException
异常
那么我们能否更换wait()和notify()的执行顺序呢?也就是先执行notify方法在执行wait方法,我们发现,代码虽然表面上没有报错,但是执行wait的那个线程一直在等待下一个唤醒他的线程。
故而我们得出结论· wait和notify方法必须要在同步块或者方法里面,且成对出现使用,先wait后notify才OK。
await()和signal()存在的不足
public class LockSupportDemo {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t" + "start");
condition.await();
System.out.println(Thread.currentThread().getName() + "\t" + "被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start();
//暂停几秒钟线程
try {TimeUnit.SECONDS.sleep(3L);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(() -> {
lock.lock();
try {
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + "\t" + "通知了");
}, "t2").start();
}
}
与之前的尝试一样,await和signal方法能否脱离lock()方法使用呢?显然不行,和同步代码块一样,也报了java.lang.IllegalMonitorStateException
异常,同样的先调用signal方法再调用await方法也会导致调用await的方法一直阻塞。
所以对于await和signal方法,我们可以得出结论
- Condtion中的线程等待和唤醒方法之前,需要先获取锁
- 一定要先await后signal,不要反了
以上两组方法的测试我们可以得出结论
- 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
- 必须要先等待后唤醒,线程才能够被唤醒
LockSupport
· 通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
This class associates, with each thread that uses it, a permit (in the sense of the
Semaphore
class). A call topark
will return immediately if the permit is available, consuming it in the process; otherwise it may block. A call tounpark
makes the permit available, if it was not already available. (Unlike with Semaphores though, permits do not accumulate. There is at most one.)
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和0,默认是0.可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
pack()-阻塞当前线程/阻塞传入的具体线程
public static void park() {
UNSAFE.park(false, 0L);
}
permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时, park方法会被唤醒,然后会将permit再次设置为0并返回
unpack()-唤醒处于阻塞状态的指定线程
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回
public class LockSupportDemo {
public static void main(String[] args) {
//正常使用+不需要锁块
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " " + "1111111111111");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " " + "2222222222222------end被唤醒");
}, "t1");
t1.start();
//暂停几秒钟线程
try {TimeUnit.SECONDS.sleep(3L);} catch (InterruptedException e) {e.printStackTrace();}
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + " -----LockSupport.unparrk() invoked over");
}
}
上述代码明显它不需要必须在锁块中调用,那么如果我们先执行unpack方法再执行pack方法,pack所在的线程会被阻塞吗?不会!因为unpack方法执行后,permit被置为1,当pack方法需要时,拿到permit发现是1,消费即可,不会阻塞
他解决了之前线程等待唤醒机制存在的两大问题
-
LockSupport不用持有锁块,不用加锁,程序性能好
-
先后执行顺序不影响是否导致阻塞
值得注意的是,如果我们先执行两次unpack方法,再执行两次pack方法,我们发现,线程被阻塞住了,因为unpack两次,并不会+2,许可证仍是1,第一个pack方法使用后置为0,第二个pack没有许可证也就阻塞住了。
总结
-
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。本质调用的Unsafe中的native方法。
-
LockSupport和每个使用它的线程都有一个许可(permit)关联,permit相当于0和1的开关,默认是0.
-
每个线程都有一个相关的permit,permit最多有一个,重复调用unpack不会积累凭证。
-
当调用pack方法时
- 如果有凭证,消耗凭证正常结束阻塞
- 如果没有凭证,就阻塞直到凭证可用(为1)
-
当调用unpack方法时,它会增加一个凭证,但凭证最多只能有一个,累加无效
-
调用被pack方法阻塞的线程的interrupt方法,也会唤醒该线程