本節內容: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方法的開始