LockSupport是什么
用来创建锁和其他同步类的基本线程阻塞原语
LockSupport就是线程等待唤醒机制 wait/notify 的改良加强版
那么问题来了,为什么要加强等待唤醒机制,原来使用的等待唤醒机制有什么缺点
使用synchornized和Lock的等待唤醒机制
1. 使用Object中的 wait() ,notify() 方法等待,唤醒线程
问题1.使用wait和notify的时候 必须包裹在synchronized关键字中
使用wait和notify的时候,添加了关键字 sychronized代码块
public class SyncTest {
static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
//wait方法被包裹在synchronized关键字里面了
synchronized (obj) {
try {
System.out.println(Thread.currentThread().getName() + "开始执行");
obj.wait();
System.out.println(Thread.currentThread().getName() + "开始执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AA").start();
new Thread(() -> {
// notifyAll方法被包裹在synchronized关键字里面了
synchronized (obj) {
System.out.println("AA线程被通知");
obj.notifyAll();
}
}, "BB").start();
}
}
执行结果如下
那么问题来了,如果不使用synchronized关键字呢, 将上述synchronized关键字给去掉,可以看到,刚好在执行wait和执行notifyAll方法的时候,出现了 IllegalMonitorStateException异常
问题2. 在程序执行的时候,如果先执行notify方法,然后执行wait方法,会发生什么呢
public class SyncTest {
static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
//wait方法被包裹在synchronized关键字里面了
synchronized (obj) {
try {
System.out.println(Thread.currentThread().getName() + "开始执行");
obj.wait();
System.out.println(Thread.currentThread().getName() + "开始执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AA").start();
new Thread(() -> {
// notifyAll方法被包裹在synchronized关键字里面了
synchronized (obj) {
System.out.println("AA线程被通知");
obj.notifyAll();
}
}, "BB").start();
}
}
执行结果如下,线程进入阻塞状态,一直不能将锁释放, AA线程一直不能被唤醒
2. 使用JUC包中的 Condition的await() 方法让线程等待,signal() 方法唤醒线程
问题1 Condition在使用的时候,和synchronized一样也需要被锁住,不然会出错
public class LockTest {
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() + "开始执行");
condition.await();
System.out.println(Thread.currentThread().getName() + "开始执行");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "AA").start();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "执行通知操作");
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "BB").start();
}
}
使用锁包裹住的运行结果
不使用锁包裹住的运行结果
问题2. 先执行唤醒操作,后执行等待操作
public class LockTest {
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "开始执行");
condition.await();
System.out.println(Thread.currentThread().getName() + "开始执行");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "AA").start();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "执行通知操作");
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "BB").start();
}
}
执行结果如下
synchornized和Lock的缺点
- 必须在同步代码块中完成
- 如果先执行唤醒操作,后执行等待操作,会出现死锁现象
LockSupport解决了这写问题
使用LockSupport类中的 park() 方法 让线程阻塞,unpark(Thread thread)方法 唤醒指定被阻塞的线程
public class LockSupportTest {
public static void main(String[] args) {
Thread aa = new Thread(() -> {
//try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
try {
System.out.println(Thread.currentThread().getName() + "开始执行");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " park后 开始执行");
} catch (Exception e) {
e.printStackTrace();
}
}, "AA");
aa.start();
Thread bb = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "执行通知操作");
LockSupport.unpark(aa);
} catch (Exception e) {
e.printStackTrace();
}
}, "BB");
bb.start();
}
}
不设置等待时间的操作结果,可以看到 LockSupport操作可以直接调用,而不需要在同步代码块中执行
设置等待时间的操作结果,可以看到,程序并没有出现死锁现象
,而是 在程序运行的时候 相当于是把 LockSupport.park(); 这一行命令忽略掉了一样
可以看到 LockSupport解决了 synchronized和Lock的缺点
LockSupport原理
LockSupport类使用了一种名为Permit ( 许可 ) 的概念 来做到阻塞和唤醒线程的功能,每个线程都有一个许可 ( permit )
permit 只有两个值 0 和 1,默认值为0
可以把许可看成是一种 ( 0, 1 ) 信号量 ( Semaphore ),但与Semaphore不同的是,许可的累加上限是1
LockSupport中的park()
和 unpark()
的作用分别是阻塞线程
和解除阻塞线程
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码
LockSupport阻塞线程,解除阻塞线程的过程
LockSupport和每个使用它的线程都有一个许可 ( permit ) 关联,permit相当于1,0 的开关,默认是0
调用一次unpart就加1 变成1
调用一次 park会消费 permit,也就是将1 变成 0 ,同时 park立即返回
如果再次调用 park 会变成阻塞 ( 因为permit为 0 了 会阻塞在这里,直到permit变成 1 ),这时调用unpark 会吧 permit 置为 1
每一个线程都有一个相关的permit, permit最多只有一个,重复调用 unpark也不会积累凭证
形象理解
线程阻塞需要消费凭证 ( permit ), 这个凭证最多只有1个
当调用park方法时
- 如果有凭证,则会直接消耗掉这个凭证然后正常退出
- 如果无凭证,就必须阻塞等待凭证可用
而unpark则相反,他会增加一个凭证,但凭证最多只能有1个,累加无效
使用生活实例理解:
park、unpark操作相当于是使用密码解锁不同的门,unpark是重置门的密码 重置了之后不管输入什么都可以开门( 不管重置几次,设置了1次之后 这个门的密码就永久改变了,只能再次重置才可以开锁 ),park就相当于是设置一扇未知密码的门,现在这个门锁 就相当于是一个凭证 permit
举个例子:
例子1
先执行 unpark操作后执行park操作 为什么线程不会阻塞,线程A,线程B执行的时候,先调用 unpark操作 相当于是把门锁的密码给重置了,那么 我park操作的时候,不管怎么样都可以通过了
public class LockSupportTest {
public static void main(String[] args) {
Thread aa = new Thread(() -> {
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
try {
System.out.println(Thread.currentThread().getName() + "开始执行, " + System.currentTimeMillis());
// 如果先执行的 unpark操作,后执行park操作,这一行就相当于是没有执行一样, 可以根据时间来验证
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " park后 开始执行," + System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}
}, "AA");
aa.start();
Thread bb = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "执行通知操作");
LockSupport.unpark(aa);
} catch (Exception e) {
e.printStackTrace();
}
}, "BB");
bb.start();
}
}
例子2:连续执行2次park操作,2次unpark操作
A线程开始执行,发现门锁的密码没有被重置,就只能在外面等着
B线程开始执行,首先执行了一次 unpark操作,把门的密码重置了 还没等A线程通过呢;又执行了一次unpark操作又把这一扇门的unpark的密码重置了
A线程现在不管输入什么 都可以通过第一次的那个unpark了
但是问题来了 还有一扇门,没有人可以给我重置密码了,那么我就只能在外面等着了,直到有人给我重置密码,或者有人来把这扇门给我炸了( 程序终止等操作 )
public class LockSupportTest {
public static void main(String[] args) {
Thread aa = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "开始执行, " + System.currentTimeMillis());
// 如果先执行的 unpark操作,后执行park操作,这一行就相当于是没有执行一样, 可以根据时间来验证
LockSupport.park();
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " park后 开始执行," + System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}
}, "AA");
aa.start();
Thread bb = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "执行通知操作");
LockSupport.unpark(aa);
// try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
LockSupport.unpark(aa);
} catch (Exception e) {
e.printStackTrace();
}
}, "BB");
bb.start();
}
}
BB线程不等待时的运行结果,程序不会终止
BB线程停顿1s中的执行结果
例子3:执行2次park操作,2次unpark操作,在执行unpark操作的时候,在中间停顿1s钟
A线程开始执行,发现门锁的密码没有被重置,就只能在外面等着
B线程开始执行,首先执行了一次 unpark操作,把门的密码重置了,B线程停顿了1S中,那么A线程开始执行
A线程发现 诶第一扇门我顺利通过了,然后发现还有第二扇门,执行等待,B线程又开始执行
B线程停顿了1s种后,再一次调用unpark操作,把第二扇门的密码给重置了
A 美滋滋,第一扇门,第二扇门的密码都给我重置了,就可以顺利通过了
面试题
为什么可以先唤醒线程后阻塞线程
先唤醒线程相当于是发放了一个凭证,那么我调用park的时候 有这个凭证 就可以直接使用了 而不需要进入阻塞状态
为什么唤醒2次后 阻塞2次,但最终结果还是阻塞状态
这个还是要分情况讨论的,如果 unpark是2次连续执行,就会进入阻塞状态,如果中间停顿了1s,就不会进入阻塞状态
原因使用中的实例了来理解