天天看點

有關線程安全的探讨--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. 靜态方法
  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++ 是沒有問題的),盡量少地使用靜态變量,更不要嘗試對靜态變量的值進行修改

後記:本文作者在并發領域隻是新手,學習實踐中偶有所得特此為記,可能出現錯漏,還請多多指教,一定虛心學習

繼續閱讀