文章目录
- 一、死锁
- (1)说明
- (2)案例
- 1、案例1
- 2、案例2
- 3、案例3
- (3)诱发死锁的原因及解决方案
- 1、诱发死锁的原因
- 2、避免死锁
- 二、JDK5.0新特性:Lock(锁)
- (1)介绍
- (2)案例
- (3)synchronized与Lock的对比
一、死锁
(1)说明
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
比如线程1拿着同步监视器(锁),它拿着锁1等着锁2;但是线程2拿着锁2等着锁1。两个线程僵持着,互相等待,这就构成了死锁。
我们编写程序时,要避免出现死锁。
一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
(2)案例
1、案例1
StringBuilder
:跟字符串相关的类(后边说,这里看成字符串就可以了,里面没有任何数据)
用它造两个对象:
public class DeadLockTest {public static void main(String[] args) {StringBuilder s1=new StringBuilder();StringBuilder s2=new StringBuilder();}
}
然后new一个线程:
new Thread(){@Overridepublic void run() {}
}.start();
线程里面调用了run方法,把s1当作同步监视器(锁),然后用s1调用append
方法(用于添加),添加一个“a”,如下:
new Thread(){@Overridepublic void run() {synchronized (s1){s1.append("a"); //理解为空字符串" "+"a"}}
}.start();
然后s2也添加一个“1”,如下:
new Thread(){@Overridepublic void run() {synchronized (s1){s1.append("a");s2.append("1");}}
}.start();
将s2当作一个锁,给s1添加一个“b”,给s2添加一个“2”。如下:
new Thread(){@Overridepublic void run() {synchronized (s1){s1.append("a");s2.append("1");synchronized (s2){s1.append("b");s2.append("2");System.out.println(s1);System.out.println(s2);}}}
}.start();
然后在这里sleep
睡一下,便于演示死锁的问题:
new Thread(){@Overridepublic void run() {synchronized (s1){s1.append("a");s2.append("1");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s2){s1.append("b");s2.append("2");System.out.println(s1);System.out.println(s2);}}}
}.start();
说一下大致过程:
线程1进入run,获得锁s1,然后稍微sleep一会,醒来之后再拿着锁s2,执行后面的操作,执行结束后打印s1与s2。
现在我们再写一个线程,原理与上面的类似,只不过这个线程2先获得锁s2,再获得锁s1,如下:
//线程2
new Thread(){@Overridepublic void run() {synchronized (s2){s1.append("c");s2.append("3");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s1){s1.append("d");s2.append("4");System.out.println(s1);System.out.println(s2);}}}
}.start();
若此时没有sleep,如下:
public class DeadLockTest {public static void main(String[] args) {StringBuilder s1=new StringBuilder();StringBuilder s2=new StringBuilder();//线程1new Thread(){@Overridepublic void run() {synchronized (s1){s1.append("a");s2.append("1");synchronized (s2){s1.append("b");s2.append("2");System.out.println(s1);System.out.println(s2);}}}}.start();//线程2new Thread(){@Overridepublic void run() {synchronized (s2){s1.append("c");s2.append("3");synchronized (s1){s1.append("d");s2.append("4");System.out.println(s1);System.out.println(s2);}}}}.start();}
}
运行如下:
可以发现并没有出现死锁,这就是问题所在,能执行成功不意味着这个程序没有问题。
其实它是存在问题的可能性的。
从输出结果上来看,先执行的是线程1,s1获得a,s2获得1,然后s1获得b,s2获得2。最终s1得到ab,s2得到12。
然后线程2执行,在原有基础上,又添加了数据,最终s1得到abcd,s2得到1234。
也就是说,线程1执行结束,线程2才 开始执行。
当线程1执行结束的时候,s1与s2两个同步监视器都被释放了,所以线程2执行的时候才能顺利拿到s1与s2。
如果先执行的是第2个线程,再执行第1个线程,结果就是:cd,34,cdab,3412。
现在我们加上sleep,让死锁的概率高一点,注意这里只是让它出现的概率变高了,并不是从无到有(只是数量上的,并不是质变)。
🌱代码
package yuyi04.lock;/*** ClassName: DeadLockTest* Package: yuyi04.lock* Description:** @Author 雨翼轻尘* @Create 2024/2/1 0001 15:18*/
public class DeadLockTest {public static void main(String[] args) {StringBuilder s1=new StringBuilder();StringBuilder s2=new StringBuilder();//线程1new Thread(){@Overridepublic void run() {synchronized (s1){s1.append("a");s2.append("1");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s2){s1.append("b");s2.append("2");System.out.println(s1);System.out.println(s2);}}}}.start();//线程2new Thread(){@Overridepublic void run() {synchronized (s2){s1.append("c");s2.append("3");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s1){s1.append("d");s2.append("4");System.out.println(s1);System.out.println(s2);}}}}.start();}
}
🍺输出
可以看到,出现了死锁
。
线程1拿着同步监视器s1,在sleep的时候,线程2执行。
然后线程2拿着同步监视器s2往后执行,也碰到了sleep。
线程1醒来之后,去拿同步监视器s2,但是s2在线程2手里。大家就僵持住了。
这就构成了死锁
,如下:
2、案例2
案例1很容易看出来会出现死锁
,现在再来看一个例子:
🌱代码
package yuyi04.lock;/*** ClassName: DeadLock* Package: yuyi04.lock* Description:** @Author 雨翼轻尘* @Create 2024/2/1 0001 18:01*/class A {public synchronized void foo(B b) {System.out.println("当前线程名: " + Thread.currentThread().getName()+ " 进入了A实例的foo方法"); // ①try {Thread.sleep(200);} catch (InterruptedException ex) {ex.printStackTrace();}System.out.println("当前线程名: " + Thread.currentThread().getName()+ " 企图调用B实例的last方法"); // ③b.last();}public synchronized void last() {System.out.println("进入了A类的last方法内部");}
}
class B {public synchronized void bar(A a) {System.out.println("当前线程名: " + Thread.currentThread().getName()+ " 进入了B实例的bar方法"); // ②try {Thread.sleep(200);} catch (InterruptedException ex) {ex.printStackTrace();}System.out.println("当前线程名: " + Thread.currentThread().getName()+ " 企图调用A实例的last方法"); // ④a.last();}public synchronized void last() {System.out.println("进入了B类的last方法内部");}
}public class DeadLock implements Runnable { //用实现Runnable接口的方式创建多线程A a = new A();B b = new B();public void init() {Thread.currentThread().setName("主线程");// 调用a对象的foo方法a.foo(b);System.out.println("进入了主线程之后");}public void run() {Thread.currentThread().setName("副线程");// 调用b对象的bar方法b.bar(a);System.out.println("进入了副线程之后");}public static void main(String[] args) {DeadLock dl = new DeadLock();new Thread(dl).start(); //分线程创建,并调用start()方法,即调用run()方法dl.init(); //主线程调用init()方法}
}
🍰分析
可以看到,类A与类B里面定义了两个方法,都加了synchronized
,就是同步方法,同步监视器就是当前类的对象。
如下:
然后在实现类里面声明了a与b两个成员变量,还有两个方法,如下:
在main方法种,创建了当前实现类DeadLock的对象dl,然后将dl当作形参传入Thread()
中,用实现Runnable接口的方式创建一个线程,调用start()
方法,于是这个分线程就去执行run()
方法。
然后又使用dl调用init()
方法,这就是主线程的代码。就将dl当作普通的对象,它调用init()
方法。
现在就是,主线程调用init()方法,分线程调用run()方法。
分线程调用run
方法,主要执行b.bar(a);
:
public void run() {Thread.currentThread().setName("副线程");// 调用b对象的bar方法b.bar(a);System.out.println("进入了副线程之后");
}
主线程调用init
方法,主要执行a.foo(b);
:
public void init() {Thread.currentThread().setName("主线程");// 调用a对象的foo方法a.foo(b);System.out.println("进入了主线程之后");
}
可以得出主线程与分线程分别调用的方法如下:
分线程调用bar方法,需要拿着A类的一个对象a作为锁,这个锁就是B的对象。
然后执行a.last()
,如下:
注意锁是当前对象this,用b调用bar(a),this就是b,也就是拿着锁b进入了同步方法bar中,顺便将参数a带进来了。
在执行最后有一个a.last(),就是用对象a调用last()方法,last
方法也是一个同步方法,a
是传进来的A的对象,相当于又需要握着a这个同步监视器。如下:
也就是说,分线程握着B的对象,还要握A的对象。
主线程正好相反,握着A的对象a又需要b的同步监视器。
🍺输出结果
可以看见输出结果在这里出现了死锁
,僵持不动了。
3、案例3
public class TestDeadLock {public static void main(String[] args) {Object g = new Object();Object m = new Object();Owner s = new Owner(g,m);Customer c = new Customer(g,m);new Thread(s).start();new Thread(c).start();}
}
class Owner implements Runnable{private Object goods;private Object money;public Owner(Object goods, Object money) {super();this.goods = goods;this.money = money;}@Overridepublic void run() {synchronized (goods) {System.out.println("先给钱");synchronized (money) {System.out.println("发货");}}}
}
class Customer implements Runnable{private Object goods;private Object money;public Customer(Object goods, Object money) {super();this.goods = goods;this.money = money;}@Overridepublic void run() {synchronized (money) {System.out.println("先发货");synchronized (goods) {System.out.println("再给钱");}}}
}
(3)诱发死锁的原因及解决方案
1、诱发死锁的原因
①互斥条件(一定会出现)
比如线程1握着同步监视器(锁),另外一个线程就无法获得这个锁。(必然的,这是同步机制,加同步的目的就是为了某个线程能获得锁而另一个线程握不住)
②占用且等待
比如线程1握着同步监视器(锁),然后又等待另外一个锁。另外一个线程又拿着这个锁。
③不可抢夺(或不可抢占)
某线程等待的时候不能将其他线程拥有的锁抢过来。
④循环等待
这种情况会一直僵持着,解不开。
以上4个条件,同时出现就会触发死锁。
2、避免死锁
死锁
一旦出现,基本很难人为干预,只能尽量规避。
可以考虑打破上面的任意一个诱发条件。
①针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
②针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
③针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源,让别人先来,这样其他的线程不需要等待,不会僵持了。
④针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题(按顺序获取,先获取序号小的,才能获取后边大的序号)。
二、JDK5.0新特性:Lock(锁)
(1)介绍
以前说的“同步机制”,其实就是synchronized
的使用方式。
除了使用synchronized同步机制
处理线程安全问题之外,还可以使用jdk5.0提供的Lock锁
的方式。
- 除了
synchronized
的方式,JDK5.0还提供了Lock锁
的方式(另外一种解决线程安全的方式)。 - JDK5.0的新增功能,保证线程的安全。
- 与采用
synchronized
相比,Lock可提供多种锁方案,更灵活、更强大。Lock通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。 java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。- 在实现线程安全的控制中,比较常用的是
ReentrantLock
,可以显式加锁、释放锁。ReentrantLock
类实现了 Lock 接口,它拥有与synchronized
相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
- Lock锁也称同步锁,加锁与释放锁方法,如下:
public void lock()
:加同步锁。public void unlock()
:释放同步锁。
- 代码结构
class A{//1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例private final ReentrantLock lock = new ReenTrantLock();public void m(){//2. 调动lock(),实现需共享的代码的锁定lock.lock();try{//保证线程安全的代码;}finally{//3. 调用unlock(),释放共享代码的锁定lock.unlock(); }}
}
注意:如果同步代码有异常,要将unlock()写入finally语句块。
【举例】
import java.util.concurrent.locks.ReentrantLock;class Window implements Runnable{int ticket = 100;//1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例private final ReentrantLock lock = new ReentrantLock();public void run(){while(true){try{//2. 调动lock(),实现需共享的代码的锁定lock.lock();if(ticket > 0){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(ticket--);}else{break;}}finally{//3. 调用unlock(),释放共享代码的锁定lock.unlock();}}}
}public class ThreadLock {public static void main(String[] args) {Window t = new Window();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();t2.start();}
}
(2)案例
【步骤】
步骤1:创建Lock的实例,需要确保多个线程共用同一个Lock实例。
需要考虑将此对象声明为static final
。
步骤2.:执行lock()
方法,锁定对共享资源的调用。
步骤3.:unlock()
的调用,释放对共享数据的锁定。
以下面代码(继承的方式)为例:
🌱代码
package yuyi04.lock;/*** ClassName: WindowTest2* Package: yuyi04.lock* Description:* 使用继承Thread类的方式,实现卖票* @Author 雨翼轻尘* @Create 2024/2/2 0002 9:54*/public class WindowTest2 {public static void main(String[] args) {//3.创建3个窗口 创建当前Thread的子类的对象Window w1=new Window();Window w2=new Window();Window w3=new Window();//命名w1.setName("窗口1");w2.setName("窗口2");w3.setName("窗口3");//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法w1.start();w2.start();w3.start();}}class Window extends Thread{ //卖票 1.创建一个继承于Thread类的子类//票static int ticket=100;//2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {while (true){if(ticket>0){ //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket); //最开始票号为100ticket--;}else{break;}}}
}
🍺输出
上面代码有线程安全问题。
现在我们用Lock
来解决线程安全问题。
“concurrent
”就是并发的意思,Java的并发编程其实就是说这个包里面的API。
现在我们就可以看到这个包里面的一个API,叫Lock
,它是一个接口,目前已知的实现类如下:
我们需要使用ReentrantLock
(可重入锁)这个实现类,接下来创建这个实现类的对象。
如下:
class Window extends Thread{ //卖票 1.创建一个继承于Thread类的子类ReentrantLock lock=new ReentrantLock();//...
}
①权限修饰
我们需要先考虑权限修饰。
首先不希望在外部可以被访问,因为这纯粹是为了保证线程安全的,所以加一个private
。
其次这个锁有要求,需要保证线程的安全,多个线程需要共用同一个lock,所以还要再加一个static
。
最后再加一个final
,保证给lock赋值之后不要再修改了,是唯一的不能改变的。
如下:
private static final ReentrantLock lock=new ReentrantLock();
Runnable接口一般不需要static的,一般是把这个接口的一个实例作为多个线程对象的形参,一般情况只会有一个接口的实例。
②用方法限制
这个锁定操作,不像同步代码块,有一个大括号,里面是需要被同步的代码。
lock比较灵活,它只需要用两个方法去限制,两个方法执行当中的代码就是需要被同步的代码。
针对案例,需要被同步的代码如下:(蓝色部分)
在这一段代码之前,调用一个方法lock()
;然后在代码执行结束,调用unlock()
方法。如下:
两个方法中间的代码就是之前我们放到同步代码块当中的代码。
🌱代码
package yuyi04.lock;import java.util.concurrent.locks.ReentrantLock;/*** ClassName: WindowTest2* Package: yuyi04.lock* Description:* 使用继承Thread类的方式,实现卖票* @Author 雨翼轻尘* @Create 2024/2/2 0002 9:54*/public class WindowTest2 {public static void main(String[] args) {//3.创建3个窗口 创建当前Thread的子类的对象Window w1=new Window();Window w2=new Window();Window w3=new Window();//命名w1.setName("窗口1");w2.setName("窗口2");w3.setName("窗口3");//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法w1.start();w2.start();w3.start();}}class Window extends Thread{ //卖票 1.创建一个继承于Thread类的子类//票static int ticket=100;private static final ReentrantLock lock=new ReentrantLock();//2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {while (true){lock.lock();if(ticket>0){ //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket); //最开始票号为100ticket--;}else{break;}lock.unlock();}}
}
🍺输出
可以看见线程安全了。
🗳️这里可能出现了没有解锁的场景:
如果死锁的话就执行不到unlock,虽然关闭了程序,但是如果这些资源是指向别的资源的话就无法释放了。
unlock()
方法一定要保证会被执行,可以考虑写入finally
中。
现在我们在这里加一个try
,将下面蓝色部分移入try里面:
然后再写一个finally
,将unlock
写入,确保它一定会被执行。如下:
🌱代码
package yuyi04.lock;import java.util.concurrent.locks.ReentrantLock;/*** ClassName: WindowTest2* Package: yuyi04.lock* Description:* 使用继承Thread类的方式,实现卖票* @Author 雨翼轻尘* @Create 2024/2/2 0002 9:54*/public class WindowTest2 {public static void main(String[] args) {//3.创建3个窗口 创建当前Thread的子类的对象Window w1=new Window();Window w2=new Window();Window w3=new Window();//命名w1.setName("窗口1");w2.setName("窗口2");w3.setName("窗口3");//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法w1.start();w2.start();w3.start();}}class Window extends Thread{ //卖票 1.创建一个继承于Thread类的子类//票static int ticket=100;//①创建Lock的实例(Lock是一个接口,现在用的是可重入锁ReentrantLock)// 需要确保多个线程共用同一个Lcok实例,需要考虑将此对象声明为static final//若没有加static,Window一共造了3个对象,相当于现在就有3把锁,每一个对象锁自己的,就不好使private static final ReentrantLock lock=new ReentrantLock(); //2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中@Overridepublic void run() {while (true){try {//②执行lock()方法,锁定对共享资源的调用lock.lock();if(ticket>0){ //如果票数大于0就可以售票try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//哪个窗口卖票了,票卖了多少System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket); //最开始票号为100ticket--;}else{break;}}finally{//③unlock()的调用,释放对共享数据的锁定,解锁之后其他线程就可以来操作了lock.unlock();}}}
}
🍺输出
可以看到,程序结束了,而且没有线程安全问题。如下:
(3)synchronized与Lock的对比
synchronized与Lock的对比
1、Lock
是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized
是隐式锁,出了作用域、遇到异常等自动解锁。
2、Lock
只有代码块锁,synchronized
有代码块锁和方法锁。
3、使用Lock
锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类),更体现面向对象。
4、(了解)Lock
锁可以对读不加锁,对写加锁,synchronized
不可以。
5、(了解)Lock
锁可以有多种获取锁的方式,可以从sleep
的线程中抢到锁,synchronized
不可以。
说明:
开发建议中处理线程安全问题优先使用顺序为:
Lock ----> 同步代码块 ----> 同步方法
【面试题】
🎲synchronized
同步的方式 与Lock
的对比 ?
synchronized
不管是同步代码块还是同步方法,都需要在结束一对{}
之后,释放对同步监视器的调用。Lock
是通过两个方法控制需要被同步的代码,更灵活一些。Lock
作为接口,提供了多种实现类(也就是很多锁),适合更多更复杂的场景,效率更高。
对于Lock,JUC里面再详细说,这里不做深入。