1. 程式人生 > >執行緒通訊詳解

執行緒通訊詳解

上文我們說到了關於子執行緒中能否更新UI的問題,本篇我們來說一說關於執行緒又一個熱考的知識點,這個問題面試中可能算是一個頻繁被詢問的知識點,那麼今天就讓我們看看,這到底是什麼一個東西。

執行緒通訊!

乍一被問這個問題的時候還是有點蒙的,什麼通訊?執行緒怎麼和通訊扯上關係了,其實我覺得這個過程叫做執行緒同值更合適,其實就是在多執行緒的情況下,我們如何實現資料的一致性。

可能平時大家不會不會有這個概念,那我舉個例子,雙十一剛過去,大家應該買了不少東西吧,我就舉買東西的例子。

假設現在商家生意火爆,只剩下一個商品了,但是此時此刻,有倆個人都想要,並且同時下了單,這種就算是多執行緒的實際情況。

然後倆個人同時問商家。

顧客1,2:老闆,還有貨麼?

老闆:還有一個,那就給你吧。

顧客1:好的。

顧客2:好的。

那麼此時問題出現了,老闆只有了倆個訂單,但是卻只有一個貨。

上面的例子就暴露了在多執行緒情況下資料不能一致帶來的後果,倆個顧客都付了錢,但是最終只有一個顧客能拿到貨,那怎麼辦,讓倆個顧客打架去?

其實這裡暴露的問題就是資料的不一致性,如果其中一個顧客問老闆要貨時,老闆給了他之後再去接待第二個顧客,就會告訴第二個顧客已經沒有貨了,那就不會存在這種問題了。

那麼為了解決這個問題,就有了多執行緒的同步概念。

這裡我提出幾個我們為了達到同步需要的幾個系統工具
```
volatile修飾符**

synchronize關鍵字**

Lock類**
```
這裡暫時就提出常用的集中方式,下面一一介紹

   volatile修飾符**

volatile我們可以把它看做String int float long類似的修飾符,具體使用方法為:
```
private volatile int i = 0;
private volatile float j = 0;
private volatile String k = "";
```
使用比較簡單,就當做普通修飾符放在變數的前面即可,那麼作用是什麼呢?

加了volatile修飾的變數其實就是為了這個變數操作的原子性,何謂原子性,就是指取出,修改,儲存為一個整體的過程,即三者要不都執行了,要不都不執行。那我們看看最終實現的效果。
```
private volatile int i =0;
    @Override
        protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
               for (int j =0; j <10; j++) {
                       new Thread() {
                       @Override
                       public void run() {
                               super.run();
                                      i++;
                                      Log.d("volatile",i +"");
                                }
                       }.start();
                 }

                 for (int j =0; j <10; j++) {
                        new Thread() {
                        @Override
                         public void run() {
                               super.run();
                                       i++;
                                       Log.d("volatile",i +"");
                               }
                       }.start();
            }
}
```
然後我們再看一下效果:

