併發編程-線程安全性

本節內容:java

  併發模擬工具的使用、演示案例、線程安全性-原子性並演示JUC之Atomic包、回顧synchronized、lock、volatile關鍵字編程


併發模擬工具:JMeter。我用的是windows下的,關於中文只需修改JMeter的bin目錄下的jmeter.properties。修改language=zh_CN 再次運行jmeter.bat打開就是中文版。windows

主界面打開,建立一個線程組。先了解一下線程屬性。線程數:指虛擬用戶數。Ramp-up:虛擬用戶增加時長,用戶作某一個操做的高峯期時長 分鐘*秒。循環次數:虛擬用戶作操做幾回後中止。接下里開始一個簡單的操做。在線程組上建立Http請求-建立圖形結果-建立察看結果樹-選項中設置日誌查看。一些對應的參數這裏就不作講解了。經過圖形結果和結果樹咱們能夠清楚的看到併發時的接口狀況,和每次請求詳細的狀況。安全


 併發模擬代碼:咱們利用一些輔助類來構建併發代碼。CountDownLatch和Semaphore多線程

CountDownLatch:位於java.util.concurrent包下,利用它能夠實現相似計數器的功能。好比有一個任務A,它要等待其餘4個任務執行完畢以後才能執行,此時就能夠利用CountDownLatch來實現這種功能了。併發

該類只提供了一個構造器app

public CountDownLatch(int count) {  };  //參數count爲計數值

還有三個重要的方法:工具

public void await() throws InterruptedException { };   //調用await()方法的線程會被掛起,它會等待直到count值爲0才繼續執行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  //和await()相似,只不過等待必定的時間後count值還沒變爲0的話就會繼續執行
public void countDown() { };  //將count值減1

  

如圖示:計數器初始值爲3,線程A調用await方法後,進程進入等待狀態。其餘進程代碼裏執行countDown時,計數器減1.當計數器爲0時,線程A才繼續執行。該類能夠阻塞線程並保證線程知足某種特定狀況下繼續執行。優化


 Semaphore:字面量就是信號量。能夠控制同時訪問的線程個數,經過 acquire() 獲取一個許可,若是沒有就等待,而 release() 釋放一個許可。ui

Semaphore類位於java.util.concurrent包下,它提供了2個構造器:

