java併發安全
本次內容主要執行緒的安全性、死鎖相關知識點。
1、什麼是執行緒安全性
1.1 執行緒安全定義
前面使用8個篇幅講到了Java併發程式設計的知識,那麼我們有沒有想過什麼是執行緒的安全性?在《Java併發程式設計實戰》中定義如下:當多個執行緒訪問某個類時,不管執行時環境採用何種排程方式或者這些執行緒將如何交替執行,並且在呼叫程式碼中不需要任何額外的同步或者協同,這個類都能表現出正確的行為,那麼就稱這個類是執行緒安全的。
1.2 無狀態類
沒有任何成員變數的類,就叫無狀態類,這種類一定是執行緒安全的。但是有一種情況是,這個類方法的引數中用到了物件,看下面的程式碼:
public class StatelessClass { public void test(User user) { //do business } }
此時這個類還是執行緒安全的嗎?那肯定也是,為什麼呢?因為多執行緒下的使用,固然user這個物件的例項會不正常,但是對於StatelessClass這個類的物件例項來說,它並不持有User的物件例項,它自己並不會有問題,有問題的是User這個類,而非StatelessClass本身。
1.2 volatile
並不能保證類的執行緒安全性,只能保證類的可見性,最適合一個執行緒寫,多個執行緒讀的情景。
1.3 鎖和CAS
我們最常使用的保證執行緒安全的手段,使用synchronized關鍵字,使用顯式鎖,使用各種原子變數,修改資料時使用CAS機制等等。
1.4 ThreadLocal
ThreadLocal是實現執行緒封閉的最好方法。關於ThreadLocal如何保證執行緒的安全性,請閱讀《java執行緒間的共享》,裡面有詳細的介紹。
1.5 安全的釋出
1)類中持有的成員變數,如果是基本型別,釋出出去,並沒有關係,因為釋出出去的其實是這個變數的一個副本。看下面的程式碼:
public class SafePublish { private int number; public SafePublish() { number = 2; } public int getNumber() { return number; } public static void main(String[] args) { SafePublish safePublish = new SafePublish(); int result = safePublish.getNumber(); System.out.println("before modify, result = " + result); result = 3; System.out.println("before modify, result =" + result); System.out.println("getNumber() = " + safePublish.getNumber()); } }
從程式輸出可以看到,number的值並沒被改變,因為result只是一個副本,這樣的成員變數釋出出去是安全的。
2)如果類中持有的成員變數是物件的引用,如果這個成員物件不是執行緒安全的,通過get等方法釋出出去,會造成這個成員物件本身持有的資料在多執行緒下不正確的修改,從而造成整個類執行緒不安全的問題。看下面程式碼:
public class UnSafePublish { private final User user = new User(); public User getUser() { return user; } public static void main(String[] args) { UnSafePublish unSafePublish = new UnSafePublish(); User user = unSafePublish.getUser(); System.out.println("before modify, user = " + unSafePublish.getUser()); user.setAge(88); System.out.println("after modify, user = " + unSafePublish.getUser()); } static class User { private int age; public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "UserVo[" + "age=" + age + ']'; } } }
從程式輸出可以看到,user物件的內容發生了改變,如果多個執行緒同時操作,user物件在堆中的資料是不可預知的。
那麼這個問題應該怎麼處理呢?我們在釋出這物件出去的時候,就應該用執行緒安全的方式包裝這個物件。對於我們自己使用或者宣告的類,JDK自然沒有提供這種包裝類的辦法,但是我們可以仿造這種模式或者委託給執行緒安全的類,當然,對這種通過get等方法釋出出去的物件,最根本的解決辦法還是應該在實現上就考慮到執行緒安全問題。對上面的程式碼進行改造:
public class SafePublicUser { private final User user; public User getUser() { return user; } public SafePublicUser(User user) { this.user = new SynUser(user); } /** * 執行緒安全的類,將內部成員物件進行執行緒安全包裝 */ static class SynUser extends User { private final User user; private final Object lock = new Object(); public SynUser(User user) { this.user = user; } @Override public int getAge() { synchronized (lock) { return user.getAge(); } } @Override public void setAge(int age) { synchronized (lock) { user.setAge(age); } } } static class User { private int age; public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "UserVo[" + "age=" + age + ']'; } } }
2、死鎖
2.1 死鎖定義
死鎖的發生必須具備以下四個必要條件:
1)互斥條件:指程序對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個程序佔用。如果此時還有其它程序請求資源,則請求者只能等待,直至佔有資源的程序用畢釋放。
2)請求和保持條件:指程序已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它程序佔有,此時請求程序阻塞,但又對自己已獲得的其它資源保持不放。
3)不剝奪條件:指程序已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
4)環路等待條件:指在發生死鎖時,必然存在一個程序——資源的環形鏈,即程序集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1佔用的資源;P1正在等待P2佔用的資源,……,Pn正在等待已被P0佔用的資源。
舉個例子來說明:
老王和老宋去大保健,老王搶到了1號技師,擅長頭部按摩,老宋搶到了2號技師,擅長洗腳。但是老王和老宋都想同時洗腳和頭部按摩,於是互不相讓,老王搶到了1號,還想要2號,老宋搶到了2號,還想要1號。在洗腳和頭部按摩這個事情上老王和老宋就產生了死鎖,怎麼樣可以解決這個問題呢?
方案1:老闆瞭解到情況,派3號技師過來,3號技師擅長頭部按摩,老王只有一個頭,所以3號只能給老宋服務,這個時候死鎖就被打破。
方案2:大保健會所的老闆比較霸道,規定了只能先頭部按摩,再洗腳。這種情況下,老王和老宋誰先搶到1號,誰就先享受,另一個沒搶到的就等著,這種情況也不會產生死鎖。
對死鎖做一個通俗易懂的總結:
死鎖是必然發生在多個操作者(M>=2)情況下,爭奪多個資源(N>=2,且M>=N)才會發生這種情況。很明顯,單執行緒不會有死鎖,只有老王一個去,1號2號都歸他,沒人跟他搶。單資源呢?只有1號,老王和老宋也只會產生激烈競爭,打得不可開交,誰搶到就是誰的,但不會產生死鎖。同時,死鎖還有兩個重要的條件,爭奪資源的順序不對,如果爭奪資源的順序是一樣的,也不會產生死鎖,另一個條件就是,爭奪者拿到資源後不放手。
2.2 死鎖的危害
一旦程式中出現了死鎖,危害是非常致命的,大致有以下幾個原因:
1)執行緒不工作了,但是整個程式還是活著的。
2)沒有任何的異常資訊可以供我們檢查。
3)程式發生了發生了死鎖,是沒有任何的辦法恢復的,只能重啟程式,對生產平臺的程式來說,這是個很嚴重的問題。
2.3 死鎖的例子
上面講了那麼多關於死鎖的概念,現在直接擼一段死鎖程式碼看看。
public class DeadLockDemo { private static Object No1 = new Object(); private static Object No2 = new Object(); /*** * 老王搶到了1號,還想要2號 * @throws InterruptedException */ private static void laowang() throws InterruptedException { String threadName = Thread.currentThread().getName(); synchronized (No1) { System.out.println(threadName + " get NO1"); Thread.sleep(100); synchronized (No2) { System.out.println(threadName + " get NO2"); } } } /*** * 老宋搶到了2號,還想要1號 * @throws InterruptedException */ private static void laosong() throws InterruptedException { String threadName = Thread.currentThread().getName(); synchronized (No2) { System.out.println(threadName + " get NO2"); Thread.sleep(100); synchronized (No1) { System.out.println(threadName + " get NO1"); } } } private static class Laowang extends Thread { private String name; public Laowang(String name) { this.name = name; } @Override public void run() { Thread.currentThread().setName(name); try { laowang(); } catch (Exception e) { e.printStackTrace(); } } } private static class Laosong extends Thread { private String name; public Laosong(String name) { this.name = name; } @Override public void run() { Thread.currentThread().setName(name); try { laosong(); } catch (Exception e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { Laosong laosong = new Laosong("laosong"); laosong.start(); Laowang laowang = new Laowang("laowang"); laowang.start(); Thread.sleep(10); } }
程式輸出可以看到,老宋搶到了2號,老王搶到了1號,因為產生了死鎖,程式沒有結束,但是並沒有往下執行。
2.4 死鎖的定位
通過JDK的jps檢視應用的id,再使用jstack檢視應用持有鎖的情況。
可以看到"laowang"這個執行緒持有了<0x000000076b393b78>鎖,還想獲得<0x000000076b393b88>鎖;"laosong"這個執行緒持有了<0x000000076b393b88>鎖,還想獲取<0x000000076b393b78>鎖。
2.5 死鎖的解決方案
1)保證拿鎖的順序一致,內部通過順序比較,確定拿鎖的順序。
2)採用嘗試拿鎖的機制。
我們分別用這2種解決方案來改造上面死鎖的程式碼,先看方案1:
public class NormalLockDemo { private static Object No1 = new Object(); private static Object No2 = new Object(); /** * 按照No1、No2順序加鎖 * @throws InterruptedException */ private static void laowang() throws InterruptedException { String threadName = Thread.currentThread().getName(); synchronized (No1) { System.out.println(threadName + " get NO1"); Thread.sleep(100); synchronized (No2) { System.out.println(threadName + " get NO2"); } } } /** * 按照No1、No2順序加鎖 * @throws InterruptedException */ private static void laosong() throws InterruptedException { String threadName = Thread.currentThread().getName(); synchronized (No1) { System.out.println(threadName + " get NO1"); Thread.sleep(100); synchronized (No2) { System.out.println(threadName + " get NO2"); } } } static class Laowang extends Thread { private String name; public Laowang(String name) { this.name = name; } @Override public void run() { Thread.currentThread().setName(name); try { laowang(); } catch (Exception e) { e.printStackTrace(); } } } static class Laosong extends Thread { private String name; public Laosong(String name) { this.name = name; } @Override public void run() { Thread.currentThread().setName(name); try { laosong(); } catch (Exception e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { Laosong laosong = new Laosong("laosong"); laosong.start(); Laowang laowang = new Laowang("laowang"); laowang.start(); Thread.sleep(1000); System.out.println("2個人都完成了大保健"); } }
從程式輸出可以看到,通過順序拿鎖的方式,2個人都完成了大保健,解決了死鎖問題。
再看方案2,使用ReentrantLock採用嘗試獲取鎖的方式,如果對ReentrantLock不熟悉,歡迎閱讀《java之AQS和顯式鎖》。
import java.util.Random; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TryLock { private static Lock No1 = new ReentrantLock(); private static Lock No2 = new ReentrantLock(); /*** * 先嚐試拿No1鎖,再嘗試拿No2鎖,No2鎖沒拿到,連同No1鎖一起釋放掉 * @throws InterruptedException */ private static void laowang() throws InterruptedException { String threadName = Thread.currentThread().getName(); Random r = new Random(); while (true) { if (No1.tryLock()) { try { System.out.println(threadName + " get NO2"); if (No2.tryLock()) { try { System.out.println(threadName + " get NO1"); break; } finally { No2.unlock(); } } } finally { No1.unlock(); } } Thread.sleep(r.nextInt(5)); } } /** * 先嚐試拿No2鎖,再嘗試拿No1鎖,No1鎖沒拿到,連同No2鎖一起釋放掉 * * @throws InterruptedException */ private static void laosong() throws InterruptedException { String threadName = Thread.currentThread().getName(); Random r = new Random(); while (true) { if (No2.tryLock()) { try { System.out.println(threadName + " get NO2"); if (No1.tryLock()) { try { System.out.println(threadName + " get NO1"); break; } finally { No1.unlock(); } } } finally { No2.unlock(); } } } Thread.sleep(r.nextInt(5)); } static class Laowang extends Thread { private String name; public Laowang(String name) { this.name = name; } @Override public void run() { Thread.currentThread().setName(name); try { laowang(); } catch (Exception e) { e.printStackTrace(); } } } static class Laosong extends Thread { private String name; public Laosong(String name) { this.name = name; } @Override public void run() { Thread.currentThread().setName(name); try { laosong(); } catch (Exception e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { Laosong laosong = new Laosong("laosong"); laosong.start(); Laowang laowang = new Laowang("laowang"); laowang.start(); Thread.sleep(1000); System.out.println("2個人都完成了大保健"); } }
從程式輸出可以看到,laowang執行緒搶到了NO2這把鎖,但是在獲取NO1的時候失敗了,所以把NO2也釋放了。這樣做就使得2個執行緒都可以獲取到鎖,不會有死鎖問題產生。
3、結語
本篇幅就介紹這麼多內容,希望大家看了有收穫。Java併發程式設計專題要分享的內容到此就結束了,下一個專題將介紹Java效能優化和JVM相關內容,閱讀過程中如發現描述有誤,請指出,謝謝。