從零開始學多執行緒之執行緒安全(一)
最近在複習、整理之前學習的多執行緒的知識,本著燃燒自己,照亮他人 的想法,把自己整理的一些關於多執行緒的學習筆記、心得分享給大家.
博主準備把自己關於多執行緒的學習筆記寫成三個部分分享給大家: 基礎、實戰、測試&優化
這三個部分是一環扣一環的.
1.基礎: 多執行緒操作的物件必須是執行緒安全的,所以構建執行緒安全的物件是一切的基礎.這一部分講的就是如何構建執行緒安全的類,和一些多執行緒的基礎知識.
2. 實戰: 構建好了執行緒安全的類,我們就以用執行緒/執行緒池,去構建我們的併發程式了,如何執行任務?如何關閉執行緒池?如何擴充套件執行緒池?這裡都會給你答案
3. 測試&優化: 構建好的程式會不會發生死鎖? 如何優化程式? 如何知道執行的結果是否正確? 這一部分會 一 一為你解答.
好了廢話不多說,本篇部落格是系列的第一篇,我們來講述一下執行緒安全.
執行緒安全
在多執行緒環境下,保證執行緒訪問的資料的安全格外重要 .編寫執行緒安全的程式碼,本質上就管理狀態的訪問 ,而且通常是共享的 、可變的 狀態.
狀態:可以理解為物件的成員變數.
共享: 是指變數可以被多個執行緒訪問
可變: 是指變數的值在生命週期內可以改變.
保證執行緒安全就是要在不可控制的併發訪問中保護資料.
如果物件在多執行緒環境下無法保證執行緒安全,就會導致髒資料和其他不可預期的後果
有很多在單執行緒環境下執行良好的程式碼,在多執行緒環境下卻有問題.例如自增操作:
public class Increment { private int num = 0; public void doSomething(){ //do something num++; } }
每次呼叫doSomething()方法的時候,num都會執行自增操作.但是在多執行緒環境下,這段程式碼是有問題的.
原因在於num++並不是原子操作,而是由三個離散操作組合而來的 :"讀-改-寫",讀取當前的值,加1,寫入變數.
可能會出現某一時刻,兩個執行緒同時讀到num的數值,然後分別+1,分別寫入.這樣其中一次計數就不存在了.
有一個專門形容這類情況的名詞,叫競爭條件
當計算的正確性依賴於執行時相關的時序或者多執行緒的交替時 ,會產生競爭條件.
我對競爭條件的理解就是,多個執行緒同時訪問一段程式碼,因為順序的問題,可能導致結果不正確,這就是競爭條件.
"檢查-再執行",也是一種競爭條件.
public class Singleton { private Singleton singleton; private Singleton() { } public Singleton getSingleton(){ if(singleton == null){ singleton = new Singleton(); } return singleton; } }
看這個例子,我們把構造方法宣告為private的這樣就只能通過getSingleton()來獲得這個物件的例項了,先檢查 這個物件是否被例項化了,如果沒有,那就例項化並返回,但是可能同一時刻兩個執行緒同時通過了條件判斷,這樣就產生了兩個物件的例項 .
問題已經很清楚了,那麼如何解決問題呢?
Java提供了synchronized(同步) 關鍵字.只要是使用了synchronized關鍵字修飾的方法,就被加鎖了,synchronized鎖是互斥鎖,同一時間只能有一個執行緒佔有鎖,其他物件想要獲得鎖只能等到佔有鎖的執行緒釋放鎖.
我們來修改一下上面的程式碼,使它們成為執行緒安全的:
private int num = 0; public synchronized void doSomething(){ //do something num++; }
public synchronized void test(){ if (state){ //做一些事 }else{ // 做另外一些事 } }
好了,現在它們又是執行緒安全的了.
這種方式雖然很簡單,但是由於synchronized塊包住的程式碼都會順序的執行,有時會導致令人無法忍受的響應速度
決定synchronized塊的大小需要權衡各種設計要求,包括安全性 、簡單性 和效能 ,其中安全性是絕對不能妥協的 ,而簡單性和效能又是互相影響的 (將整個方法宣告為synchronized很簡單,但是響應速度不太好,將同步塊的程式碼縮小,可能很麻煩,但是效能變好了).
那麼在簡單性和效能之間我們要如何取捨呢? 這裡有個原則: 通常簡單性與效能之間是相互牽制的,實現一個同步策略時,不要過早地為了效能而犧牲簡單性(這是對安全性潛在的妥協).
如有有耗時長的操作(I/O啊,長時間的計算啊),切記不能放在鎖裡,否則可能引發活躍度(死鎖)與效能(響應慢)的風險.
下面我們再看一段程式碼:
1 public class Employees { 2//程式設計師的等級 3private int level; 4//技能庫 5public Map<String,String> skills; 6 7//工資 8private int sal; 9 10public void updateSal(String multithreading){ 11// 如果有會多執行緒這個技術 12if (multithreading.equals(skills.get(multithreading))){ 13//根據你的等級升職加薪操作.. 14sal = level * sal; 15}else{ 16//如果不會多執行緒,學習多執行緒,更改等級為中級 17skills.put(multithreading,multithreading); 18level = 2; 19//根據等級加薪,.. 20updateSal(multithreading); 21} 22} 23 }
員工類有個方法,根據你會不會多執行緒技術來提高你的薪水,如果你的技能庫裡有多執行緒技術,執行加薪操作,如果沒有會讓你學習,給你的技能庫 加上這個技能,並且提高你的等級,但是在一些極端的情況下會出現問題,執行緒A走到17行新增完技能又沒修改等級的時候,可能有另一個執行緒重新呼叫方法,通過了12行的驗證,但是等級沒有 改變,執行加薪操作的時候是按照等級的過期值執行的.
這裡我們就要注意了,
當不變約束涉及到多個變數的時候,要原子的更新它們.
在這個方法上加鎖就又可以保證這個方法是執行緒安全的了.
最後給大家介紹一下原子變數atomic,使用原子變數也可以把自增操作變為原子的.
private AtomicLong num = new AtomicLong(0); public void doSomething(){ //do something num.incrementAndGet(); }
好了關於執行緒安全和鎖就為大家簡單的介紹到這裡,博主下一篇會更新關於安全釋出物件 的知識,這兩篇結合起來就可以幫助我們構建 執行緒安全的類了.
如果大家有任何疑問或者對博主有什麼建議,歡迎大家留下評論.樓主一定會盡快回復.本期分享就到這裡,我們 下期再見吧!
public class Employees { //程式設計師的等級 private int level; //技能庫 public Map<String,String> skills; //工資 private int sal; public void updateSal(String multithreading){ // 如果有會多執行緒這個技術 if (multithreading.equals(skills.get(multithreading))){ //根據你的等級升職加薪操作.. sal = level * sal; }else{ //如果不會多執行緒,學習多執行緒,更改等級為中級 skills.put(multithreading,multithreading); level = 2; //根據等級加薪,.. updateSal(multithreading); } } }