線程安全和線程不安全理解

線程安全就是多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其餘線程不能進行訪問直到該線程讀取完,其餘線程纔可以使用。不會出現數據不一致或者數據污染。      線程不安全就是不提供數據訪問保護,有可能出現多個線程前後更改數據形成所獲得的數據是髒數據html

概念:java

        若是你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。若是每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也和預期的是同樣的,就是線程安全的。設計模式

        或者說:一個類或者程序所提供的接口對於線程來講是原子操做或者多個線程之間的切換不會致使該接口的執行結果存在二義性,也就是說咱們不用考慮同步的問題。緩存

        線程安全問題都是由全局變量及靜態變量引發的。安全

        若每一個線程中對全局變量、靜態變量只有讀操做,而無寫操做,通常來講,這個全局變量是線程安全的;如有多個線程同時執行寫操做,通常都須要考慮線程同步,不然的話就可能影響線程安全。多線程

 

安全性:併發

        好比一個 ArrayList 類,在添加一個元素的時候,它可能會有兩步來完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。高併發

在單線程運行的狀況下,若是 Size = 0,添加一個元素後,此元素在位置 0,並且 Size=1;性能

        而若是是在多線程狀況下,好比有兩個線程,線程 A 先將元素存放在位置 0。可是此時 CPU 調度線程A暫停,線程 B 獲得運行的機會。線程B也向此 ArrayList 添加元素,由於此時 Size 仍然等於 0 (注意哦,咱們假設的是添加一個元素是要兩個步驟哦,而線程A僅僅完成了步驟1),因此線程B也將元素存放在位置0。而後線程A和線程B都繼續運行,都增長 Size 的值。測試

        那好,咱們來看看 ArrayList 的狀況,元素實際上只有一個,存放在位置 0,而 Size 卻等於 2。這就是「線程不安全」了。

 

安全性:

        線程安全性不是一個非真即假的命題。 Vector 的方法都是同步的,而且 Vector 明確地設計爲在多線程環境中工做。可是它的線程安全性是有限制的,即在某些方法之間有狀態依賴(相似地,若是在迭代過程當中 Vector 被其餘線程修改,那麼由 Vector.iterator() 返回的 iterator會拋出ConcurrentModifiicationException)。

        對於 Java 類中常見的線程安全性級別,沒有一種分類系統可被普遍接受,不太重要的是在編寫類時儘可能記錄下它們的線程安全行爲。

        Bloch 給出了描述五類線程安全性的分類方法:不可變、線程安全、有條件線程安全、線程兼容和線程對立。只要明確地記錄下線程安全特性,那麼您是否使用這種系統都不要緊。這種系統有其侷限性 -- 各種之間的界線不是百分之百地明確,並且有些狀況它沒照顧到 -- 可是這套系統是一個很好的起點。這種分類系統的核心是調用者是否能夠或者必須用外部同步包圍操做(或者一系列操做)。下面幾節分別描述了線程安全性的這五種類別。

 

不可變

        不可變的對象必定是線程安全的,而且永遠也不須要額外的同步 。由於一個不可變的對象只要構建正確,其外部可見狀態永遠也不會改變,永遠也不會看到它處於不一致的狀態。Java 類庫中大多數基本數值類如 Integer 、 String 和 BigInteger 都是不可變的。

        須要注意的是,對於Integer,該類不提供add方法,加法是使用+來直接操做。而+操做是不具線程安全的。這是提供原子操做類AtomicInteger的原。

 

線程安全

        線程安全的對象具備在上面「線程安全」一節中描述的屬性 -- 由類的規格說明所規定的約束在對象被多個線程訪問時仍然有效,無論運行時環境如何排線程都不須要任何額外的同步。這種線程安全性保證是很嚴格的 -- 許多類,如 Hashtable 或者 Vector 都不能知足這種嚴格的定義。

 

