同步代碼塊比較經典的例子是火車站的售票員售票的過程,下面通過代碼來分析同步代碼塊在這里面的作用。
package cn.sunzn.synchronize; public class SynchronizeCode { public static void main(String[] args) { new TicketSeller().start(); new TicketSeller().start(); new TicketSeller().start(); new TicketSeller().start(); } } class TicketSeller extends Thread { private static int ticket = 100; private static Object lock = new Object(); public void run() { while (true) { synchronized (lock) { /************ 每次售票前進行判斷 ************/ if (ticket == 0) { break; } /************ 模擬售票的網絡延遲 ************/ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } /************ 符合條件后進行售票 ************/ System.out.println(Thread.currentThread().getName() + " 售出了第 " + ticket-- + " 張票"); } } } }
上面的代碼在主線程中開啟了 4 個線程,也就是同時有 4 個售票員在窗口進行售票。為了保證 4 個售票員操作的是同一張票,所以 ticket 在初始化的時候將其設置為 static , 具體再看 TicketSeller 的 run() 方法,每個售票員在進行售票之前都會去查詢 ticket 的剩余數量,當 ticket 的數量等於零的時候停止售票,如果剩余票數不等於零,則進行售票。在這個過程中有 2 個操作(1:查詢剩余票數;2:售票)對應了同一個 ticket,這時如果不采用同步代碼塊就會產生線程安全的問題。
為什么會產生線程安全問題,在分析開始之前首先要明確一個問題:那就是一個 CPU 在同一時間只能執行一個線程,體現給用戶多任務的假象是通過 CPU 在各個線程之間進行高速切換來實現的。 下面來分析售票過程,因為每個售票員在進行售票前都會去查詢剩余票數,如果 1 號售票員在查詢完剩余票數后由於網絡延遲而沒有及時將所查詢的票賣出,這時 CPU 切換到了 2 號售票員,2 號售票員同樣對剩余票數進行查詢,查詢的結果里包含 1 號售票員查詢過但沒賣出的票,如果票還有剩余 2 號售票員會進行賣票操作並將票數減一。這時我們假設只剩下一張票,1 號售票員進行查詢后發現票還有剩余,但由於網絡延遲沒來得及出票,這時 CPU 切換到了 2 號售票員,2 號售票員同樣查詢剩余票數為一,2 號售票員進行了售票操作並將票數減一,剩余票數為零。操作完畢后 CPU 切換到了 1 號售票員,由於之前 1 號售票員已經查詢過剩余票數為一,所以 1 號售票員在重新獲取 CPU 資源后不會重新進行剩余票數的判斷,而是直接進入賣票的環節,這個過程中 1 號售票員跳過了對剩余票數為零判斷的操作,最終導致賣出負數的票數。
通 過上面的分析,我們這時就會將剩余票數的查詢和賣票操作統一到一起,不允許在一個售票員完成這兩項操作的過程中插入另一個售票員的查詢操作。這時候我們就 需要將查詢和售票操作進行同步,使其原子化合為一個操作,這樣對於每一個售票員來說都將 2 個操作看作了一個操作來對待。拿上面的假設為例,1 號售票員查詢到剩余票數為一,等待網絡延遲的過程中, CUP 切換到了 2 號售票員,由於查詢和賣票進行了同步(查詢和賣票在同步后具有原子性,2 個操作不可被分割),2 號售票員雖然獲取到了 CPU 資源,但是無法打斷 1 號售票員的操作,2 號售票員只能等待 CPU 切換到 1 號售票員處理完賣票操作。這時 1 號售票員重新獲得 CPU 資源直接進行賣票操作並將票數減一,剩余票數為零。CPU 切換到 2 號售票員,2 號售票員進行剩余票數查詢操作,發現剩余票數為零,則停止售票。