Kotlin進階-6-重入鎖+synchronized+volatile

目錄

1、介紹

2、線程的狀態 

3、創建線程

4、線程同步

4.1、可重入鎖

4.2、不可重入鎖的實現

4.3、可重入鎖的實現

4.4、Java中的可重入鎖---ReentrantLock

4.5、同步方法---synchronized

5、volatile

5.1、Java 內存模型

5.1.1、原子性

5.1.2、可見性

5.1.3、有序性

5.2、volatile功能

5.2.1、volatile變量具有可見性

5.2.2、volatile不保證原子性

5.2.3、volatile保證有序性

5.3、正確使用volatile


1、介紹

進程:進程是操作系統結構的基礎,是程序在一個數據集合上運行的過程,使系統進行資源分配和調度的基本單位。進程可以被看作程序的實體,同樣,它也是線程的容器。

2、線程的狀態 

Java線程在運行的聲明週期中,可能處於6種不同的狀態:

New:新創建狀態-----線程被創建,但是還沒調用start()方法。

Runnable:可運行狀態-----一旦調用了start()方法,該線程就會處於Runnable狀態。一個可運行狀態的線程可能在運行,也可能不在運行,這取決於操作系統給該線程提供的運行的時間。

Blocked:阻塞狀態-----表示該線程被鎖阻塞,該線程暫時無法運行。

Waiting:等待狀態------線程暫時不活動,並且不運行任何代碼,這消耗最小的資源,直到其他線程通知它,它纔會返回Runnable狀態。

Timed Waiting:超時等待狀態-----和Waiting 狀態一樣都會停止運行,但是和等待狀態不同的是,它在指定的時間之後會自行返回Runnable狀態。

Terminated:終止狀態-----表示該線程已經執行結束。導致該線程終止有兩種情況:1、run方法執行完畢正常退出;2、因爲一個沒有被捕獲的異常而終止了run方法,導致了線程進入終止狀態。

3、創建線程

線程的創建和運行一般有三種方式:

1、實現Thread的子類,重寫它的run()方法;

2、實現Runnable接口,並且重寫它的run()方法;

3、實現Callable接口,重寫它的call()方法。Callable接口其實是Executor框架中的功能類,它和Runnable接口類似,但是比Runnable接口功能強大,主要表現在以下三點:

<1>Callable可以在運行結束後提供一個返回值,Runnable沒有返回值

<2>Callable中的call()方法可以主動拋出異常

<3>運行Callable對象可以得到一個Future對象,Future表示異步計算的結果。由於線程屬於異步計算模型,因此無法從別的線程中得到函數的返回結果,在這種情況下,就可以使用Future來監視目標線程調用call()的返回結果。當你調用Future的get()方法獲取返回值的時候,當前線程會被阻塞,直到call()方法返回結果。

4、線程同步

在多線程應用中, 通常情況下,兩個或者兩個以上的線程需要共享對同一個數據的存取。如果兩個線程存取相同的對象,並且每一個線程都調用了修改該對象的方法,這種情況通常被叫做競爭條件。而解決這種競爭情況的方法就是 鎖機制。

在Java中,我們一般使用鎖的方式是通過synchronized關鍵字來實現。但是這裏我們先介紹以下可重入鎖不可重入鎖來讓你對synchronized有一個更深的認識。

4.1、可重入鎖

可重入鎖:指的是以線程爲單位,當一個線程獲取對象A的鎖之後,這個線程可以再次獲取對象A上的鎖,而其他的線程是不可以的。

synchronized 和   ReentrantLock 都是可重入鎖。

可重入鎖的作用:防止死鎖。

實現原理:是通過爲每個鎖關聯一個請求計數器和一個佔有它的線程。當計數爲0時,認爲鎖是未被佔有的;線程請求一個未被佔有的鎖時,JVM將記錄鎖的佔有者,並且將請求計數器置爲1。

如果同一個線程再次請求這個鎖,計數將遞增;

每次佔用線程退出同步塊,計數器值將遞減。直到計數器爲0,鎖被釋放。

4.2、不可重入鎖的實現

我們先來實現一下不可重入鎖的實現,代碼如下圖:(這裏實現用Java實現MyLock的原因是Kotlin中Any類無法調用wait()方法)

由上面的可重入鎖的定義我們應該直到不可重入鎖的定義:當一個線程獲取對象A的鎖之後,這個線程不可以再次獲取對象A上的鎖。

左側是我們鎖的實現,我們可以根據該鎖的lock() 和unlock()方法來實現同步代碼塊,當一個線程第一次調用該鎖的lock()方法時,會將isLock置爲true,當另一個線程(或者自己)想再次調用lock()方法時,會進入阻塞狀態,這也就和定義形成了照應,一個線程無法多次獲取MyLock對象的鎖。