有條件的

        有條件的線程安全類對於單獨的操做能夠是線程安全的,可是某些操做序列可能須要外部同步。條件線程安全的最多見的例子是遍歷由 Hashtable 或者 Vector 或者返回的迭代器 -- 由這些類返回的 fail-fast 迭代器假定在迭代器進行遍歷的時候底層集合不會有變化。爲了保證其餘線程不會在遍歷的時候改變集合,進行迭代的線程應該確保它是獨佔性地訪問集合以實現遍歷的完整性。一般,獨佔性的訪問是由對鎖的同步保證的 -- 而且類的文檔應該說明是哪一個鎖(一般是對象的內部監視器(intrinsic monitor))。

        若是對一個有條件線程安全類進行記錄,那麼您應該不只要記錄它是有條件線程安全的,並且還要記錄必須防止哪些操做序列的併發訪問。用戶能夠合理地假設其餘操做序列不須要任何額外的同步。

 

線程兼容

        線程兼容類不是線程安全的,可是能夠經過正確使用同步而在併發環境中安全地使用。這可能意味着用一個 synchronized 塊包圍每個方法調用,或者建立一個包裝器對象,其中每個方法都是同步的(就像 Collections.synchronizedList() 同樣)。也可能意味着用 synchronized 塊包圍某些操做序列。爲了最大程度地利用線程兼容類,若是全部調用都使用同一個塊,那麼就不該該要求調用者對該塊同步。這樣作會使線程兼容的對象做爲變量實例包含在其餘線程安全的對象中,從而能夠利用其全部者對象的同步。

        許多常見的類是線程兼容的,如集合類 ArrayList 和 HashMap 、 java.text.SimpleDateFormat 、或者 JDBC 類 Connection 和 ResultSet 。

 

線程對立

        線程對立類是那些無論是否調用了外部同步都不能在併發使用時安全地呈現的類。線程對立不多見,當類修改靜態數據,而靜態數據會影響在其餘線程中執行的其餘類的行爲,這時一般會出現線程對立。線程對立類的一個例子是調用 System.setOut() 的類。

當咱們查看JDK API的時候,總會發現一些類說明寫着,線程安全或者線程不安全,好比說到StringBuilder中,有這麼一句,「將StringBuilder 的實例用於多個線程是不安全的。若是須要這樣的同步,則建議使用StringBuffer。」,提到StringBuffer時,說到「StringBuffer是線程安全的可變字符序列,一個相似於String的字符串緩衝區,雖然在任意時間點上它都包含某種特定的字符序列,但經過某些方法調用能夠改變該序列的長度和內容。可將字符串緩衝區安全地用於多個線程。能夠在必要時對這些方法進行同步,所以任意特定實例上的全部操做就好像是以串行順序發生的,該順序與所涉及的每一個線程進行的方法調用順序一致」。StringBuilder是一個可變的字符序列,此類提供一個與StringBuffe兼容的API,但不保證同步。該類被設計用做StringBuffer的一個簡易替換,用在字符串緩衝區被單個線程使用的時候(這種狀況很廣泛)。若是可能,建議優先採用該類,由於在大多數實現中,它比StringBuffer要快。將StringBuilder的實例用於多個線程是不安全的,若是須要這樣的同步,則建議使用StringBuffer。

   根據以上JDK文檔中對StringBuffer和StringBuilder的描述,獲得對String、StringBuilder與StringBuffer三者使用狀況的總結:
   一、若是要操做少許的數據用String
   二、單線程操做字符串緩衝區下操做大量數據StringBuilder
   三、多線程操做字符串緩衝區下操做大量數據StringBuffer

   那麼下面手動建立一個線程不安全的類,而後在多線程中使用這個類,看看有什麼效果。

public class Count {  
    private int num;  
    public void count() {  
        for(int i = 1; i <= 10; i++) {  
            num += i;  
        }  
        System.out.println(Thread.currentThread().getName() + "-" + num);
    }  
}  

在這個類中的count方法計算1一直加到10的和,並輸出當前線程名和總和,咱們指望的是每一個線程都會輸出55。

public class ThreadTest {  
    public static void main(String[] args) {  
        Runnable runnable = new Runnable() {  
            Count count = new Count();  
            public void run() {  
                count.count();  
            }  
        };  

        for(int i = 0; i < 10; i++) {  
            new Thread(runnable).start();  
        }  
    }  
}  


   這裏啓動了10個線程,看一下輸出結果:

