執行緒安全什麼是執行緒安全
執行緒安全就是指的多條執行緒操作共享變數時會出現資料不安全問題。使用多執行緒模擬4個視窗售票,4個視窗就是四條執行緒,4個視窗賣票的過程都是一樣的,因此使用Runnable任務模擬。而4個視窗共同賣100張票,因此每個任務物件共享同100張票
由於Java的執行緒排程機制是搶佔式,當在賣第一百張票時四個執行緒幾乎同時獲得CPU資源去執行任務,因此控制檯的四個視窗都在賣第一百張票。而四個執行緒都會對ticketCount進行減1操作,因此一輪下來tickeCount是96,這樣就造成了賣重複票和漏票,重複票就是第一百張票賣了四次,漏票就是第99張,98張,97張都沒有賣出去。然後當ticketCount為1的時候,四條執行緒幾乎同時獲得CPU資源去執行任務,此時ticketCount會依次被四個執行緒減1,因此ticketCount等於-3,但是while迴圈中有個條件判斷,當ticketCount小於1時退出迴圈,因此控制檯最後輸出的票是-2張,此時便出現了負數票。
以上都是由多個執行緒同時共享一個變數(這裡指的是ticketCount)時併發導致執行緒安全問題。
synchronized關鍵字由於Java的執行緒排程採用搶佔式機制,當執行緒在執行任務時被其他執行緒打斷。當使用多個執行緒訪問同一資源時,且多個執行緒對資源有寫的操作,就容易出現執行緒安全問題。Java提供了synchronized同步機制解決該問題。synchronized關鍵字表示同步的意思,它可以對程式碼塊進行同步,即將多行程式碼當成一個整體,當一個執行緒進入到程式碼塊時,會等到全部執行完畢後,其他執行緒才會執行。這樣可以保證程式碼塊整體被一個執行緒執行完畢。synchronized被稱為重量級的鎖,也是悲觀鎖,效率比較低。在使用synchronized時主要有兩種方式
同步程式碼塊synchronized關鍵字可以用於方法的某個區塊中,表示只對這個區塊的資源實行互斥訪問。當執行緒執行到同步程式碼塊時加鎖,而執行緒執行完同步程式碼塊釋放鎖。synchronized同步程式碼塊的語法格式
synchronized(鎖物件){ 需要同步的程式碼塊}
鎖物件表示在物件上標記了一個鎖,鎖物件可以是任意型別,多個執行緒物件要使用同一把鎖,因為多條執行緒要實現同步,那麼多條執行緒的鎖物件要一致。
例如在TicketWindowSynchronizedBlock類中建立一個鎖物件
/** * 同步鎖物件 */ private static final Object lock =new Object();
然後在TicketWindowSynchronizedBlock類的run方法中使用synchonized同步程式碼塊,這樣便可以解決執行緒安全的問題。
@Override public void run() { //同步程式碼塊 synchronized (lock){ // 死迴圈買票 while (true) { // 票賣完了退出迴圈 if (ticketCount < 1) { break; } try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } // 每賣出一張 log.info(Thread.currentThread().getName() + "正在出售第{}張票", this.ticketCount); // 數量減1 ticketCount--; } } }
在同步程式碼塊測試類ThreadSynchronizedBlockTest中依次建立共享的任務物件和四個執行緒物件並啟動執行緒
因為TicketWindowSynchronizedBlock在測試類ThreadSynchronizedBlockTest只建立了一次該類的物件,建議使用final修飾一下。
在synchronized同步程式碼塊的物件鎖中可以使用this關鍵字,也可以實現執行緒同步。
同步方法同步方法表示使用synchronized關鍵字修飾的方法,同步方法可以保證當A執行緒執行任務時,其他執行緒只能在方法外等著。當執行緒執行到同步方法時加鎖,而執行緒執行完同步方法放鎖。
同步方法的語法格式
訪問修飾符 synchronized 返回值 方法名(形參列表){ 需要同步的程式碼塊}
在TicketWindowSynchronizedMethod中將賣票的實現從run方法中抽取成一個同步方法sellTickets(),該方法被run()方法呼叫
同步的方法可以是靜態方法或者是非靜態方法
當同步的方法是靜態方法時,同步鎖物件是同步方法所在類的位元組碼物件,例如這裡的TicketWindow.class當同步的方法是非靜態方法時,同步鎖物件是this如果A執行緒使用了同步程式碼塊,B執行緒使用了同步方法,A,B執行緒需要實現同步的話,那麼A,B執行緒的鎖物件必須一致。而同步方法的物件是無法修改的,所以只能修改同步程式碼塊的鎖物件寫成和同步方法的鎖物件一樣。
當B執行緒的同步方法是非靜態方法時,A執行緒同步程式碼塊的鎖物件只能是B執行緒呼叫方法的物件定義一個Order類,該類有個非靜態方法doOrder()
@Log4j2class Order { /** 非靜態同步方法 */ public synchronized void doOrder() { log.info(Thread.currentThread().getName() + "使用者註冊"); log.info(Thread.currentThread().getName() + "使用者登入"); log.info(Thread.currentThread().getName() + "使用者瀏覽商品"); log.info(Thread.currentThread().getName() + "使用者新增到購物車"); log.info(Thread.currentThread().getName() + "使用者支付"); log.info(Thread.currentThread().getName() + "使用者提交訂單"); }}
然後在測試類ThreadSynchronizedNonStaticMethodSynchronizedBlockTest中建立A,B兩個執行緒,A執行緒在run方法中執行和B執行緒一樣的業務B執行緒在run方法中呼叫Order物件的doOrder()方法。此時A執行緒的同步程式碼塊的鎖物件只能是B執行緒呼叫方法的物件,也就是Order物件
當B執行緒的同步方法是靜態方法時,A執行緒的同步程式碼塊的鎖物件是B執行緒所在類的位元組碼物件首先定義一個TaobOrder類,該類中包含一個靜態同步方法doTaobaoOrder()
@Log4j2class TaoBaoOrder { /** 靜態同步方法 */ public static synchronized void doTaoBaoOrder() { log.info(Thread.currentThread().getName() + "淘寶使用者註冊"); log.info(Thread.currentThread().getName() + "淘寶使用者登入"); log.info(Thread.currentThread().getName() + "淘寶使用者瀏覽商品"); log.info(Thread.currentThread().getName() + "淘寶使用者新增到購物車"); log.info(Thread.currentThread().getName() + "淘寶使用者支付"); log.info(Thread.currentThread().getName() + "淘寶使用者提交訂單"); }}
然後在測試類ThreadStaticSynchronizedMethodSynchronizedBlockTest中依次建立兩個執行緒並啟動
由於執行緒B呼叫的是TaobaoOrder的靜態同步方法doTaoOrder(),因此執行緒A需要使用同步B執行緒所在類的子節碼物件,也就是TaobaoOrder.class
Lock鎖java.util.concurrent.locks.Lock機制提供了比synchronized程式碼塊和synchronized方法更靈活的鎖操作,同步程式碼塊,同步方法Lock鎖都有,除此之外更加強大和靈活。Lock鎖也被稱為同步鎖,提供了加鎖和釋放鎖的方法
public void lock():加同步鎖public void unlock():釋放同步鎖在TicketWindowLock 類中建立lock物件,實現類採用ReentrantLock,然後在run方法的入口加鎖,在run方法的最後一行釋放鎖。