輸出結果如下:

從輸出結果我們可以看出,線程Thread-0 只執行了add()方法的內容,當執行到decrease()的lock.lock()方法時,我們線程進入了阻塞的狀態,而且一直阻塞下去,這就導致了死鎖了狀態,這也就解釋了我們的可重入鎖的作用爲什麼是防止死鎖了。

4.3、可重入鎖的實現

實現如下圖:

它和不可重入鎖的區別在於,如果是線程A執行了lock對象鎖構成的同步代碼塊時,當該線程A再次執行獲取lock對象的鎖時,不會被阻塞,而是會繼續執行,但是其他線程獲取不到lock對象的鎖。

輸出結果也證明了它的定義。

4.4、Java中的可重入鎖---ReentrantLock

Java中對於可重入鎖的實現除了synchronized之外,還有ReentrantLock類,它的使用如下圖:

我們可以通過lock()和unlock()方法來實現下同步代碼塊,使用try...finally 語句是必要的,因爲在我們同步代碼塊執行異常的情況下,也應該正常釋放鎖。否則的話,其他線程將永遠會被阻塞。

-------------------------------------------------------------------------------

對於ReentrantLock,當我們的線程在調用ReentrantLock構成的同步代碼塊時,進入死循環中時,可以通過條件對象來讓該線程釋放鎖,給其他線程執行的機會。

如右下圖綠色框中,當該線程執行內容不符合條件時,會進入到死循環中,這時候我們可以通過await()方法來讓線程退出死循環,並且進入阻塞狀態和放棄對鎖的控制,讓其他符合條件的線程進行執行。

那麼你阻塞的線程怎麼恢復執行呢?可以通過黃色框內的signalAll()來重新激活因爲while循環條件不符合而進入阻塞的線程。

4.5、同步方法---synchronized

Lock和Condition接口爲程序設計人員提供了高度的鎖控制,然而大多數情況下, 並不需要那樣的控制,並且可以使用一種嵌入到Java語言內部的機制。從Java 1.0版本開始,Java中的每一個對象都有一個內部鎖 ,如果一個方法用synchronized聲明,那麼對象的鎖將保護整個方法。

下圖中,黃色框和綠色框內的內容是等價的。

在Kotlin中是沒有Synchronized、Volatile關鍵字的,但是有他們的註解,注解出來的是同步的方法,對於同步代碼塊可以如下圖那樣編寫。但是同步代碼塊是比較脆弱的,所以我們建議可以使用阻塞隊列,或者上面講到的Lock、Condition來實現同步代碼。

5、volatile

有時僅僅爲了讀寫一個或者兩個實例域就使用同步的話,顯得開銷過大;而Volatile關鍵字爲實例域的同步訪問提供了免鎖的機制。

在詳細講解volatile之前,我們先了解一下Java內存模型。

5.1、Java 內存模型

Java中堆內存用來存儲對象實例,堆內存是所有線程的共享的運行時內存區域。

而局部變量、方法定義的參數則不會在線程之間共享。

Java內存模型定義了線程和主存之間的的抽象關係,線程之間的共享變量存儲在主存中,每個線程都有自己的私有的本地內存,本地內存中存儲了該線程共享變量的副本。

需要注意的是,主存和本地內存是Java內存模型的一個抽象概念,其實並不存在,它涵蓋了緩存、寫緩存區、寄存器等區域。

Java內存模型控制線程之間進行通行,它決定一個線程對主存共享變量的寫入 何時對另一個線程可見。

----------------------------------------------------------------------------------------------

Java內存模型的抽象示意圖如下:

那麼線程A和線程B之間的通信過程:

線程A更新本地內存共享變量副本的值---->將該值刷新到主存中----->線程B從主存中去讀取該共享變量的值---->更新線程B共享變量的副本

5.1.1、原子性

對於基本數據類型變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要麼執行完畢,要麼就不執行。

對於上圖三條語句,只有第一條語句是原子性操作;

語句2包含了兩個操作:1、讀取x的值;2、將x的值寫入到工作內存中。這兩個操作單獨放的話就是原子性操作,但是合起來就不是了。

語句3包含了三個操作:1、讀取x的值;2、將x的值+1;3、將x的值寫入到工作內存中。

通過上面的介紹,我們應該直到一個語句包含多個操作的話,就不是原子性操作,只有簡答的讀取和賦值纔是原子性操作。

