並發編程學習筆記之線程安全(一)


最近在復習、整理之前學習的多線程的知識,本着燃燒自己,照亮他人的想法,把自己整理的一些關於多線程的學習筆記、心得分享給大家.

 

博主准備把自己關於多線程的學習筆記寫成三個部分分享給大家: 基礎、實戰、測試&優化

 

這三個部分是一環扣一環的.

 

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     //程序員的等級
 3     private int level;  4     //技能庫
 5     public Map<String,String> skills;  6 
 7     //工資
 8     private int sal;  9 
10     public void updateSal(String multithreading){ 11         // 如果有會多線程這個技術
12         if (multithreading.equals(skills.get(multithreading))){ 13             //根據你的等級升職加薪操作..
14             sal = level * sal; 15         }else{ 16             //如果不會多線程,學習多線程,更改等級為中級
17  skills.put(multithreading,multithreading); 18             level = 2; 19             //根據等級加薪,..
20  updateSal(multithreading); 21  } 22  } 23 }

 

 

 

 員工類有個方法,根據你會不會多線程技術來提高你的薪水,如果你的技能庫里有多線程技術,執行加薪操作,如果沒有會讓你學習,給你的技能庫加上這個技能,並且提高你的等級,但是在一些極端的情況下會出現問題,線程A走到17行添加完技能又沒修改等級的時候,可能有另一個線程重新調用方法,通過了12行的驗證,但是等級沒有改變,執行加薪操作的時候是按照等級的過期值執行的.

 

 

這里我們就要注意了,當不變約束涉及到多個變量的時候,要原子的更新它們.在這個方法上加鎖就又可以保證這個方法是線程安全的了.

 

 最后給大家介紹一下原子變量atomic,使用原子變量也可以把自增操作變為原子的.

private AtomicLong num = new AtomicLong(0); public void doSomething(){ //do something
 num.incrementAndGet(); }

 

 

好了關於線程安全和鎖就為大家簡單的介紹到這里,博主下一篇會更新關於安全發布對象的知識,這兩篇結合起來就可以幫助我們構建線程安全的類了.

 

如果大家有任何疑問或者對博主有什么建議,歡迎大家留下評論.樓主一定會盡快回復.本期分享就到這里,我們下期再見吧!

            


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM