【JavaEE】了解synchronized
synchronized也称 加锁,当一个操作为非原子性操作时,可以通过 加锁 成为原子性操作。synchronized可以修饰代码块、实例方法以及静态方法。synchronized的重要特性:可重入锁。
关于死锁
1.一个线程针对一把锁,加锁两次,如果不是可重入锁那就会出现死锁现象。
2.两个线程针对两把锁,无论是否是可重入锁,都会出现死锁现象。
3.n个线程,m把锁,更容易出现死锁现象(经典问题:哲学家就餐问题)
1.一个线程一把锁
可重入性:一个线程针对一把锁,加锁两次不会出现死锁现象。
死锁:“车里锁了家门钥匙,家里锁了车钥匙”这个可以称为死锁,使得该锁无法解开的锁就称为死锁。如下例子:在还不知道synchronized有可重入性这个概念时,都会默认以下代码是死锁现象
public class Demo01 {public static void main(String[] args) {Object lock = new Object();Thread t1 = new Thread(()->{synchronized (lock){synchronized(lock){System.out.println("nihao");}}});t1.start();System.out.println("hahaha");}
}
synchronized 第一次加锁已经成功了,lock就属于被锁定的状态,第二次再加锁,就应该处于阻塞状态,只有等上一次lock锁被释放后才能加锁成功,但是在进行第二次加锁时就已经阻塞了,程序无法往后继续进行就会出现死锁的现象。这种情况只是在我们不知道synchronized的特性的情况下的想法,正是它有这个可重入性,就不会出现以上的死锁现象。
接着我们进一步分析synchronized是如何做到不会出现死锁的:
当synchronized第一次加锁成功后,又遇到了第二次加锁,此时的第一次加锁并没有释放,可重入性是通过计数器实现的,所以锁对象中不仅需要记录被谁使用还需要记使用次数。正如上面这个例子:当一次加锁成功后,lock这个锁对象中的计数器随其加一,第二次在加锁时计数器也所着加一,直到遇到一个右大括号,相当于解一次锁计数器就减一,这样就是可重入锁。为此锁对象真正释放锁必须是计数器为零时。
2.两个线程两把锁
已知 线程t1,线程t2,锁A,锁B,线程t1已经获得A、线程t2已经获得B,t1还想获得B,t2还想获得A,此时就会出现死锁现象。如下:
public static void main(String[] args) {Object A = new Object();Object B = new Object();Thread t1 = new Thread(()->{synchronized (A){System.out.println("t1->A");synchronized (B){System.out.println("t1->B");}}});Thread t2 = new Thread(()->{synchronized (B){System.out.println("t2->B");synchronized (A){System.out.println("t2->A");}}});t1.start();t2.start();}
以上代码不太严谨,还需要加强,可能会出现一个线程同时获得AB两把锁的情况,所以需要加一个sheep休眠。如下结果,两个线程就出现阻塞现象,两个线程僵持不下就阻塞进程。相反如果是线程t1获得A后又释放,再获得B,这是不会出现死锁现象的。
3.N个线程M把锁
哲学家就餐问题
在圆形餐桌上,每两个哲学家中间就一只筷子,每个哲学家只会作两件事,一件是思考(不拿筷子),另一件是吃面(只能拿左右两边的筷子),每个哲学家思考和吃面都是随机的,当左右边筷子少一个都得等两边哲学家吃完才能拿筷子(等价于阻塞等待),一般情况下是可以正常运行的,如果是极端情况下,就会出现已知等待线程,每个哲学家都拿左手边的筷子,并且都在等右手边的哲学家放下筷子,这时每个人都在等,就等同于出现死锁现象了。
死锁是一个很严重的bug,它会直接导致线程卡住,无法执行后面工作。那我们程序员就需要解决bug,接着就学习如何解决死锁问题。
解决死锁问题
死锁的成因
想要解决死锁问题,需要先了解死锁的成因:
1.互斥使用(锁的基本特性),当一个线程持有一把锁之后,另一个线程也想获得该锁,这个线程就会出现阻塞等待。
2.不可抢占(锁的基本特性),当锁已经被线程t1拿到之后,线程t2只能等待线程t1主动释放锁,而不能强行占有锁。
3.请求保持(代码结构),一个线程尝试获得多把锁(如:上面两个线程两把锁这个例子)。
4.循环等待/环路等待(代码结构),等待关系形成环(如:上面N个线程M把锁、家钥匙锁车里,家里锁车钥匙)。
出现死锁就需要满足以上四个条件,缺一不可,而第一第二都是锁的特性本身就满足条件,只要满足三和四就会出现死锁现象。
解决死锁
既然要解决死锁,那就让以上条件其中有一是不满足即可:
面对1和2,既是锁的特性,那就是不可修改的,我们只需要破坏3和4其中一个即可。
针对3:我们可以通过调整代码避免出现“锁嵌套”,比如:将嵌套关系改为并列关系;
针对4:如果一定需要嵌套关系,那可以约定加锁顺序,针对锁进行编号,比如:加多把锁时先加编号小的后加编号大的,针对所有线程都遵循这个约定。