java.util.concurrent.atomic包中有很多類使用了很高效的機器級指令(而不是使用鎖)來保證其他操作的原子性。例如AtomicInteger類提供了方法incrementAndGet()和decrementAndGet,它們分別以原子方式將一個整數自增自減。可以安全地使用AtomicInteger類作爲共享計數器而無需同步。

5.1.2、可見性

可見性,是指線程之間的可見性,一個線程修改的狀態是對另一個線程是可見的。也就是說一個線程修改的結果,另一個線程可以馬上看到。當一個共享變量被volatile修飾時,它會保證被修改的值立即更新到主存中,所以對其他線程是可見的。沒有被volatile修飾的變量則無法保證變量的可見性,當其他線程讀取該值的時候,該值有可能還沒有被更新到主存中。

5.1.3、有序性

Java內存模型允許編譯器和處理器對指令進行重排序,雖然重排序過程不會影響到單線程執行的正確性, 但是會影響到多線程併發執行的正確性。

這是可以通過volatile來保證有序性,除了volatile,也可以通過synchronized和Lock來保證有序性,我們知道synchronized和Lock構成的同步代碼塊,每個時刻只有一個線程執行,這相當於讓多線程順序地執行同步代碼,從而保證了有序性。

5.2、volatile功能

當一個共享變量被vlatile修飾時,其就具備兩個含義:

1、一個線程修改了變量的值,變量的新值對其他線程是立即可見的

2、禁止使用指令重排序

-------------------------------------------

什麼是指令重排序呢?

重排序通常是編譯器或者運行環境爲了優化程序性能而採取的對指令重新排序執行的一種手段。重排序分爲兩類:編譯期重排序和運行期重排序,分別對應着編譯時和運行時環境。

5.2.1、volatile變量具有可見性

看下面的程序,如果按照正常的執行程序的話,我們線程1中的while循環應該每次都能在線程2改變isStop的值之後,退出這個死循環,執行count2++的語句,但是當我們執行一百次的時候,發現結果,並不是每次都能跳出while循環,這就是因爲線程2在改變了isStop的值之後沒有將值更新到主存中,這樣線程1以爲isStop還是false的狀態,這樣它就不會停止循環。

但是當我們用Volatile修飾該變量之後,就不會出現這種情況了。

5.2.2、volatile不保證原子性

看如下代碼:

當你在多次執行該代碼的時候,你會發現它每次的輸出結果都是不一樣的。

假設線程1讀取了count1的原始值=0.然後開始自增操作,當自增到999的時候,線程1被阻塞了,這時候線程1還沒有把5這個值寫入到主存中,所以線程2去讀取的時候,count1的值還是0,這時候線程2開始自增,當線程2自增結束,並將count1的值更新到了主存中,這時候線程1恢復了自增操作,並將1000的值也更新到了主存中,這時候你會發現總共進行了2000次自增操作,但是值還是1000。

前面講過了 count1++語句不是原子性操作,它具有三個子操作步驟:1、讀取x的值;2、將x的值+1;3、將x的值寫入到工作內存中。

所以我們的volatile語句也不能保證對變量的操作是原子性的。

5.2.3、volatile保證有序性

volatile能禁止指令的重排序,因此volatile保證有序性。

volatile禁止指令重排序的含義:當程序執行到volatile變量的時候,在其前面的語句已經全部執行完成,並且結果對後面可見,而且該變量後面的語句都沒有執行。

5.3、正確使用volatile

synchronized可以防止多個線程同時執行一段代碼,這會阻塞一部分線程的執行,這樣就會影響程序的執行效率。

而volatile在某些情況下的性能是優於synchronized的。

但是volatile無法替代synchronized,因爲volatile無法保證操作的原子性。

--------------------------------------------------------------

通常情況下,我們使用volatile關鍵字要避開兩種場景:

1、對變量的寫操作依賴當前值;比如前面我們示例的自增操作。

2、該變量包含在具有其他變量的不變式中。如下圖:我們的初始範圍  0-5 ,但如果兩個線程同時對該類進行執行的時候,比如setLower1(4),setUpper1(3),那麼我們最後的範圍將會變成  4-3  。這顯然是不對的。

使用volatile的場景有很多,這裏介紹兩種常見的場景:

場景1:狀態標誌

當多線程執行該類的時候,我們需要對狀態標誌stop保持可見性,這樣我們的運行才能實時保持正確的執行。這種情況如果使用sychronized的話顯然要複雜的多。

場景2: 雙重檢查模式(DCL)

我們的單例模式經常會這樣寫,第一次判空是爲了不必要的同步操作,第二次判斷是只有在MyLock實例==null的時候纔會去new一個實例出來,當多線程調用時,當進行這兩次判空時,我們需要保證instance的可見性。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章