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

個人代碼中已經屢次使用了線程,而後還很是喜歡使用聽說是線程不安全的靜態方法,而後又看到不少地方最容易提的問題就是這個東西線程不安全
 
因而我難免產生了如下幾個亟待解決的問題:
  1. 什麼樣的代碼是天生線程安全的?而不用加鎖
  2. 線程是否安全的本質是什麼?
  3. 什麼是快速把一段代碼變成線程安全的通用方法
  4. final static 單例 線程安全 之間的關係
 
一、首先咱們知道,若是線程只是執行本身內部的代碼(其實也是使用一些對象的方法,可是是局部變量,那麼就線程安全),那必定是線程安全的
  1. 這句話嚴格一些說能夠是這樣:線程使用在run( )方法中實例化的局部變量的方法,是線程安全的
 
二、那下一個問題就是,一個線程能調用哪些代碼,或者說能訪問到哪些東西?訪問這些東西的安全性如何?
一個線程能訪問哪些東西,應該是跟它建立的環境有關,線程啓動從這個意義上有兩個方式
  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. 匿名類中更加危險,要謹慎調用
 
三、線程是否安全的本質是什麼?什麼是快速把一段代碼變成線程安全的通用方法?
而所謂的線程安全性具體又指的是什麼
  1. 不能同時被多個線程調用
    1. 這個是最普通的,也是常規上咱們的線程安全的含義
    2. 這個問題能夠經過加鎖解決
  2. 不能被多個線程調用(不一樣時也不行)
    1. 這個在第一類的程度上有所增長,不是經常使用的狀況,可能你不只是要使用變量,你還須要記錄變量的值
    2. 這個問題通常是把相關變量變成ThreadLocal的
  3. 不能被超過一次地調用
    1. 這個的狀況更加特殊
    2. 通常使用單例模式解決
 
四、final static 單例 線程安全 之間的關係
  1. final
    1. 意思是,這個對象的值(基本類型就是值,引用類型是引用地址),不會再被改變
    2. 與線程安全的關係,如上文,必定程度上能使某些變量強制變得線程安全
  2. static
    1. 意思是,這個對象是一個全局變量了,你能夠在多個地方,多個線程中調用到它,並且調用的是同一個它
    2. 與線程安全的關係,通常這種的變量很容易形成線程不安全的狀況
  3. 單例
    1. 這首先是一種特殊的需求,就是某個類的實例在JVM中只能存在一個,跟前面的static,線程安全都不同
    2. 與線程安全的關係。實現單例須要考慮複雜的多線程的狀況,這個東西須要線程安全
 
五、舉個例子
常被說的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對象,這個對象存儲值的變量被多個線程同時使用(修改),形成了混亂
 
六、OK,說了這麼多,那知道了這些以後對我寫代碼有哪些指導做用呢?
  1. 你確定是喜歡使用匿名內部類的,以這個爲基礎
    1. 注意若是是調用所在方法中的局部變量,儘可能不要繞過final機制,若是須要繞過,並且會對這個局部變量進行修改的話,那必定是知道不會多個這樣的線程同時運行(好比做爲UI主線程外的一個子線程,這個子線程只會有一個)
    2. 不要嘗試修改不是在本身內部實例化出的對象的值(只能改局部變量的值)(儘可能使用局部變量)
    3. 你還喜歡使用靜態工具方法,全部的靜態工具方法中使用變量儘可能使用局部變量(for循環中的i++ 是沒有問題的),儘可能少地使用靜態變量,更不要嘗試對靜態變量的值進行修改
 
後記:本文做者在併發領域只是新手,學習實踐中偶有所得特此爲記,可能出現錯漏,還請多多指教,必定虛心學習
相關文章
相關標籤/搜索