public Semaphore(int permits) {          //參數permits表示許可數目,即同時能夠容許多少線程進行訪問
    sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {    //這個多了一個參數fair表示是不是公平的,即等待時間越久的越先獲取許可
    sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
}

 其中有幾個重要的方法:

public void acquire() throws InterruptedException {  }     //獲取一個許可
public void acquire(int permits) throws InterruptedException { }    //獲取permits個許可
public void release() { }          //釋放一個許可
public void release(int permits) { }    //釋放permits個許可

acquire()用來獲取一個許可,若無許可可以得到,則會一直等待,直到得到許可。

release()用來釋放許可。注意,在釋放許可以前,必須先獲得到許可。

這4個方法都會被阻塞,若是想當即獲得執行結果,可使用下面幾個方法:

public boolean tryAcquire() { };    //嘗試獲取一個許可,若獲取成功,則當即返回true,若獲取失敗,則當即返回false
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { };  //嘗試獲取一個許可,若在指定的時間內獲取成功,則當即返回true,不然則當即返回false
public boolean tryAcquire(int permits) { }; //嘗試獲取permits個許可,若獲取成功,則當即返回true,若獲取失敗,則當即返回false
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; //嘗試獲取permits個許可,若在指定的時間內獲取成功,則當即返回true,不然則當即返回false

 咱們如今利用這兩個輔助類,來完成一段併發演示代碼。首先介紹一下,我本身定義了幾個註釋來區別不一樣的類:

@NoRecommend:標記不推薦的類或寫法
@Recommend:標記推薦的類或寫法
@NotThreadSafe:線程不安全的類或寫法
@ThreadSafe:線程安全的類或寫法

 首先咱們建立一個線程不安全的類:

@NotThreadSafe
public class ConcurrencyExample1 {
    //請求總數
    public static int clientTotal = 5000;
    //同時併發執行的線程數
    public static int threadTotal = 200;
    //共享資源
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //建立線程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //定義信號量
        final Semaphore semaphore = new Semaphore(threadTotal);
        //定義計數器
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(() ->{
                try {
            //獲取許可 判斷當前進程是否容許被執行 semaphore.acquire(); add();
//釋放 semaphore.release(); } catch (Exception e) { System.out.println("exception:"+e.getMessage()); }
           //將計數值減1 countDownLatch.countDown(); }); }
      //保證全部進程執行完 countDownLatch.await(); executorService.shutdown(); System.out.println(
"count:{}"+count); } private static void add(){ count++; } }

 屢次執行會發現count的最終結果並非5000,緣由我就不在講述了,在以前的相關博文中這一線程安全問題已經講了好幾遍了。

既然出現了線程不安全咱們就來複習一下何爲線程安全性:

線程安全性定義:當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些進程將如何交替執行,而且在主調代碼中不須要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的。

 那麼關於線程安全性又有很重要的:原子性、可見性、有序性了。此處將以前的概念進行了小小的精簡。

原子性:提供了互斥訪問,同一時刻只能有一個線程來懟它進行操做。
可見性:一個線程對主內存的修改能夠及時的被其餘線程觀察到。
有序性:一個線程觀察其餘線程中的指令執行順序,因爲指令重排序的存在,該觀察結果通常雜亂無序。

 在以前的多線程模塊中。咱們已經瞭解了可見性及有序性的處理。如今咱們學一下如何在併發編程時保證原子性。

原子性:併發包JUC--Atomic(此節單獨對Atomic中的幾種類進行講解。)


 可見性:概念就不說了,主要來精簡一下致使共享變量在線程間不可見的緣由

  一、線程交叉執行

  二、重排序結合線程交叉執行

  三、共享變量更新後的值沒有在工做內存與主內存間及時更新

那處理可見性咱們也有幾種方式:synchronized、lock、volatile

  synchronized和lock能保證同一時刻只有一個線程獲取鎖而後執行同步代碼。並在釋放鎖以前對變量的修改刷新到住內存中,以此來保證可見性。

  在java內存模型中,關於synchronized有兩條規定:

  一、線程解鎖前,必須把共享變量的最新值刷新到主內存。

  二、線程加鎖時,將清空工做內存中共享變量的值,從而使用共享變量時須要從主內存中從新讀取最新的值。注意:加鎖與解鎖是同一把鎖

  當一個共享變量被volatile修飾時,它會保證修改的值當即被更新到主內存。其餘線程讀取時會從內存中讀到新值。普通的共享變量不能保證可見性,其被寫入內存的時機不肯定。當其餘線程去讀,可能讀到的是舊的值。

  它的實現是經過內存屏障和禁止重排序優化來實現:

  一、對volatile變量寫操做時,會在寫操做後加入一條store屏障指令,將本地內存中的共享變量值刷新到主內存。

  二、對volatile變量讀操做時,會在讀操做前加入一條load屏障指令,從主內存中讀取共享變量。


 有序性-先行發生原子(happens-before)

  針對於內存模型中的有序來講,先行發生是java內存模型中定義的兩項操做之間的偏序關係。

  簡單介紹一下java內存模型下一些自然的先行發生關係,這些關係無須任何同步器協助就已經存在,可在編碼中直接使用。

  程序次序規則:一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做

  鎖定規則:一個unLock操做先行發生於後面對同一個鎖的lock操做

  volatile變量規則:對一個變量的寫操做先行發生於後面對這個變量的讀操做

  傳遞規則:若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於caozuoB

  線程啓動規則:Thread對象的start方法先行發生於此線程的每個動做

  線程中斷規則:對線程interrupt方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生

  線程終結規則:線程中全部的操做都先行發生於線程的終止檢測,咱們能夠經過Thread.join方法結束Thread.isAlive的返回值手段檢測到線程已經終止執行

  對象終結規則:一個對象的初始化完成先行發生於他的finalize方法的開始

相關文章
相關標籤/搜索