三种线程的等待和唤醒的方式
-
Object类中的wait()方法让线程等待,使用Object类中的notify()方法唤醒线程
-
Condition接口中的await后signal方法实现线程的等待和唤醒
-
LockSupport类中的park等待和unpark唤醒
Object类中的wait()方法让线程等待,使用Object类中的notify()/notifyAll()方法唤醒线程
概念介绍
-
wait()、notify/notifyAll() 方法是Object的本地final方法
-
wait()使当前线程阻塞,前提是必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法
-
由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的
- 当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态
- 只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁
- notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁
-
wait() 需要被try catch包围,以便发生异常中断也可以使wait等待的线程唤醒
-
notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的
-
notify 和 notifyAll的区别
- notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。
代码实例
- 正确实例
- 代码
Object objectLock = new Object();
new Thread(() -> {
synchronized (objectLock) {
System.out.println(Thread.currentThread().getName() + "----> come");
try {
objectLock.wait();
} catch (Exception 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();
复制代码
- 输出结果(抛出异常)
- wait和notify去除sync代码块
- 代码
Object objectLock = new Object();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "----> come");
try {
objectLock.wait();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "----> 被唤醒");
}, "a").start();
new Thread(() -> {
objectLock.notify();
System.out.println(Thread.currentThread().getName() + "----> 通知");
}, "b").start();
复制代码
- 输出结果
3.** 结论: object类中的wait/notify/notifyAll用于线程等待和唤醒的方法.都必须在sync内部执行(必须使用synchronized关键字)**
- 将notify放在wait前面
- 代码
Object objectLock = new Object();
new Thread(() -> {
//当前线程先睡眠三秒
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (objectLock) {
System.out.println(Thread.currentThread().getName() + "----> come");
try {
objectLock.wait();
} catch (Exception 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();
复制代码
- 输出结果(程序无法终止)
3.** 结论:notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的**
Condition接口中的await后signal方法实现线程的等待和唤醒
概念介绍
- Condition接口包含了多种await方式和两个通知方法
- ConditionObject实现了Condition接口,是AbstractQueuedSynchronizer的内部类
- Reentrantlock的newCondition方法返回与某个lock实例相关的Condition对象
- 执行流程:调用await的线程都会进入一个Condition队列。调用signal的线程每一次都会从firstWaiter开始找出未取消的Condition Node放到release队列里,然后调用signal的线程在await或者unlock的时候执行release方法才有机会将其解除阻塞
代码实例
- 正确代码
- 代码
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();
System.out.println(Thread.currentThread().getName() + "----------> 被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "A").start();
new Thread(() -> {
lock.lock();
try {
condition.signal();
System.out.println(Thread.currentThread().getName() + "----------> 通知");
} catch (IllegalMonitorStateException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "B").start();
}
复制代码
- 输出结果
- 去除await和signal的锁
- 代码
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();
System.out.println(Thread.currentThread().getName() + "----------> 被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//lock.unlock();
}
}, "A").start();
new Thread(() -> {
//lock.lock();
try {
condition.signal();
System.out.println(Thread.currentThread().getName() + "----------> 通知");
} catch (IllegalMonitorStateException e) {
e.printStackTrace();
} finally {
//lock.unlock();
}
}, "B").start();
}
复制代码
- 输出结果(抛出异常)
- 结论:使用await和signal,线程必须要先获得并持有锁,必须在锁块(sync或lock)中
- signal放在await前面
1.代码
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
}
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "----------> come in");
condition.await();
System.out.println(Thread.currentThread().getName() + "----------> 被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "A").start();
new Thread(() -> {
lock.lock();
try {
condition.signal();
System.out.println(Thread.currentThread().getName() + "----------> 通知");
} catch (IllegalMonitorStateException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "B").start();
}
复制代码
- 输出结果(程序无法终止)
3.结论:必须要先等待后唤醒(即先await后signal),线程才能够被唤醒
LockSupport类中的park等待和unpark唤醒
概念介绍
- LockSupport是用来创建锁和其他同步类的基本线程阻塞源语
- LockSupport类使用了一种名为permit(许可)的概念来做到阻塞和唤醒线程的功能,每一个线程都有一个许可(permit),permit只有两个值1和零,默认是零
- 阻塞(park()/park(Object blocker)):阻塞当前线程/阻塞传入的具体线程 —-(permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回)
- 唤醒(unpark(Thread thread)):唤醒处于阻塞的指定线程 —-(调用unaprk(thread)方法后,就会将thread线程的许可permit设置为1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回)
代码实例
- 无锁块代码
- 代码
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();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
}
Thread b = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "----------> 通知");
LockSupport.unpark(a);
}, "b");
b.start();
}
复制代码
- 输出结果(正常运行)
3.结论:park()和unpark()无需强制在锁中执行
- 先执行unpark后park
- 代码
public static void main(String[] args) {
Thread a = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "----------> come in");
// 被阻塞...等待通知等待放行,它要通过需要许可证
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "----------> 被唤醒");
}, "a");
a.start();
Thread b = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "----------> 通知");
LockSupport.unpark(a);
}, "b");
b.start();
}
复制代码
- 输出结果(正常运行)
3.** 结论:LockSupport无需先等待后唤醒,先唤醒后等待同样支持**
LockSupport解析
- LockSupport是用来创建锁和其他同步类的基本线程阻塞源语
- LockSupport是一个线程阻塞的工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native方法
- 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也不会积累凭证
- 形象的理解
- 线程阻塞需要消耗凭证(permit),这个凭证最多只有一个
- 当调用park方法时
- 如果有凭证,则会直接消耗掉这个凭证然后正常退出
- 如果无凭证,就必须阻塞等待凭证可用
- 而unpark则相反,它会增加一个凭证,但凭证最多只能有一个,累加无效
LockSupport面试题
- 为什么可以先唤醒线程后阻塞线程
- 因为unpark获得了一个凭证,之后在调用park方法,就可以名正言顺的凭证消费,故不会阻塞
- 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
- 因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark的效果一样,只会增加一个凭证。而调用两次park却需要消费两个凭证,证不够不能放行
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END