Java 多執行緒安全問題簡單切入詳細解析
執行緒安全
假如Java程式中有多個執行緒在同時執行,而這些執行緒可能會同時執行一部分的程式碼。如果說該Java程式每次執行的結果和單執行緒的執行結果是一樣的,並且其他的變數值也都是和預期的結果是一樣的,那麼就可以說執行緒是安全的。
解析什麼是執行緒安全:賣電影票案例
假如有一個電影院上映《葫蘆娃大戰奧特曼》,售票100張(1-100號),分三種情況賣票:
情況1
該電影院開設一個售票視窗,一個視窗賣一百張票,沒有問題。就如同單執行緒程式不會出現安全問題一樣。
情況2
該電影院開設n(n>1)個售票視窗,每個售票視窗售出指定號碼的票,也不會出現問題。就如同多執行緒程式,沒有訪問共享資料,不會產生問題。
情況3
該電影院開設n(n>1)個售票視窗,每個售票窗口出售的票都是沒有規定的(如:所有的視窗都可以出售1號票),這就會出現問題了,假如三個視窗同時在賣同一張票,或有的票已經售出,還有視窗還在出售。就如同多執行緒程式,訪問了共享資料,會產生執行緒安全問題。
賣100張電影票Java程式實現:出現情況3類似情況
public class MovieTicket01 implements Runnable { /** * 電影票數量 */ private static int ticketNumber = 100; /** * 在實現類中重寫Runnable介面的run方法,並設定此執行緒要執行的任務 */ @Override public void run() { // 設定此執行緒要執行的任務 while (ticketNumber > 0) { // 提高程式安全的概率,讓程式睡眠10毫秒 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } // 電影票出售 System.out.println("售票視窗(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket01.ticketNumber + "號電影票"); ticketNumber --; } } }
// 測試 public class Demo01MovieTicket { public static void main(String[] args) { // 建立一個 Runnable介面的實現類物件。 MovieTicket01 movieTicket = new MovieTicket01(); // 建立Thread類物件,構造方法中傳遞Runnable介面的實現類物件(三個視窗)。 Thread window0 = new Thread(movieTicket); Thread window1 = new Thread(movieTicket); Thread window2 = new Thread(movieTicket); // 設定一下視窗名字,方便輸出確認 window0.setName("window0"); window1.setName("window1"); window2.setName("window2"); // 呼叫Threads類中的start方法,開啟新的執行緒執行run方法 window0.start(); window1.start(); window2.start(); } }
控制檯部分輸出: 售票視窗(window0)正在出售:100號電影票 售票視窗(window2)正在出售:99號電影票 售票視窗(window1)正在出售:100號電影票 售票視窗(window0)正在出售:97號電影票 售票視窗(window2)正在出售:97號電影票 售票視窗(window1)正在出售:97號電影票 售票視窗(window1)正在出售:94號電影票 售票視窗(window2)正在出售:94號電影票 . . . . . . 售票視窗(window0)正在出售:7號電影票 售票視窗(window2)正在出售:4號電影票 售票視窗(window0)正在出售:4號電影票 售票視窗(window1)正在出售:2號電影票 售票視窗(window1)正在出售:1號電影票 售票視窗(window2)正在出售:0號電影票 售票視窗(window0)正在出售:-1號電影票
可以看到,三個視窗(執行緒)同時出售不指定號數的票(訪問共享資料),出現了賣票重複,和出售了不存在的票號數(0、-1)
Java程式中為什麼會出現這種情況
- 在CPU執行緒的排程分類中,Java使用的是搶佔式排程。
- 我們開啟了三個執行緒,3個執行緒一起在搶奪CPU的執行權,誰能搶到誰就可以被執行。
- 從輸出結果可以知道,剛開始搶奪CPU執行權的時候,執行緒0(window0視窗)先搶到,再到執行緒1(window1視窗)搶到,最後執行緒2(window2視窗)才搶到。
- 那麼為什麼100號票已經在0號窗口出售了,在1號視窗還會出售呢?其實很簡單,執行緒0先搶到CPU執行權,於是有了執行權後,他就開始囂張了,作為第一個它通過while判斷,很自豪的拿著ticketNumber = 100進入while裡面開始執行。
- 可執行緒0是萬萬沒有想到,這時候的執行緒1,在拿到執行權後,線上程0剛剛實現print語句還沒開始ticketNumber --的時候,執行緒1以ticketNumber = 100跑進了while裡面。
- 執行緒2很遺憾,線上程0執行了ticketNumber --了才急匆匆的進入while裡面,不過它也不甘落後,於是拼命追趕。終於,後來居上,線上程1還沒開始print的時候,他就開始print了。於是便出現了控制檯的前三條輸出的情況。
售票視窗(window0)正在出售:100號電影票 售票視窗(window2)正在出售:99號電影票 售票視窗(window1)正在出售:100號電影票
window0、window1、window2分別對應執行緒0、執行緒1、執行緒2
- 以此類推,直到最後程式執行完畢。
解決情況3的共享資料問題
通過執行緒的同步,來解決共享資料問題。有三種方式,分別是同步程式碼塊、同步方法、鎖機制。
同步程式碼塊
public class MovieTicket02 implements Runnable { /** * 電影票數量 */ private static int ticketNumber = 100; /** * 建立鎖物件 */ Object object = new Object(); /** * 在實現類中重寫Runnable介面的run方法,並設定此執行緒要執行的任務 */ @Override public void run() { // 設定此執行緒要執行的任務 synchronized (object) { // 把訪問了共享資料的程式碼放到同步程式碼中 while (ticketNumber > 0) { // 提高程式安全的概率,讓程式睡眠10毫秒 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } // 電影票出售 System.out.println("售票視窗(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket02.ticketNumber + "號電影票"); ticketNumber --; } } } }
// 進行測試 public class Demo02MovieTicket { public static void main(String[] args) { // 建立一個 Runnable介面的實現類物件。 MovieTicket02 movieTicket = new MovieTicket02(); // 建立Thread類物件,構造方法中傳遞Runnable介面的實現類物件(三個視窗)。 Thread window0 = new Thread(movieTicket); Thread window1 = new Thread(movieTicket); Thread window2 = new Thread(movieTicket); // 設定一下視窗名字,方便輸出確認 window0.setName("window0"); window1.setName("window1"); window2.setName("window2"); // 呼叫Threads類中的start方法,開啟新的執行緒執行run方法 window0.start(); window1.start(); window2.start(); } }
控制檯輸出: 售票視窗(window0)正在出售:100號電影票 售票視窗(window0)正在出售:99號電影票 售票視窗(window0)正在出售:98號電影票 售票視窗(window0)正在出售:97號電影票 售票視窗(window0)正在出售:96號電影票 . . . . . . 售票視窗(window0)正在出售:5號電影票 售票視窗(window0)正在出售:4號電影票 售票視窗(window0)正在出售:3號電影票 售票視窗(window0)正在出售:2號電影票 售票視窗(window0)正在出售:1號電影票
這時候,控制檯不再出售不存在的電影號數以及重複的電影號數了。
通過程式碼塊中的鎖物件,可以使用任意的物件。但是必須保證多個執行緒使用的鎖物件是同一。鎖物件作用:把同步程式碼塊鎖住,只讓一個執行緒在同步程式碼塊中執行。
總結:同步中的執行緒,沒有執行完畢,不會釋放鎖,同步外的執行緒,沒有鎖,進不去同步。
同步方法
public class MovieTicket03 implements Runnable { /** * 電影票數量 */ private static int ticketNumber = 100; /** * 建立鎖物件 */ Object object = new Object(); /** * 在實現類中重寫Runnable介面的run方法,並設定此執行緒要執行的任務 */ @Override public void run() { // 設定此執行緒要執行的任務 ticket(); } public synchronized void ticket() { // 把訪問了共享資料的程式碼放到同步程式碼中 while (ticketNumber > 0) { // 提高程式安全的概率,讓程式睡眠10毫秒 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } // 電影票出售 System.out.println("售票視窗(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket03.ticketNumber + "號電影票"); ticketNumber --; } } }
測試與同步程式碼塊一樣。
鎖機制(Lock鎖)
在Java中,Lock鎖機制又稱為同步鎖,加鎖public void lock(),釋放同步鎖public void unlock()。
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class MovieTicket05 implements Runnable { /** * 電影票數量 */ private static int ticketNumber = 100; Lock reentrantLock = new ReentrantLock(); /** * 在實現類中重寫Runnable介面的run方法,並設定此執行緒要執行的任務 */ @Override public void run() { // 設定此執行緒要執行的任務 while (ticketNumber > 0) { reentrantLock.lock(); // 提高程式安全的概率,讓程式睡眠10毫秒 try { Thread.sleep(10); // 電影票出售 System.out.println("售票視窗(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket05.ticketNumber + "號電影票"); ticketNumber --; } catch (InterruptedException e) { e.printStackTrace(); } finally { reentrantLock.unlock(); } } } }
// 測試 public class Demo05MovieTicket { public static void main(String[] args) { // 建立一個 Runnable介面的實現類物件。 MovieTicket05 movieTicket = new MovieTicket05(); // 建立Thread類物件,構造方法中傳遞Runnable介面的實現類物件(三個視窗)。 Thread window0 = new Thread(movieTicket); Thread window1 = new Thread(movieTicket); Thread window2 = new Thread(movieTicket); // 設定一下視窗名字,方便輸出確認 window0.setName("window0"); window1.setName("window1"); window2.setName("window2"); // 呼叫Threads類中的start方法,開啟新的執行緒執行run方法 window0.start(); window1.start(); window2.start(); } }
控制檯部分輸出: 售票視窗(window0)正在出售:100號電影票 售票視窗(window0)正在出售:99號電影票 售票視窗(window0)正在出售:98號電影票 售票視窗(window0)正在出售:97號電影票 售票視窗(window0)正在出售:96號電影票 . . . . . . 售票視窗(window1)正在出售:7號電影票 售票視窗(window1)正在出售:6號電影票 售票視窗(window1)正在出售:5號電影票 售票視窗(window1)正在出售:4號電影票 售票視窗(window1)正在出售:3號電影票 售票視窗(window2)正在出售:2號電影票 售票視窗(window1)正在出售:1號電影票
與前兩種方式不同,前兩種方式,只有執行緒0能夠進入同步機制執行程式碼,Lock鎖機制,三個執行緒都可以進行執行,通過Lock鎖機制來解決共享資料問題。
Java 多執行緒安全問題就到這裡了,如果有什麼不足、錯誤的地方,希望大佬們指正。