Thread-0-55  
Thread-1-110  
Thread-2-165  
Thread-4-220  
Thread-5-275  
Thread-6-330  
Thread-3-385  
Thread-7-440  
Thread-8-495  
Thread-9-550  


   只有Thread-0線程輸出的結果是咱們指望的,而輸出的是每次都累加的,要想獲得咱們指望的結果,有幾種解決方案:

   一、將Count類中的成員變量num變成count方法的局部變量;

public class Count {  
    public void count() {  
        int num = 0;  
        for(int i = 1; i <= 10; i++) {  
            num += i;  
        }  
        System.out.println(Thread.currentThread().getName() + 」-「 + num);  
    }  
}  


   二、將線程類成員變量拿到run方法中,這時count引用是線程內的局部變量;

public class ThreadTest4 {  
    public static void main(String[] args) {  
        Runnable runnable = new Runnable() {  
            public void run() {  
                Count count = new Count();  
                count.count();  
            }  
        };  
        for(int i = 0; i < 10; i++) {  
            new Thread(runnable).start();  
        }  
    }  
}   


   三、每次啓動一個線程使用不一樣的線程類,不推薦。

   經過上述測試,咱們發現,存在成員變量的類用於多線程時是不安全的,不安全體如今這個成員變量可能發生非原子性的操做,而變量定義在方法內也就是局部變量是線程安全的。想一想在使用struts1時,不推薦建立成員變量,由於action是單例的,若是建立了成員變量,就會存在線程不安全的隱患,而struts2是每一次請求都會建立一個action,就不用考慮線程安全的問題。因此,平常開發中,一般須要考慮成員變量或者說全局變量在多線程環境下,是否會引起一些問題。

   要說明線程同步問題首先要說明Java線程的兩個特性,可見性和有序性。

   多個線程之間是不能直接傳遞數據進行交互的,它們之間的交互只能經過共享變量來實現。拿上面的例子來講明,在多個線程之間共享了Count類的一個實例,這個對象是被建立在主內存(堆內存)中,每一個線程都有本身的工做內存(線程棧),工做內存存儲了主內存count對象的一個副本,當線程操做count對象時,首先從主內存複製count對象到工做內存中,而後執行代碼count.count(),改變了num值,最後用工做內存中的count刷新主內存的 count。當一個對象在多個工做內存中都存在副本時,若是一個工做內存刷新了主內存中的共享變量,其它線程也應該可以看到被修改後的值,此爲可見性。

   多個線程執行時,CPU對線程的調度是隨機的,咱們不知道當前程序被執行到哪步就切換到了下一個線程,一個最經典的例子就是銀行匯款問題,一個銀行帳戶存款100,這時一我的從該帳戶取10元,同時另外一我的向該帳戶匯10元,那麼餘額應該仍是100。那麼此時可能發生這種狀況,A線程負責取款,B線程負責匯款,A從主內存讀到100,B從主內存讀到100,A執行減10操做,並將數據刷新到主內存,這時主內存數據100-10=90,而B內存執行加10操做,並將數據刷新到主內存,最後主內存數據100+10=110,顯然這是一個嚴重的問題,咱們要保證A線程和B線程有序執行,先取款後匯款或者先匯款後取款,此爲有序性。
 

高併發下線程安全的單例模式

概念:
  java中單例模式是一種常見的設計模式,單例模式的寫法有好幾種,這裏主要介紹三種:懶漢式單例、餓漢式單例、登記式單例。
  單例模式有如下特色:
  一、單例類只能有一個實例。
  二、單例類必須本身建立本身的惟一實例。
  三、單例類必須給全部其餘對象提供這一實例。

  單例模式確保某個類只有一個實例,並且自行實例化並向整個系統提供這個實例。在計算機系統中,線程池、緩存、日誌對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。這些應用都或多或少具備資源管理器的功能。每臺計算機能夠有若干個打印機,但只能有一個Printer Spooler,以免兩個打印做業同時輸出到打印機中。每臺計算機能夠有若干通訊端口,系統應當集中管理這些通訊端口,以免一個通訊端口同時被兩個請求同時調用。總之,選擇單例模式就是爲了不不一致狀態,避免政出多頭。


