進程和線程的關係就是:一個進程能夠包含一個或多個線程,但至少會有一個線程。java
操做系統調度的最小任務單位其實不是進程,而是線程。經常使用的Windows、Linux等操做系統都採用搶佔式多任務,如何調度線程徹底由操做系統決定,程序本身不能決定何時執行,以及執行多長時間。數據庫
Java語言內置了多線程支持:一個Java程序其實是一個JVM進程,JVM進程用一個主線程來執行main()
方法,在main()
方法內部,咱們又能夠啓動多個線程。此外,JVM還有負責垃圾回收的其餘工做線程等。編程
所以,對於大多數Java程序來講,咱們說多任務,其實是說如何使用多線程實現多任務。安全
和單線程相比,多線程編程的特色在於:多線程常常須要讀寫共享數據,而且須要同步。例如,播放電影時,就必須由一個線程播放視頻,另外一個線程播放音頻,兩個線程須要協調運行,不然畫面和聲音就不一樣步。所以,多線程編程的複雜度高,調試更困難。網絡
Java多線程編程的特色又在於:多線程
咱們但願新線程能執行指定的代碼,有如下幾種方法:架構
Thread
派生一個自定義類,而後覆寫run()
方法Thread
實例時,傳入一個Runnable
實例直接調用run()
方法,至關於調用了一個普通的Java方法,當前線程並無任何改變,也不會啓動新線程。併發
必須調用Thread
實例的start()
方法才能啓動新線程,若是咱們查看Thread
類的源代碼,會看到start()
方法內部調用了一個private native void start0()
方法,native
修飾符表示這個方法是由JVM虛擬機內部的C代碼實現的,不是由Java代碼實現的。性能
能夠對線程設定優先級,設定優先級的方法是:網站
Thread.setPriority(int n) // 1~10, 默認值5
優先級高的線程被操做系統調度的優先級較高,操做系統對高優先級線程可能調度更頻繁,但咱們決不能經過設置優先級來確保高優先級的線程必定會先執行。
Java用Thread
對象表示一個線程,經過調用start()
啓動一個新線程;
一個線程對象只能調用一次start()
方法;
線程的執行代碼寫在run()
方法中;
線程調度由操做系統決定,程序自己沒法決定調度順序;
Thread.sleep()
能夠把當前線程暫停一段時間。
在Java程序中,一個線程對象只能調用一次start()
方法啓動新線程,並在新線程中執行run()
方法。一旦run()
方法執行完畢,線程就結束了。所以,Java線程的狀態有如下幾種:
run()
方法的Java代碼;sleep()
方法正在計時等待;run()
方法執行完畢。用一個狀態轉移圖表示以下:
┌─────────────┐ │ New │ └─────────────┘ │ ▼ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌─────────────┐ ┌─────────────┐ ││ Runnable │ │ Blocked ││ └─────────────┘ └─────────────┘ │┌─────────────┐ ┌─────────────┐│ │ Waiting │ │Timed Waiting│ │└─────────────┘ └─────────────┘│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ▼ ┌─────────────┐ │ Terminated │ └─────────────┘
當線程啓動後,它能夠在Runnable
、Blocked
、Waiting
和Timed Waiting
這幾個狀態之間切換,直到最後變成Terminated
狀態,線程終止。
線程終止的緣由有:
run()
方法執行到return
語句返回;run()
方法由於未捕獲的異常致使線程終止;Thread
實例調用stop()
方法強制終止(強烈不推薦使用)。一個線程還能夠等待另外一個線程直到其運行結束。例如,main
線程在啓動t
線程後,能夠經過t.join()
等待t
線程結束後再繼續運行
public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { System.out.println("hello"); }); System.out.println("start"); t.start(); t.join(); System.out.println("end"); } }
當main
線程對線程對象t
調用join()
方法時,主線程將等待變量t
表示的線程運行結束,即join
就是指等待該線程結束,而後才繼續往下執行自身線程。因此,上述代碼打印順序能夠確定是main
線程先打印start
,t
線程再打印hello
,main
線程最後再打印end
。
若是t
線程已經結束,對實例t
調用join()
會馬上返回。此外,join(long)
的重載方法也能夠指定一個等待時間,超過等待時間後就再也不繼續等待。
Java線程對象Thread
的狀態包括:New
、Runnable
、Blocked
、Waiting
、Timed Waiting
和Terminated
;
經過對另外一個線程對象調用join()
方法能夠等待其執行結束;
能夠指定等待時間,超過等待時間線程仍然沒有結束就再也不等待;
對已經運行結束的線程調用join()
方法會馬上返回。
若是線程須要執行一個長時間任務,就可能須要能中斷線程。中斷線程就是其餘線程給該線程發一個信號,該線程收到信號後結束執行run()
方法,使得自身線程能馬上結束運行。
咱們舉個栗子:假設從網絡下載一個100M的文件,若是網速很慢,用戶等得不耐煩,就可能在下載過程當中點「取消」,這時,程序就須要中斷下載線程的執行。
中斷一個線程很是簡單,只須要在其餘線程中對目標線程調用interrupt()
方法,目標線程須要反覆檢測自身狀態是不是interrupted狀態,若是是,就馬上結束運行。
@Slf4j public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(10); // 暫停1毫秒 t.interrupt(); // 中斷t線程 t.join(); // 等待t線程結束 log.info("end"); } } @Slf4j class MyThread extends Thread { public void run() { int n = 0; while (!isInterrupted()) { n++; log.info(n + " hello!"); } } }
仔細看上述代碼,main
線程經過調用t.interrupt()
方法中斷t
線程,可是要注意,interrupt()
方法僅僅向t
線程發出了「中斷請求」,至於t
線程是否能馬上響應,要看具體代碼。而t
線程的while
循環會檢測isInterrupted()
,因此上述代碼能正確響應interrupt()
請求,使得自身馬上結束運行run()
方法。
若是線程處於等待狀態,例如,t.join()
會讓main
線程進入等待狀態,此時,若是對main
線程調用interrupt()
,join()
方法會馬上拋出InterruptedException
,所以,目標線程只要捕獲到join()
方法拋出的InterruptedException
,就說明有其餘線程對其調用了interrupt()
方法,一般狀況下該線程應該馬上結束運行。
咱們來看下面的示例代碼:
@Slf4j public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1000); t.interrupt(); // 中斷t線程 t.join(); // 等待t線程結束 log.info("end"); } } @Slf4j class MyThread extends Thread { public void run() { Thread hello = new HelloThread(); hello.start(); // 啓動hello線程 try { hello.join(); // 等待hello線程結束 } catch (InterruptedException e) { log.info("MyThread interrupted!"); } hello.interrupt(); } } @Slf4j class HelloThread extends Thread { public void run() { int n = 0; while (!isInterrupted()) { n++; log.info(n + " hello!"); try { Thread.sleep(100); } catch (InterruptedException e) { log.info("HelloThread interrupted!"); break; } } } }
main
線程經過調用t.interrupt()
從而通知t
線程中斷,而此時t
線程正位於hello.join()
的等待中,此方法會馬上結束等待並拋出InterruptedException
。因爲咱們在t
線程中捕獲了InterruptedException
,所以,就能夠準備結束該線程。在t
線程結束前,對hello
線程也進行了interrupt()
調用通知其中斷。若是去掉這一行代碼,能夠發現hello
線程仍然會繼續運行,且JVM不會退出。
另外一個經常使用的中斷線程的方法是設置標誌位。咱們一般會用一個running
標誌位來標識線程是否應該繼續運行,在外部線程中,經過把HelloThread.running
置爲false
,就可讓線程結束:
public class Main { public static void main(String[] args) throws InterruptedException { HelloThread t = new HelloThread(); t.start(); Thread.sleep(1); t.running = false; // 標誌位置爲false } } class HelloThread extends Thread { public volatile boolean running = true; public void run() { int n = 0; while (running) { n++; System.out.println(n + " hello!"); } System.out.println("end!"); } }
注意到HelloThread
的標誌位boolean running
是一個線程間共享的變量。線程間共享變量須要使用volatile
關鍵字標記,確保每一個線程都能讀取到更新後的變量值。
爲何要對線程間共享的變量用關鍵字volatile
聲明?這涉及到Java的內存模型。在Java虛擬機中,變量的值保存在主內存中,可是,當線程訪問變量時,它會先獲取一個副本,並保存在本身的工做內存中。若是線程修改了變量的值,虛擬機會在某個時刻把修改後的值回寫到主內存,可是,這個時間是不肯定的!
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ Main Memory │ │ ┌───────┐┌───────┐┌───────┐ │ │ var A ││ var B ││ var C │ │ └───────┘└───────┘└───────┘ │ │ ▲ │ ▲ │ ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─ │ │ │ │ ┌ ─ ─ ┼ ┼ ─ ─ ┐ ┌ ─ ─ ┼ ┼ ─ ─ ┐ ▼ │ ▼ │ │ ┌───────┐ │ │ ┌───────┐ │ │ var A │ │ var C │ │ └───────┘ │ │ └───────┘ │ Thread 1 Thread 2 └ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ┘
這會致使若是一個線程更新了某個變量,另外一個線程讀取的值可能仍是更新前的。例如,主內存的變量a = true
,線程1執行a = false
時,它在此刻僅僅是把變量a
的副本變成了false
,主內存的變量a
仍是true
,在JVM把修改後的a
回寫到主內存以前,其餘線程讀取到的a
的值仍然是true
,這就形成了多線程之間共享的變量不一致。
所以,volatile
關鍵字的目的是告訴虛擬機:
volatile
關鍵字解決的是可見性問題:當一個線程修改了某個共享變量的值,其餘線程可以馬上看到修改後的值。
若是咱們去掉volatile
關鍵字,運行上述程序,發現效果和帶volatile
差很少,這是由於在x86的架構下,JVM回寫主內存的速度很是快,可是,換成ARM的架構,就會有顯著的延遲。
對目標線程調用interrupt()
方法能夠請求中斷一個線程,目標線程經過檢測isInterrupted()
標誌獲取自身是否已中斷。若是目標線程處於等待狀態,該線程會捕獲到InterruptedException
;
目標線程檢測到isInterrupted()
爲true
或者捕獲了InterruptedException
都應該馬上結束自身線程;
經過標誌位判斷須要正確使用volatile
關鍵字;
volatile
關鍵字解決了共享變量在線程間的可見性問題。
若是有一個線程沒有退出,JVM進程就不會退出。因此,必須保證全部線程都能及時結束。
然而這類線程常常沒有負責人來負責結束它們。可是,當其餘線程結束時,JVM進程又必需要結束,怎麼辦?
答案是使用守護線程(Daemon Thread)。
守護線程是指爲其餘線程服務的線程。在JVM中,全部非守護線程都執行完畢後,不管有沒有守護線程,虛擬機都會自動退出。
所以,JVM退出時,沒必要關心守護線程是否已結束。
如何建立守護線程呢?方法和普通線程同樣,只是在調用start()
方法前,調用setDaemon(true)
把該線程標記爲守護線程:
Thread t = new MyThread(); t.setDaemon(true); t.start();
在守護線程中,編寫代碼要注意:守護線程不能持有任何須要關閉的資源,例如打開文件等,由於虛擬機退出時,守護線程沒有任何機會來關閉文件,這會致使數據丟失。
守護線程是爲其餘線程服務的線程;
全部非守護線程都執行完畢後,虛擬機退出;
守護線程不能持有須要關閉的資源(如打開文件等)。
當多個線程同時運行時,線程的調度由操做系統決定,程序自己沒法決定。所以,任何一個線程都有可能在任何指令處被操做系統暫停,而後在某個時間段後繼續執行。
這個時候,有個單線程模型下不存在的問題就來了:若是多個線程同時讀寫共享變量,會出現數據不一致的問題。
咱們來看一個例子:
public class Main { public static void main(String[] args) throws Exception { AddThread add = new AddThread(); DecThread dec = new DecThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println(Counter.count); } } class Counter { public static int count = 0; } class AddThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { Counter.count += 1; } } } class DecThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { Counter.count -= 1; } } }
上面的代碼很簡單,兩個線程同時對一個int
變量進行操做,一個加10000次,一個減10000次,最後結果應該是0,可是,每次運行,結果實際上都是不同的。
這是由於對變量進行讀取和寫入時,結果要正確,必須保證是原子操做。原子操做是指不能被中斷的一個或一系列操做。
例如,對於語句:
n = n + 1;
看上去是一行語句,實際上對應了3條指令:
ILOAD IADD ISTORE
咱們假設n
的值是100
,若是兩個線程同時執行n = n + 1
,獲得的結果極可能不是102
,而是101
,緣由在於:
┌───────┐ ┌───────┐ │Thread1│ │Thread2│ └───┬───┘ └───┬───┘ │ │ │ILOAD (100) │ │ │ILOAD (100) │ │IADD │ │ISTORE (101) │IADD │ │ISTORE (101)│ ▼ ▼
若是線程1在執行ILOAD
後被操做系統中斷,此刻若是線程2被調度執行,它執行ILOAD
後獲取的值仍然是100
,最終結果被兩個線程的ISTORE
寫入後變成了101
,而不是期待的102
。
這說明多線程模型下,要保證邏輯正確,對共享變量進行讀寫時,必須保證一組指令以原子方式執行:即某一個線程執行時,其餘線程必須等待:
┌───────┐ ┌───────┐ │Thread1│ │Thread2│ └───┬───┘ └───┬───┘ │ │ │-- lock -- │ │ILOAD (100) │ │IADD │ │ISTORE (101) │ │-- unlock -- │ │ │-- lock -- │ │ILOAD (101) │ │IADD │ │ISTORE (102) │ │-- unlock -- ▼ ▼
經過加鎖和解鎖的操做,就能保證3條指令老是在一個線程執行期間,不會有其餘線程會進入此指令區間。即便在執行期線程被操做系統中斷執行,其餘線程也會由於沒法得到鎖致使沒法進入此指令區間。只有執行線程將鎖釋放後,其餘線程纔有機會得到鎖並執行。這種加鎖和解鎖之間的代碼塊咱們稱之爲臨界區(Critical Section),任什麼時候候臨界區最多隻有一個線程能執行。
可見,保證一段代碼的原子性就是經過加鎖和解鎖實現的。Java程序使用synchronized
關鍵字對一個對象進行加鎖:
synchronized(lock) { n = n + 1; }
synchronized
保證了代碼塊在任意時刻最多隻有一個線程能執行。咱們把上面的代碼用synchronized
改寫以下:
public class Main { public static void main(String[] args) throws Exception { AddThread add = new AddThread(); DecThread dec = new DecThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println(Counter.count); } } class Counter { public static final Object lock = new Object(); public static int count = 0; } class AddThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { synchronized (Counter.lock) { Counter.count += 1; } } } } class DecThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { synchronized (Counter.lock) { Counter.count -= 1; } } } }
注意到代碼:
synchronized(Counter.lock) { // 獲取鎖 ... } // 釋放鎖
它表示用Counter.lock
實例做爲鎖,兩個線程在執行各自的synchronized(Counter.lock) { ... }
代碼塊時,必須先得到鎖,才能進入代碼塊進行。執行結束後,在synchronized
語句塊結束會自動釋放鎖。這樣一來,對Counter.count
變量進行讀寫就不可能同時進行。上述代碼不管運行多少次,最終結果都是0。
使用synchronized
解決了多線程同步訪問共享變量的正確性問題。可是,它的缺點是帶來了性能降低。由於synchronized
代碼塊沒法併發執行。此外,加鎖和解鎖須要消耗必定的時間,因此,synchronized
會下降程序的執行效率。
咱們來歸納一下如何使用synchronized
:
synchronized(lockObject) { ... }
。在使用synchronized
的時候,沒必要擔憂拋出異常。由於不管是否有異常,都會在synchronized
結束處正確釋放鎖:
public void add(int m) { synchronized (obj) { if (m < 0) { throw new RuntimeException(); } this.value += m; } // 不管有無異常,都會在此釋放鎖 }
咱們再來看一個錯誤使用synchronized
的例子:
public class Main { public static void main(String[] args) throws Exception { AddThread add = new AddThread(); DecThread dec = new DecThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println(Counter.count); } } class Counter { public static final Object lock1 = new Object(); public static final Object lock2 = new Object(); public static int count = 0; } class AddThread extends Thread { public void run() { for (int i=0; i<10000; i++) { synchronized(Counter.lock1) { Counter.count += 1; } } } } class DecThread extends Thread { public void run() { for (int i=0; i<10000; i++) { synchronized(Counter.lock2) { Counter.count -= 1; } } } }
結果並非0,這是由於兩個線程各自的synchronized
鎖住的不是同一個對象!這使得兩個線程各自均可以同時得到鎖:由於JVM只保證同一個鎖在任意時刻只能被一個線程獲取,但兩個不一樣的鎖在同一時刻能夠被兩個線程分別獲取。
所以,使用synchronized
的時候,獲取到的是哪一個鎖很是重要。鎖對象若是不對,代碼邏輯就不對。
咱們再看一個例子:
public class Main { public static void main(String[] args) throws Exception { Thread[] ts = new Thread[]{new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread()}; for (Thread t : ts) { t.start(); } for (Thread t : ts) { t.join(); } System.out.println(Counter.studentCount); System.out.println(Counter.teacherCount); } } class Counter { public static final Object lock = new Object(); public static int studentCount = 0; public static int teacherCount = 0; } class AddStudentThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { synchronized (Counter.lock) { Counter.studentCount += 1; } } } } class DecStudentThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { synchronized (Counter.lock) { Counter.studentCount -= 1; } } } } class AddTeacherThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { synchronized (Counter.lock) { Counter.teacherCount += 1; } } } } class DecTeacherThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { synchronized (Counter.lock) { Counter.teacherCount -= 1; } } } }
上述代碼的4個線程對兩個共享變量分別進行讀寫操做,可是使用的鎖都是Counter.lock
這一個對象,這就形成了本來能夠併發執行的Counter.studentCount += 1
和Counter.teacherCount += 1
,如今沒法併發執行了,執行效率大大下降。實際上,須要同步的線程能夠分紅兩組:AddStudentThread
和DecStudentThread
,AddTeacherThread
和DecTeacherThread
,組之間不存在競爭,所以,應該使用兩個不一樣的鎖,即:
AddStudentThread
和DecStudentThread
使用lockStudent
鎖:
synchronized(Counter.lockStudent) { ... }
AddTeacherThread
和DecTeacherThread
使用lockTeacher
鎖:
synchronized(Counter.lockTeacher) { ... }
這樣才能最大化地提升執行效率。
JVM規範定義了幾種原子操做:
long
和double
除外)賦值,例如:int n = m
;List list = anotherList
。long
和double
是64位數據,JVM沒有明確規定64位賦值操做是否是一個原子操做,不過在x64平臺的JVM是把long
和double
的賦值做爲原子操做實現的。
單條原子操做的語句不須要同步。例如:
public void set(int m) { synchronized(lock) { this.value = m; } }
就不須要同步。
對引用也是相似。例如:
public void set(String s) { this.value = s; }
上述賦值語句並不須要同步。
可是,若是是多行賦值語句,就必須保證是同步操做,例如:
class Pair { int first; int last; public void set(int first, int last) { synchronized(this) { this.first = first; this.last = last; } } }
有些時候,經過一些巧妙的轉換,能夠把非原子操做變爲原子操做。例如,上述代碼若是改形成:
class Pair { int[] pair; public void set(int first, int last) { int[] ps = new int[] { first, last }; this.pair = ps; } }
就再也不須要同步,由於this.pair = ps
是引用賦值的原子操做。而語句:
int[] ps = new int[] { first, last };
這裏的ps
是方法內部定義的局部變量,每一個線程都會有各自的局部變量,互不影響,而且互不可見,並不須要同步。
多線程同時讀寫共享變量時,會形成邏輯錯誤,所以須要經過synchronized
同步;
同步的本質就是給指定對象加鎖,加鎖後才能繼續執行後續代碼;
注意加鎖對象必須是同一個實例;
對JVM定義的單個原子操做不須要同步。
若是一個類被設計爲容許多線程正確訪問,咱們就說這個類就是「線程安全」的(thread-safe),上面的Counter
類就是線程安全的。Java標準庫的java.lang.StringBuffer
也是線程安全的。
還有一些不變類,例如String
,Integer
,LocalDate
,它們的全部成員變量都是final
,多線程同時訪問時只能讀不能寫,這些不變類也是線程安全的。
最後,相似Math
這些只提供靜態方法,沒有成員變量的類,也是線程安全的。
除了上述幾種少數狀況,大部分類,例如ArrayList
,都是非線程安全的類,咱們不能在多線程中修改它們。可是,若是全部線程都只讀取,不寫入,那麼ArrayList
是能夠安全地在線程間共享的。
沒有特殊說明時,一個類默認是非線程安全的。
下面兩種寫法是等價的:
public void add(int n) { synchronized(this) { // 鎖住this count += n; } // 解鎖 } public synchronized void add(int n) { // 鎖住this count += n; } // 解鎖
所以,用synchronized
修飾的方法就是同步方法,它表示整個方法都必須用this
實例加鎖。
對於static
方法,是沒有this
實例的,由於static
方法是針對類而不是實例。可是咱們注意到任何一個類都有一個由JVM自動建立的Class
實例,所以,對static
方法添加synchronized
,鎖住的是該類的class
實例。下面兩種寫法是等價的:
public class Counter { public synchronized static void test(int n) { ... } } public class Counter { public static void test(int n) { synchronized(Counter.class) { ... } } }
考察Counter
的get()
方法:
public class Counter { private int count; public int get() { return count; } ... }
它沒有同步,由於讀一個int
變量不須要同步。
然而,若是咱們把代碼稍微改一下,返回一個包含兩個int
的對象:
public class Counter { private int first; private int last; public Pair get() { Pair p = new Pair(); p.first = first; p.last = last; return p; } ... }
就必需要同步了。
用synchronized
修飾方法能夠把整個方法變爲同步代碼塊,synchronized
方法加鎖對象是this
;
經過合理的設計和數據封裝可讓一個類變爲「線程安全」;
一個類沒有特殊說明,默認不是thread-safe;
多線程可否安全訪問某個非線程安全的實例,須要具體問題具體分析。
Java的線程鎖是可重入的鎖。
什麼是可重入的鎖?咱們仍是來看例子:
public class Counter { private int count = 0; public synchronized void add(int n) { if (n < 0) { dec(-n); } else { count += n; } } public synchronized void dec(int n) { count += n; } }
觀察synchronized
修飾的add()
方法,一旦線程執行到add()
方法內部,說明它已經獲取了當前實例的this
鎖。若是傳入的n < 0
,將在add()
方法內部調用dec()
方法。因爲dec()
方法也須要獲取this
鎖,如今問題來了:
對同一個線程,可否在獲取到鎖之後繼續獲取同一個鎖?
答案是確定的。JVM容許同一個線程重複獲取同一個鎖,這種能被同一個線程反覆獲取的鎖,就叫作可重入鎖。
因爲Java的線程鎖是可重入鎖,因此,獲取鎖的時候,不但要判斷是不是第一次獲取,還要記錄這是第幾回獲取。每獲取一次鎖,記錄+1,每退出synchronized
塊,記錄-1,減到0的時候,纔會真正釋放鎖。
一個線程能夠獲取一個鎖後,再繼續獲取另外一個鎖。例如:
public void add(int m) { synchronized(lockA) { // 得到lockA的鎖 this.value += m; synchronized(lockB) { // 得到lockB的鎖 this.another += m; } // 釋放lockB的鎖 } // 釋放lockA的鎖 } public void dec(int m) { synchronized(lockB) { // 得到lockB的鎖 this.another -= m; synchronized(lockA) { // 得到lockA的鎖 this.value -= m; } // 釋放lockA的鎖 } // 釋放lockB的鎖 }
在獲取多個鎖的時候,不一樣線程獲取多個不一樣對象的鎖可能致使死鎖。對於上述代碼,線程1和線程2若是分別執行add()
和dec()
方法時:
add()
,得到lockA
;dec()
,得到lockB
。隨後:
lockB
,失敗,等待中;lockA
,失敗,等待中。此時,兩個線程各自持有不一樣的鎖,而後各自試圖獲取對方手裏的鎖,形成了雙方無限等待下去,這就是死鎖。
死鎖發生後,沒有任何機制能解除死鎖,只能強制結束JVM進程。
所以,在編寫多線程應用時,要特別注意防止死鎖。由於死鎖一旦造成,就只能強制結束進程。
那麼咱們應該如何避免死鎖呢?答案是:線程獲取鎖的順序要一致。即嚴格按照先獲取lockA
,再獲取lockB
的順序,改寫dec()
方法以下:
public void dec(int m) { synchronized(lockA) { // 得到lockA的鎖 this.value -= m; synchronized(lockB) { // 得到lockB的鎖 this.another -= m; } // 釋放lockB的鎖 } // 釋放lockA的鎖 }
Java的synchronized
鎖是可重入鎖;
死鎖產生的條件是多線程各自持有不一樣的鎖,並互相試圖獲取對方已持有的鎖,致使無限等待;
避免死鎖的方法是多線程獲取鎖的順序要一致。