![image](//upload-images.jianshu.io/upload_images/3481369-2d304ad3fa29f723?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

結果很明顯,在多執行緒的情況下,我們的i值一直能保持一致,不會出現異常情況,但是有的同學也測試了,說,不對啊,我的咋就有奇怪的現象,是不是和下面一樣呢?

![image](//upload-images.jianshu.io/upload_images/3481369-3adbe33d34dc5593?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

我們發現有時候居然會出現列印幾個一樣數值的情況,怎麼會這樣,那這裡我們就要起稍微探索一下volatile的實現原理。

我們知道目前的手機的執行速度很快得益於手機的多核系統,多個cpu同時工作,那效率真是槓槓的,每個cpu都自帶一個快取區,為什麼會帶這個呢,因為cpu的執行速度是很快的,但是從記憶體中讀取資料的速度就相對來說比較慢了,那如果我cpu沒進行一次計算都要從記憶體中進行讀取,儲存,那不就大大降低了cup的速度,所以提出了快取區的概念,這個快取區就是為了解決這個問題,假設現在系統中有個變數i,那麼當系統執行時,每個cup從記憶體中讀取這個i到自己的快取區,然後每次cpu的操作就不用直接和記憶體互動,而是直接和這個快取區進行互動即可,大大提高了執行效率。

那現在就分析一下,當沒有被volatile修飾的變數在程式執行時會發生的狀況。

1.CPU1和CPU2先將i變數讀到Cache中,比如記憶體中i=1,那麼現在兩個CPU中Cache中的i都等於1

2.CPU1和CPU2分別開啟一個執行緒來對i進行自增操作,每個執行緒都有一塊自己的獨立記憶體空間來存放i的副本。

3.執行緒1對副本count進行自增操作後,i=2 ; 執行緒2對副本i進行自增操作後,i=2

4.執行緒1和執行緒2將操作後的i寫回到Cache快取的i中

5.CPU1和CPU2將Cache中的i寫回記憶體中的i。

那麼問題就來了,執行緒1和執行緒2操作的count都是一個i副本,這兩個副本的值是一樣的,所以進行了兩次自增後,寫回記憶體的i=2。而正確的結果應該為i=3。這就是多執行緒併發所帶來的問題

那再現在就分析一下當被volatile修飾的時候會發生的狀況。

如果一個變數加了volatile關鍵字,那就等於告訴系統這個變數是對所有執行緒共享的、可見的,每當cpu需要使用i的使用,會從記憶體中讀取i到自己的快取區,每當更改快取區i的副本的時候,也會通知記憶體i及時變化,當再次取用的時候,就會取到記憶體中最新設定進去的值,以此保證資料的一致性。

但是這種資料的一致性的前提是保證是隻有一個cpu,如果是倆個及以上的cup進行操作的時候,volatile仍然是無法保證資料的一致性的,具體原因是:

cpu1從記憶體取i = 1的值到自己的快取區的時候,當cpu1更改了自己快取區i數值的時候,假設i++;會把這個變化值通知到記憶體也進行變化,當cup2(另外的cup),也取值的自己快取區的時候,雖然取到的值i會等於2,但是如果此時正在cpu2正在取的過程,cpu1又進行了i++操作,雖然也通知了記憶體i變化了,此時記憶體的i等於3了,但是由於cpu2已經完成了取值,此時cpu2快取區中的i卻仍然等於2,這就無法保證資料的一致性了。

那有同學就要問了,那搞了半天volatile沒啥作用啊,那要這玩意幹啥用?

也不能這麼說,以前我們的時候都是單核的,volatile在單核多執行緒的情況下是可以保證資料的一致性的,但是現在手機的核心越來越多,多核多執行緒的情況下,volatile自然無法發揮它的作用了,但是我們需要理解這個過程是怎麼樣的。(以上部分意見參考:[android volatile的使用](https://blog.csdn.net/bzlj2912009596/article/details/79201643))
```
synchronize關鍵字**
```
這個關鍵字同學們應該是比較常見的,那就說說它的用法,它可以用於修飾類,程式碼塊,方法,下面給出例項:
```
public class Utils {
         private int i =0;
         private static int j =0;

         public void Change() {
         synchronized (this){
                  Log.d("synchronized","我是被修飾的程式碼塊" +i++);
                 }
        }

         public synchronized void Single() {
                 Log.d("synchronized","我是被修飾的方法" +i++);
         }

         public static void getInstance() {
                 synchronized (Utils.class) {
                           Log.d("synchronized","我是被修飾的類" +j++);
                  }
          }
}
```
以上三種大致代表的synchronized可以使用的地方,下面一一解釋:

修飾程式碼塊

放到程式碼前面的意思是同一時間內,能執行此段程式碼的只能有一個執行緒,舉個例子:

![image](//upload-images.jianshu.io/upload_images/3481369-b5ef4e881db4ca01?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

我這裡開啟20個執行緒進行執行這段程式碼塊,現在看看結果:

![image](//upload-images.jianshu.io/upload_images/3481369-57053cc05d2ad142?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

結果和我們預期的一樣,不會出現i值異常的情況,那假設我們去掉了這段程式碼塊的synchronized會這麼樣呢?修改為:

public void Change(){
{
Log.d("synchronized","我是被修飾的程式碼塊" +i++);
}
}

檢視結果:

![image](//upload-images.jianshu.io/upload_images/3481369-fa5997698ba9f065?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

i值的變化很明顯不是我們想要的了。

那假設我們使用的時候用了倆個   private Utilsutils;會怎麼樣呢?更改使用程式碼為:

![image](//upload-images.jianshu.io/upload_images/3481369-e4ce4ebb0af11c50?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

再檢視一下結果:

![image](//upload-images.jianshu.io/upload_images/3481369-a0ea541ed5f94336?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

結果貌似又不對了啊,怎麼i的值變來變去的?

其實i的值是沒有問題的,我們修飾的i同步程式碼塊只能在一個物件中起作用,但是這裡我們卻建了倆個物件,所以看起來i的值隨意變,但事實上在各自的物件裡變化仍然是正常的。

我們說了程式碼塊的使用和結果,那其實修飾方法其實也是一樣的,這裡我就不在重複了。

那接著說synchronized修飾類的作用。

那我們接著看使用方法:

![image](//upload-images.jianshu.io/upload_images/3481369-a69b45281e98ac7d?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

我們再看下結果:

![image](//upload-images.jianshu.io/upload_images/3481369-c8220b86a9cdc361?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

嗯,也是我們預期的效果。

那有的同學就要問了,為什麼用synchronized修飾類的時候為什麼要把修飾的方法變成static呢?

這個問題問的好,如果我們去掉了static,j也不是static,最終我們得到的結果倆個i的變化值就是從0到9,就和修飾程式碼塊得到的結果一樣了,這樣做也是沒有意義的,又有人問為什麼synchronized為什麼這麼神奇啊,能做到這樣,還記得我們剛學程式的時候老師告訴我們的話麼,萬物皆物件(那我就不怕沒物件了)。每個物件裡面都有一個內建鎖,而用synchronized修飾就是為了得到這個這個內建鎖,以此達到同步的效果。
```
Lock類**
```
最後說一說Lock這個類。這個類我們檢視原始碼就知道這個類其實就是一個介面,帶有以下的方法:
```
void lock();                                                         //獲取鎖
boolean tryLock();                                              //嘗試獲取鎖       
boolean tryLock(long time, TimeUnit unit);       //嘗試在一定時間內獲取鎖   
void unlock();                                                     //釋放鎖
```
下面看下正確的使用姿勢:

![image](//upload-images.jianshu.io/upload_images/3481369-edf23006a08e8aeb?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

再看下結果:

![image](//upload-images.jianshu.io/upload_images/3481369-60fc7c2d193d3f9d?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

嗯,結果讓人很滿意,ReentrantLock是Lock的子類,我們一般也可以稱它為互斥鎖,我們可以用它來例項化Lock,除此之外,Lock還有倆個子類:ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock,分別是讀寫鎖,主要用於檔案的讀寫互斥操作,這裡就不在贅述了,同學們可以自行嘗試一下。

那Lock使用需要注意的是,我們使用Lock結束一定要記得呼叫unlock(),用於釋放鎖,不然可能會出現一個物件光拿著鎖卻不放的情況,其他物件也用不了,也就是所謂的死鎖情況。

說了這麼多,不知道你懂了沒有,下次再有別人問你知不知道執行緒通訊的時候,你就可以一臉驕傲的告訴他:這種面試題還要我告訴你?

好了,下回再見。