1、懶漢式單例

//懶漢式單例類.在第一次調用的時候實例化本身   
public class Singleton {  
    private Singleton() {}  
    private static Singleton single=null;  
    //靜態工廠方法   
    public static Singleton getInstance() {  
         if (single == null) {    
             single = new Singleton();  
         }    
        return single;  
    }  
}  

Singleton經過將構造方法限定爲private避免了類在外部被實例化,在同一個虛擬機範圍內,Singleton的惟一實例只能經過getInstance()方法訪問。

(事實上,經過Java反射機制是可以實例化構造方法爲private的類的,那基本上會使全部的Java單例實現失效。此問題在此處不作討論,姑且掩耳盜鈴地認爲反射機制不存在。)

可是以上懶漢式單例的實現沒有考慮線程安全問題,它是線程不安全的,併發環境下極可能出現多個Singleton實例。

要實現線程安全,有如下三種方式(都是對getInstance這個方法改造,保證了懶漢式單例的線程安全,若是你第一次接觸單例模式,對線程安全不是很瞭解,能夠先跳過下面這三小條,去看餓漢式單例,等看完後面再回頭考慮線程安全的問題):

一、在getInstance方法上加同步

public static synchronized Singleton getInstance() {  
         if (single == null) {    
             single = new Singleton();  
         }    
        return single;  
}  

 

二、不在方法上加同步,而是在方法內部使用雙重檢查鎖定

public static Singleton getInstance() {  
        if (singleton == null) {    
            synchronized (Singleton.class) {    
               if (singleton == null) {    
                  singleton = new Singleton();   
               }    
            }    
        }    
        return singleton;   
    }  

 

三、靜態內部類

public class Singleton {    
    private static class LazyHolder {    
       private static final Singleton INSTANCE = new Singleton();    
    }    
    private Singleton (){}    
    public static final Singleton getInstance() {    
       return LazyHolder.INSTANCE;    
    }    
}    

這種比上面一、2都好一些,既實現了線程安全,又避免了同步帶來的性能影響。
 

2、餓漢式單例

//餓漢式單例類.在類初始化時,已經自行實例化   
public class Singleton1 {  
    private Singleton1() {}  
    private static final Singleton1 single = new Singleton1();  
    //靜態工廠方法   
    public static Singleton1 getInstance() {  
        return single;  
    }  
}  

餓漢式在類建立的同時就已經建立好一個靜態的對象供系統使用,之後再也不改變,因此天生是線程安全的。

 

3、登記式單例(可忽略)

//相似Spring裏面的方法,將類名註冊,下次從裏面直接獲取。  
public class Singleton3 {  
    private static Map<String,Singleton3> map = new HashMap<String,Singleton3>();  
    static{  
        Singleton3 single = new Singleton3();  
        map.put(single.getClass().getName(), single);  
    }  
    //保護的默認構造子  
    protected Singleton3(){}  
    //靜態工廠方法,返還此類唯一的實例  
    public static Singleton3 getInstance(String name) {  
        if(name == null) {  
            name = Singleton3.class.getName();  
            System.out.println("name == null"+"--->name="+name);  
        }  
        if(map.get(name) == null) {  
            try {  
                map.put(name, (Singleton3) Class.forName(name).newInstance());  
            } catch (InstantiationException e) {  
                e.printStackTrace();  
            } catch (IllegalAccessException e) {  
                e.printStackTrace();  
            } catch (ClassNotFoundException e) {  
                e.printStackTrace();  
            }  
        }  
        return map.get(name);  
    }  
    //一個示意性的商業方法  
    public String about() {      
        return "Hello, I am RegSingleton.";      
    }      
    public static void main(String[] args) {  
        Singleton3 single3 = Singleton3.getInstance(null);  
        System.out.println(single3.about());  
    }  
}  

 登記式單例實際上維護了一組單例類的實例,將這些實例存放在一個Map(登記薄)中,對於已經登記過的實例,則從Map直接返回,對於沒有登記的,則先登記,而後返回。 

