Java 多线程 —— 死锁与锁的错误用法
引言
死鎖狀態(tài)的大致情況是:Thread_1在獲得A對(duì)象的鎖后,緊接著去請(qǐng)求B對(duì)象的鎖?,Thread_2在獲得了B對(duì)象的鎖后,緊接著又去請(qǐng)求A對(duì)象的鎖,如下圖:
?一、模擬一個(gè)死鎖
public class DeadLockDemo {static class A {public synchronized void saying() {System.out.println(Thread.currentThread().getName() + " A start...........");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}new B().saying();System.out.println(Thread.currentThread().getName() + " A end.............");}}static class B {public synchronized void saying() {System.out.println(Thread.currentThread().getName() + " B start...........");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}new A().saying();System.out.println(Thread.currentThread().getName() + " B end.............");}}public static void main(String[] args) {new Thread(() -> new A().saying(), "t1").start();new Thread(() -> new B().saying(), "t2").start();} }可以看到在線程 t1 調(diào)用A對(duì)象的saying互斥方法的時(shí)候,t1拿到了A對(duì)象的鎖,而如果想完成saying方法必須去請(qǐng)求B對(duì)象的鎖才可以執(zhí)行到B對(duì)象的saying互斥方法。線程 t2調(diào)用B對(duì)象的saying互斥方法的時(shí)候,t2拿到了B對(duì)象的鎖,而如果想完成saying方法必須去請(qǐng)求A對(duì)象的鎖才可以執(zhí)行到A對(duì)象的saying互斥方法。?
這就導(dǎo)致了死鎖的出現(xiàn),程序會(huì)陷入無(wú)休止的“死循環(huán)”中。
如果沒(méi)有2秒的睡眠時(shí)間,程序會(huì)很快因內(nèi)存溢出而癱瘓:
否則程序會(huì)不停的循環(huán)下去,直到崩潰。
二、synchronized 鎖的錯(cuò)誤用法
使用synchronized 并不簡(jiǎn)單,以下這些用法一定要在實(shí)際開發(fā)中注意避免。
2.1 synchronized 鎖定字符串對(duì)象
synchronized 可以給對(duì)象加鎖,但這些對(duì)象不應(yīng)該包括 String、Integer 這類共享對(duì)象。說(shuō)String、Integer是共享對(duì)象,是因?yàn)樵谀承┣闆r下,Java會(huì)共享一些數(shù)據(jù)來(lái)提高性能和節(jié)約內(nèi)存。
例如,一個(gè)字符串 "Hello",如果以此對(duì)象為鎖定目標(biāo),那么就可能在非常不恰當(dāng)?shù)奈恢迷斐删€程阻塞或死鎖:
public class T {String s1 = "Hello";String s2 = "Hello";void m1() {synchronized (s1) {}}void m2() {synchronized (s2) {}} }如上代碼所示,s1 和 s2 雖然聲明了兩個(gè)變量,但實(shí)際上,"Hello" 字符串是共享的,因此鎖也是一份,如果你不希望造成莫名其妙的線程阻塞,一定要記得synchronized 不要加在 String、Integer 這類對(duì)象上。
2.2 鎖對(duì)象的引用被重新賦值
理解這個(gè)問(wèn)題需要清楚synchronized加鎖的目標(biāo)對(duì)象是什么,究竟是棧內(nèi)存中的引用?還是堆內(nèi)存中的對(duì)象數(shù)據(jù)?
我們保證同步的目的是有序的執(zhí)行堆中的數(shù)據(jù),所以很明顯,synchronized 鎖定的應(yīng)該是堆內(nèi)存中實(shí)際的對(duì)象,而不是棧中的引用。
那么如果引用被重新賦值,那么整個(gè)并發(fā)程序可能造成更加難以排查的問(wèn)題:
public class T_ChangeLock {private Object lock = new Object();public void doSync() {synchronized (lock) {while (true) {try {TimeUnit.SECONDS.sleep(1);// 打印當(dāng)前線程System.out.println(Thread.currentThread().getName());} catch (InterruptedException e) {}}}}public static void main(String[] args) throws InterruptedException {T_ChangeLock t = new T_ChangeLock();new Thread(() -> t.doSync(), "t1").start();// 啟動(dòng)第一個(gè)線程TimeUnit.SECONDS.sleep(1);// 鎖對(duì)象改變t.lock = new Object();// 想一想,t2 是否可以被成功阻塞?new Thread(() -> t.doSync(), "t2").start();} }doSync 是個(gè)同步方法,方法內(nèi)死循環(huán)輸出當(dāng)前線程ID,t1首先搶到 doSync 的執(zhí)行權(quán)(即搶到 lock 鎖對(duì)象),不出意外的話,其他線程都將無(wú)法執(zhí)行 doSync 方法,然而,在執(zhí)行了 lock = new Object() 方法后,奇怪的事情發(fā)生了:?
所以,為了不讓你的同步邏輯失效,請(qǐng)謹(jǐn)慎處理鎖對(duì)象的引用。
?
?
總結(jié)
以上是生活随笔為你收集整理的Java 多线程 —— 死锁与锁的错误用法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Java 8中获取参数名称
- 下一篇: String StringBuilder