執行緒通訊詳解
上文我們說到了關於子執行緒中能否更新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(),用於釋放鎖,不然可能會出現一個物件光拿著鎖卻不放的情況,其他物件也用不了,也就是所謂的死鎖情況。
說了這麼多,不知道你懂了沒有,下次再有別人問你知不知道執行緒通訊的時候,你就可以一臉驕傲的告訴他:這種面試題還要我告訴你?
好了,下回再見。