這裏我對登記式單例標記了可忽略,個人理解來講,首先它用的比較少,另外其實內部實現仍是用的餓漢式單例,由於其中的static方法塊,它的單例在類被裝載的時候就被實例化了。

 

餓漢式和懶漢式區別

從名字上來講,餓漢和懶漢,

餓漢就是類一旦加載,就把單例初始化完成,保證getInstance的時候,單例是已經存在的了,

而懶漢比較懶,只有當調用getInstance的時候,纔回去初始化這個單例。

另外從如下兩點再區分如下這兩種方式:

一、線程安全:

餓漢式天生就是線程安全的,能夠直接用於多線程而不會出現問題,

懶漢式自己是非線程安全的,爲了實現線程安全有幾種寫法,分別是上面的一、二、3,這三種實如今資源加載和性能方面有些區別。
 

二、資源加載和性能:

餓漢式在類建立的同時就實例化一個靜態對象出來,無論以後會不會使用這個單例,都會佔據必定的內存,可是相應的,在第一次調用時速度也會更快,由於其資源已經初始化完成,

而懶漢式顧名思義,會延遲加載,在第一次使用該單例的時候纔會實例化對象出來,第一次調用時要作初始化,若是要作的工做比較多,性能上會有些延遲,以後就和餓漢式同樣了。

至於一、二、3這三種實現又有些區別,

第1種,在方法調用上加了同步,雖然線程安全了,可是每次都要同步,會影響性能,畢竟99%的狀況下是不須要同步的,

第2種,在getInstance中作了兩次null檢查,確保了只有第一次調用單例的時候纔會作同步,這樣也是線程安全的,同時避免了每次都同步的性能損耗

第3種,利用了classloader的機制來保證初始化instance時只有一個線程,因此也是線程安全的,同時沒有性能損耗,因此通常我傾向於使用這一種。

 

什麼是線程安全?

若是你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。若是每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也和預期的是同樣的,就是線程安全的。

或者說:一個類或者程序所提供的接口對於線程來講是原子操做,或者多個線程之間的切換不會致使該接口的執行結果存在二義性,也就是說咱們不用考慮同步的問題,那就是線程安全的。

 

應用

如下是一個單例類使用的例子,以懶漢式爲例,這裏爲了保證線程安全,使用了雙重檢查鎖定的方式:

public class TestSingleton {  
    String name = null;  
  
        private TestSingleton() {  
    }  
  
    private static volatile TestSingleton instance = null;  
  
    public static TestSingleton getInstance() {  
           if (instance == null) {    
             synchronized (TestSingleton.class) {    
                if (instance == null) {    
                   instance = new TestSingleton();   
                }    
             }    
           }   
           return instance;  
    }  
  
    public String getName() {  
        return name;  
    }  
  
    public void setName(String name) {  
        this.name = name;  
    }  
  
    public void printInfo() {  
        System.out.println("the name is " + name);  
    }  
  
}  

能夠看到裏面加了volatile關鍵字來聲明單例對象,既然synchronized已經起到了多線程下原子性、有序性、可見性的做用,爲何還要加volatile呢,緣由已經在下面評論中提到,

還有疑問可參考http://www.iteye.com/topic/652440
和http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

public class TMain {  
    public static void main(String[] args){  
        TestStream ts1 = TestSingleton.getInstance();  
        ts1.setName("jason");  
        TestStream ts2 = TestSingleton.getInstance();  
        ts2.setName("0539");  
          
        ts1.printInfo();  
        ts2.printInfo();  
          
        if(ts1 == ts2){  
            System.out.println("建立的是同一個實例");  
        }else{  
            System.out.println("建立的不是同一個實例");  
        }  
    }  
}  

 運行結果:

結論:由結果能夠得知單例模式爲一個面向對象的應用程序提供了對象唯一的訪問點,無論它實現何種功能,整個應用程序都會同享一個實例對象。

對於單例模式的幾種實現方式,知道餓漢式和懶漢式的區別,線程安全,資源加載的時機,還有懶漢式爲了實現線程安全的3種方式的細微差異。

相關文章
相關標籤/搜索