有關線程安全的探討--final、static、單例、線程安全


我的代碼中已經多次使用了線程,然后還非常喜歡使用據說是線程不安全的靜態方法,然后又看到很多地方最容易提的問題就是這個東西線程不安全
 
於是我不免產生了以下幾個亟待解決的問題:
  1. 什么樣的代碼是天生線程安全的?而不用加鎖
  2. 線程是否安全的本質是什么?
  3. 什么是快速把一段代碼變成線程安全的通用方法
  4. final static 單例 線程安全 之間的關系
 
1、首先我們知道,如果線程只是執行自己內部的代碼(其實也是使用一些對象的方法,但是是局部變量,那么就線程安全),那一定是線程安全的
  1. 這句話嚴格一些說可以是這樣:線程使用在run( )方法中實例化的局部變量的方法,是線程安全的
 
2、那下一個問題就是,一個線程能調用哪些代碼,或者說能訪問到哪些東西?訪問這些東西的安全性如何?
一個線程能訪問哪些東西,應該是跟它創建的環境有關,線程啟動從這個意義上有兩個方式
  1. 繼承並重寫一個Thread類,然后在使用的時候實例化這個類,最后調用這個對象實例的start方法啟動
    1. 這種方式的run方法中,其實能調用的東西就很少了
      1. 你在繼承時加的成員變量。(完全不會有線程是否安全的問題,因為這個類就一個run()方法是多線程方法,就跟在run()中實例化的局部變量一樣)
      2. 通過構造方法從外面傳入的變量。(這種方式需要警惕!因為傳遞的是引用,如果你在線程中對這個引用指向的內容進行修改,那么會影響到原來的東西!)
      3. 使用其他的代碼段(方法)
        1. 靜態方法(類似單例模式)
        2. 實例方法——通過實例對象
      4. 使用其他的對象
        1. 靜態對象
        2. 實例對象
      5. (兩面兩大點中,使用實例方法和實例對象都是線程安全的。而使用靜態方法和靜態對象時,是一定會沖突的)
    2. 所以總結一下,這種方式中
      1. 線程安全的有
        1. 在繼承時加的成員變量
        2. 實例化其他對象,使用這個對象,或者使用這個對象的方法
      2. 不安全的有
        1. 通過構造方法從外面傳入的變量
        2. 靜態方法
        3. 靜態對象
  2. 使用匿名內部類
    1. 這種方式,在上一種方式的繼承上,只少了構造方法的方式,然后多了好幾種危險的方式, 需要注意
      1. 所處方法中的局部變量
        1. 這個值得一提,本來這項是肯定會線程不安全,而且非常常用,所以危險指數五顆星的,但是JAVA特地為此限定了一條規則,就是這樣的局部變量必須是final的,不能修改,於是這個就變得非常安全了
        2. 但這條其實可以通過引用類型繞過,就是另一回事了,其實也說明了它的不安全
      2. 所處類中的屬性
      3. 所處類中的方法
 
另外,經過查閱資料,上面提到的所有跟方法有關的可能線程不安全的情況,其實都不是完全不安全
方法是否線程安全取決於方法中是否使用了全局變量,方法本身是在JAVA中是線程安全的,每個線程會有一個副本,但是在使用變量的時候就可能有問題
比如多線程中使用靜態方法是否有線程安全問題?這要看靜態方法是是引起線程安全問題要看在靜態方法中是否使用了靜態成員
 
總結一下,線程是否安全總的來說情況比較復雜,但是有這些特點
  1. 方法本身不會有問題,問題的根源是(普通方法、靜態方法)方法使用了變量(相對全局變量,或者說叫可共享變量)【比如靜態成員、類屬性等等】
  2. 匿名類中更加危險,要謹慎調用
 
3、線程是否安全的本質是什么?什么是快速把一段代碼變成線程安全的通用方法?
而所謂的線程安全性具體又指的是什么
  1. 不能同時被多個線程調用
    1. 這個是最普通的,也是常規上我們的線程安全的含義
    2. 這個問題可以通過加鎖解決
  2. 不能被多個線程調用(不同時也不行)
    1. 這個在第一類的程度上有所增加,不是常用的情況,可能你不僅是要使用變量,你還需要記錄變量的值
    2. 這個問題一般是把相關變量變成ThreadLocal的
  3. 不能被超過一次地調用
    1. 這個的情況更加特殊
    2. 一般使用單例模式解決
 
