死锁和线程通讯
1.死锁
1.1死锁定义
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
也就是两个线程拥有锁的情况下,又在尝试获取对方锁,从而造成程序一直阻塞的情况。
代码示例:
import java.util.concurrent.TimeUnit;
/**
* 死锁示例
*/
public class Thread18 {
public static void main(String[] args) {
Object lockA=new Object();
Object lockB=new Object();
Thread t1=new Thread(()->{
//1.占有一把锁(锁A)
synchronized (lockA){
System.out.println("线程1:获得锁A");
//休眠1s(让线程2有时间占有锁B)
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.获取线程2的锁B
synchronized (lockB){
System.out.println("线程1:获得锁B!");
}
}
});
t1.start();
Thread t2=new Thread(()->{
//1.占有一把锁(锁B)
synchronized (lockB){
System.out.println("线程2:获得锁B");
//休眠1s(让线程1有时间占有锁A)
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.获取线程1的锁A
synchronized (lockA){
System.out.println("线程2:获得锁A!");
}
}
});
t2.start();
}
}
1.2 死锁产生原因
形成死锁主要由以下 4 个因素造成的:
1.互斥条件:一个资源同一时间只能被一个线程占用
2.不可(被)剥夺条件:一个资源被占用之后,如果不是拥有资源的线程释放,那么其他线程不能得到此资源。
3.请求并持有条件:当一个线程拥有了某个资源之后,还不满足,又在请求其他资源。
4.环路等待条件:多个线程在请求资源的情况下,形成了环路链
以上四个条件(因素)是导致死锁的必要条件,要形成死锁,以上四个条件缺一不可
1.3 解决死锁:
打破形成死锁的一个或多个条件即可,其中可以被修改的条件只有两个:请求并持有条件和环路等待条件。
破坏请求并持有条件
import java.util.concurrent.TimeUnit;
/**
* 解决死锁方案:破坏请求并持有条件
*/
public class UnDeadLock {
public static void main(String[] args) {
Object lockA=new Object();
Object lockB=new Object();
Thread t1=new Thread(()->{
synchronized (lockA){
System.out.println("线程1:得到锁A");
//业务代码.....
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//
// synchronized (lockB){
// System.out.println("线程1:得到锁B");
// //业务代码.....
// System.out.println("线程1:释放锁B");
// }
System.out.println("线程1:释放锁A");
}
},"线程1");
t1.start();
Thread t2=new Thread(()->{
synchronized (lockB){
System.out.println("线程1:得到锁B");
//业务代码.....
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//
// synchronized (lockA){
// System.out.println("线程1:得到锁A");
// //业务代码.....
// System.out.println("线程1:释放锁A");
//}
System.out.println("线程1:释放锁B");
}
},"线程2");
t2.start();
}
}
环路等待条件的破坏:
形成环路等待条件:
环路等待条件的破坏:(使用顺序锁解决死锁问题)
import java.util.concurrent.TimeUnit;
/**
* 解决死锁方案:破坏环路等待条件
*/
public class UnDeadLock2 {
public static void main(String[] args) {
Object lockA=new Object();
Object lockB=new Object();
Thread t1=new Thread(()->{
synchronized (lockA){
System.out.println("线程1:得到锁A");
//业务代码.....
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println("线程1:得到锁B");
//业务代码.....
System.out.println("线程1:释放锁B");
}
System.out.println("线程1:释放锁A");
}
},"线程1");
t1.start();
Thread t2=new Thread(()->{
synchronized (lockA){
System.out.println("线程2:得到锁A");
//业务代码.....
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//
synchronized (lockB){
System.out.println("线程2:得到锁B");
//业务代码.....
System.out.println("线程2:释放锁B");
}
System.out.println("线程2:释放锁A");
}
},"线程2");
t2.start();
}
}
2.线程通讯
由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.
2.1 方法介绍
完成这个协调工作(线程通讯),主要涉及到三个方法:(对象级别,都是Object的内置方法)
wait() / wait(long timeout):让当前线程进入等待(休眠)状态。
notify():唤醒当前对象上一个休眠的线程(随机)。
notifyAll():唤醒所有处于休眠状态下的线程。
注意这三个方法都需要配合 synchronized 一起使用。
wait 使用
wait 执行流程:
1.使当前执行代码的线程进行等待. (把线程放到等待队列中)
2.释放当前的锁
3.满足一定条件时被唤醒, 重新尝试获取这个锁.
wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
wait 结束等待的条件:
1.其他线程调用该对象的 notify 方法.
2.wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
3.其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
import com.sun.deploy.uitoolkit.impl.fx.FXPluginToolkit;
/**
* wait使用
*/
public class WaitDemo {
public static void main(String[] args) {
Object lock=new Object();
Thread t1=new Thread(()->{
System.out.println("线程1开始执行");
try {
synchronized (lock) {
System.out.println("线程1调用wait方法....");
//无限等待状态
lock.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1完成执行");
},"线程1");
t1.start();
}
}
notify 使用
notify 方法是唤醒等待的线程:
1.方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
2.如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
3.在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
import com.sun.deploy.uitoolkit.impl.fx.FXPluginToolkit;
/**
* wait使用
*/
public class WaitDemo2 {
public static void main(String[] args) {
Object lock=new Object();
Object lock2=new Object();
Thread t1=new Thread(()->{
System.out.println("线程1开始执行");
try {
synchronized (lock) {
System.out.println("线程1调用wait方法....");
//无限等待状态
lock.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1完成执行");
},"线程1");
Thread t2=new Thread(()->{
System.out.println("线程2开始执行");
try {
synchronized (lock2) {
System.out.println("线程2调用wait方法....");
//无限等待状态
lock2.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2完成执行");
},"线程2");
Thread t3=new Thread(()->{
System.out.println("线程3开始执行");
try {
synchronized (lock) {
System.out.println("线程3调用wait方法....");
//无限等待状态
lock.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程3完成执行");
},"线程3");
t1.start();
t2.start();
t3.start();
//唤醒lock对象上休眠的线程的(随即唤醒一个)
Thread t4=new Thread(()->{
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程4开始执行");
synchronized (lock) {
//发出唤醒通知
lock.notify();
System.out.println("线程4执行了唤醒操作");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程4:synchronized执行完了");
}
},"线程4");
t4.start();
}
}
notify调用之后并不是里面唤醒线程开始执行,而是等待notify中的synchronized执行完(锁释放)之后才能唤醒起来开始执行
notiyAll 使用
notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程。
和notiy使用一样
2.2 注意事项
- wait /notiy/notiyAII使用前必须配合synchronized一起执行。
- wait /notiy/notiyAII 进行 synchronized 加锁,一定要使用同一个对象进行加锁,不然会报错。
- 当调用了notiy/notiyAII 之后,程序并不会立即恢复执行,而是尝试获取锁,只有得到锁之后才会继续执行
- notifyAll 并不是唤醒所有 wait 的对象,而是唤醒当前对象所有 wait 的对象。
wait()VSwait(long timeout)
不同点:
1.wait(long timeout):当线程超过了设置的时间之后,自动回复执行;wait()无限等待状态
2.使用无参的wait方法,线程会进入WAITING状态;使用有参的wait方法,线程会进入TIMED_WAITING状态
共同点:
1.无论是有参的wait方法或者是无参的wait方法,他都可以使当前线程进入休眠状态
2.无论是有参的wait方法或无参的wait方法,都可以使用notify/notifyAII进行唤醒
wait(0)VS sleep(0)
1.wait(0)无限期等待下去;sleep(0): 相对于Thread.yeild(),让出CPU执行权 ,重新调度,但是sleep(0)会继续执行
wait和sleep在有锁的情况下锁的处理情况是完全不同的:
wait方法(不管是有参的还是无参的)执行时都会释放锁,而sleep方法不会释放锁
wait和sleep区别(相同点&不同点)
相同点:
1.都可以让线程休眠;
2.都可以响应 interrupt(中断) 的请求。
不同点:
1.wait 必须在 synchronized 中使用,而 sleep 却不用;
2.sleep 是 Thread 的方法,而 wait 是 Object 的方法;
3.sleep 不释放锁,wait 释放锁;
4.sleep必须传递一个数值类型的参数,而wait可以不传参
5.sleep 和 wait 产生的线程状态是不同的,sleep 是 TIMED_WAITING 状态,而 wait 是 WAITING 状态。
6.一般情况下sleep只能等待超过时间之后再恢复运行;而wait可以接收notify/notifyAII之后就需执行
3.线程休眠和指定唤醒:LockSupport
使用 LockSupport 也可以使线程休眠和唤醒,它包含两个主要的方法:
LockSupport.park():休眠当前线程。
LockSupport.unpark(线程对象):唤醒某一个指定的线程。
3.1LockSupport 扩展
LockSupport.parkUntil(long) 等待最大时间是一个固定时间。
4.LockSupport VS wait
- LockSupport 使用时不需要加锁,而 wait 需要;
- LockSupport 不会抛出 Interrupt 的异常,而 wait 会;
- LockSupport 可以指定一个线程进行唤醒,而 wait 和 notify 不行。