4、final static 單例 線程安全 之間的關系
  1. final
    1. 意思是,這個對象的值(基本類型就是值,引用類型是引用地址),不會再被改變
    2. 與線程安全的關系,如上文,一定程度上能使某些變量強制變得線程安全
  2. static
    1. 意思是,這個對象是一個全局變量了,你可以在多個地方,多個線程中調用到它,而且調用的是同一個它
    2. 與線程安全的關系,一般這種的變量很容易造成線程不安全的情況
  3. 單例
    1. 這首先是一種特殊的需求,就是某個類的實例在JVM中只能存在一個,跟前面的static,線程安全都不一樣
    2. 與線程安全的關系。實現單例需要考慮復雜的多線程的情況,這個東西需要線程安全
 
5、舉個例子
常被說的SimpleDateFormat是非線程安全的,為什么線程不安全,來分析一下
  1. 因為創建一個 SimpleDateFormat實例的開銷比較昂貴,解析字符串時間時頻繁創建生命周期短暫的實例導致性能低下
    1. 在程序中我們應當盡量少的創建SimpleDateFormat 實例,因為創建這么一個實例需要耗費很大的代價。在一個讀取數據庫數據導出到excel文件的例子當中,每次處理一個時間信息的時候,就需要創建一個SimpleDateFormat實例對象,然后再丟棄這個對象。大量的對象就這樣被創建出來,占用大量的內存和 jvm空間。
  2. 於是,就很容易想到,將 SimpleDateFormat定義為靜態類變量,貌似能解決這個問題
  3. 於是這就引出了,SimpleDateFormat是非線程安全的,這樣的使用方式可能引發並發線程安全問題
那為什么會有這個問題呢?來看看SimpleDateFormat本身
  1. SimpleDateFormat類內部有一個Calendar對象引用,它用來儲存和這個SimpleDateFormat對象(叫sdf)相關的日期信息,例如sdf.parse(dateStr), sdf.format(date)
  2. 諸如此類的方法參數傳入的日期相關String, Date等等, 都是交友Calendar引用來儲存的
  3. 這樣就會導致一個問題:如果你的sdf是個static的, 那么多個thread 之間就會共享這個sdf, 同時也是共享這個Calendar引用, 並且, 觀察 sdf.parse() 方法,你會發現有如下的調用:
    1. Date parse() {
    2.   calendar.clear(); // 清理calendar
    3.   ... // 執行一些操作, 設置 calendar 的日期什么的
    4.   calendar.getTime(); // 獲取calendar的時間
    5. }
  4. 這里會導致的問題就是:如果 線程A 調用了 sdf.parse(), 並且進行了 calendar.clear()后還未執行calendar.getTime()的時候,線程B又調用了sdf.parse(), 這時候線程B也執行了sdf.clear()方法, 這樣就導致線程A的的calendar數據被清空了(實際上A,B的同時被清空了). 又或者當 A 執行了calendar.clear() 后被掛起, 這時候B 開始調用sdf.parse()並順利i結束, 這樣 A 的 calendar內存儲的的date 變成了后來B設置的calendar的date
上邊是復雜的具體的原因,而這個原因簡單說就是,在線程中調用了一個static對象,這個對象存儲值的變量被多個線程同時使用(修改),造成了混亂
 
6、OK,說了這么多,那知道了這些之后對我寫代碼有哪些指導作用呢?
  1. 你肯定是喜歡使用匿名內部類的,以這個為基礎
    1. 注意如果是調用所在方法中的局部變量,盡量不要繞過final機制,如果需要繞過,而且會對這個局部變量進行修改的話,那一定是知道不會多個這樣的線程同時運行(比如作為UI主線程外的一個子線程,這個子線程只會有一個)
    2. 不要嘗試修改不是在自己內部實例化出的對象的值(只能改局部變量的值)(盡量使用局部變量)
    3. 你還喜歡使用靜態工具方法,所有的靜態工具方法中使用變量盡量使用局部變量(for循環中的i++ 是沒有問題的),盡量少地使用靜態變量,更不要嘗試對靜態變量的值進行修改
 
后記:本文作者在並發領域只是新手,學習實踐中偶有所得特此為記,可能出現錯漏,還請多多指教,一定虛心學習


免責聲明!

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



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