??前言:本文內(nèi)容較長,可以帶上瓜子,爆米花細(xì)細(xì)閱讀,感興趣的朋友可以收藏關(guān)注。
??在學(xué)習(xí)JAVA的過程中往往都不可避免的會(huì)學(xué)到多線程,同時(shí)往上的內(nèi)容大多也很零散,縱使有很多寫的不錯(cuò)的文章,但是畢竟有種孤軍奮戰(zhàn)的感覺,學(xué)完就忘完,這次就帶大家解決這些問題,讓大家由以一個(gè)更廣闊和更實(shí)用的角度進(jìn)行分析,保障大家學(xué)到用到,不會(huì) 線程從學(xué)完到放棄,力求以簡單的比喻,淺顯的代碼演示給大家對(duì)線程并發(fā)最直觀的感受,同時(shí)感謝前人的鋪墊,互勉。
??為了給大家一個(gè)對(duì)將要學(xué)習(xí)內(nèi)容的直觀感受,先來一張大綱圖,本文就圍繞這些知識(shí)展開。
大綱圖

??可以看到內(nèi)容還是比較多的,那么我們就一條條往下梳理。
??在介紹多線程之前我們需要先了解一下什么是線程
??線程是我們程序運(yùn)行的最小單元,我們可以用它訪問它所在進(jìn)程的全部資源,當(dāng)存在多個(gè)線程訪問同一進(jìn)程的資源時(shí),出現(xiàn)了多線程,當(dāng)多個(gè)線程同時(shí)訪問同一資源時(shí),出現(xiàn)了線程并發(fā),由此產(chǎn)生線程同步的概念。
??在這里需要簡單的提一下線程的基本api。
??創(chuàng)建線程
??1:繼承Thread類
public class MyThread extends Thread {
@Override
public void run() {
super.run();
}
}
MyThread myThread = new MyThread();
myThread.start();
??新建我們的類,繼承Thread,實(shí)現(xiàn)run方法,最后實(shí)例化,調(diào)用start即可。
??2:實(shí)現(xiàn)Runnable類
public Runnable runnable = new Runnable() {
@Override
public void run() {
}
};
Thread thread = new Thread(runnable);
thread.start();
??和上面的差不多,只是把Runnable當(dāng)參數(shù)傳給Thread的構(gòu)造器即可。
??3:通過Callable和Future創(chuàng)建線程
public Callable runnable = new Callable<String>() {
@Override
public String call() throws Exception {
return "我是執(zhí)行結(jié)果";
}
};
FutureTask futrue = new FutureTask(runnable);
Thread thread = new Thread(futrue);
thread.start();
??需要?jiǎng)?chuàng)建一個(gè)類繼承Callable,并且可以控制泛型類型,然后實(shí)例化我們的FutureTask,再實(shí)例化我們的線程,運(yùn)行即可。
- cancel(boolean mayInterruptIfRunning)
取消當(dāng)前任務(wù)的執(zhí)行。
??需要?jiǎng)?chuàng)建一個(gè)類繼承Callable,并且可以控制泛型類型,然后實(shí)例化如果執(zhí)行cancel時(shí)任務(wù)已完成或者之前已進(jìn)行取消操作或者因?yàn)槟承┰虿荒苓M(jìn)行取消操作,那么將返回false
如果執(zhí)行cancel時(shí)線程還未執(zhí)行該任務(wù),那么該任務(wù)將不會(huì)被執(zhí)行,返回ture
??需要?jiǎng)?chuàng)建一個(gè)類繼承Callable,并且可以控制泛型類型,然后實(shí)例化如果執(zhí)行cancel時(shí)線程正在處理該任務(wù),且mayInterruptIfRunning為false,那么任務(wù)會(huì)繼續(xù)執(zhí)行到執(zhí)行完成,此時(shí)返回ture
??需要?jiǎng)?chuàng)建一個(gè)類繼承Callable,并且可以控制泛型類型,然后實(shí)例化如果執(zhí)行cancel時(shí)線程正在處理該任務(wù),且mayInterruptIfRunning為ture,那么會(huì)中斷該線程,此時(shí)返回ture
isCancelled()
獲取任務(wù)是否被取消。如果任務(wù)在取消前正常完成,那么返回tureisDone()
獲取任務(wù)是否已完成如果任務(wù)已完成,返回true。如果任務(wù)時(shí)因中斷,異常等原因被終止,也返回trueget()
獲取任務(wù)執(zhí)行結(jié)果,get()方法會(huì)一直阻塞直到任務(wù)完成。如果任務(wù)被中斷,將拋出InterruptedExceptionget(long timeout, TimeUnit unit)
在規(guī)定時(shí)間內(nèi)獲取任務(wù)執(zhí)行結(jié)果,如果沒有在規(guī)定時(shí)間內(nèi)完成任務(wù)則拋出TimeoutException。
??FutureTask的出現(xiàn)讓我們把任務(wù)和線程給分開了,提供了代碼更高的可控性。
??線程生命周期
- 新建 :從新建一個(gè)線程對(duì)象到程序start() 這個(gè)線程之間的狀態(tài),都是新建狀態(tài);
- 就緒 :線程對(duì)象調(diào)用start()方法后,就處于就緒狀態(tài),等到JVM里的線程調(diào)度器的調(diào)度;
- 運(yùn)行 :就緒狀態(tài)下的線程在獲取CPU資源后就可以執(zhí)行run(),此時(shí)的線程便處于運(yùn)行狀態(tài),運(yùn)行狀態(tài)的線程可變?yōu)榫途w、阻塞及死亡三種狀態(tài)。
- 等待/阻塞/睡眠 :在一個(gè)線程執(zhí)行了sleep(睡眠)、suspend(掛起)等方法后會(huì)失去所占有的資源,從而進(jìn)入阻塞狀態(tài),在睡眠結(jié)束后可重新進(jìn)入就緒狀態(tài)。
- 終止 :run()方法完成后或發(fā)生其他終止條件時(shí)就會(huì)切換到終止?fàn)顟B(tài)。
??線程操作 sleep/yield/join
??sleep
??當(dāng)我們需要一個(gè)線程延遲執(zhí)行的時(shí)候就可以調(diào)用這個(gè)方法,當(dāng)然也會(huì)拋出異常,我們catch一下即可。
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
??單位是毫秒,我們可以乘以1000再操作,當(dāng)我們調(diào)用Sleep后,線程會(huì)進(jìn)入阻塞狀態(tài),給其他線程執(zhí)行的機(jī)會(huì),但是不會(huì)釋放當(dāng)前所持的對(duì)象鎖,即不會(huì)釋放同步資源鎖,當(dāng)sleep()休眠時(shí)間滿后,該線程不一定會(huì)立即執(zhí)行,這是因?yàn)槠渌€程可能正在運(yùn)行而起沒有被調(diào)度為放棄執(zhí)行,除非此線程具有更高的優(yōu)先級(jí)。
??yield
??該方法和sleep方法類似,也是Thread類提供的一個(gè)靜態(tài)方法,可以讓正在執(zhí)行的線程暫停,但是不會(huì)進(jìn)入阻塞狀態(tài),而是直接進(jìn)入就緒狀態(tài)。相當(dāng)于只是將當(dāng)前線程暫停一下,然后重新進(jìn)入就緒的線程池中,讓線程調(diào)度器重新調(diào)度一次。也會(huì)出現(xiàn)某個(gè)線程調(diào)用yield方法后暫停,但之后調(diào)度器又將其調(diào)度出來重新進(jìn)入到運(yùn)行狀態(tài)。
??join
??當(dāng)B線程執(zhí)行到了A線程的.join()方法時(shí),B線程就會(huì)等待,等A線程都執(zhí)行完畢,B線程才會(huì)執(zhí)行。
??所以join可以用來臨時(shí)加入線程執(zhí)行。
final Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("ThreadInfo", "執(zhí)行了A線程");
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
threadA.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("ThreadInfo", "執(zhí)行了B線程");
}
});
threadB.start();
threadA.start();
??查看下日志:
ThreadInfo: 執(zhí)行了A線程
ThreadInfo: 執(zhí)行了B線程
??我們同時(shí)執(zhí)行了A,B鮮橙吧,但是由于我們?cè)贐線程中添加了threadA.join();,所以直到一秒后A執(zhí)行結(jié)束,才輪到B執(zhí)行,可以想象為排隊(duì)買票被人強(qiáng)插隊(duì)的情況。
??線程運(yùn)行優(yōu)先級(jí)
Thread t = new Thread();
t.priority(100); //設(shè)置優(yōu)先級(jí)
??級(jí)別越高,獲取執(zhí)行的幾率越高,當(dāng)然并不一定是,只是增大幾率,傳入?yún)?shù)是int,可自行判斷。
一 : Java內(nèi)存模型(JMM)
??Java內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這樣底層細(xì)節(jié)。此處的變量與Java編程時(shí)所說的變量不一樣,指包括了實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,但是不包括局部變量與方法參數(shù),后者是線程私有的,不會(huì)被共享。
??來,我們看一張線程從內(nèi)存中存取變量的圖。

??從這里我們看到當(dāng)線程工作的狀態(tài),我們需要知道是,一個(gè)變量的產(chǎn)生一定是從主內(nèi)存中誕生的,當(dāng)我們創(chuàng)建一個(gè)線程后,會(huì)從主內(nèi)存中復(fù)制這個(gè)線程需要用到的數(shù)據(jù)副本。
??如上圖,當(dāng)我們線程A讀取了一個(gè)參數(shù)時(shí)并修改時(shí),會(huì)先把修改的數(shù)據(jù)放到本地內(nèi)存A中,然后同步到主內(nèi)存中
??然后通知我們的線程B刷新數(shù)據(jù),那么當(dāng)我們的線程B就會(huì)將本地內(nèi)存B中的數(shù)據(jù)從主內(nèi)存中重新刷新,以便獲取到最新的值。
??而我們看到JMM就在本地內(nèi)存和主內(nèi)存之間起作用,控制數(shù)據(jù)的存取規(guī)則。
??可能有的同學(xué)會(huì)好奇,為什么要有個(gè)本地內(nèi)存A,B這個(gè)東西,多麻煩啊,直接對(duì)主內(nèi)存進(jìn)行操作多好,省了多少事。
??我們看一下面這個(gè)圖

??由于計(jì)算機(jī)的存儲(chǔ)設(shè)備與處理器的運(yùn)算能力之間有幾個(gè)數(shù)量級(jí)的差距,所以現(xiàn)代計(jì)算機(jī)系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運(yùn)算速度的高速緩存(cache)來作為內(nèi)存與處理器之間的緩沖:將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再從緩存同步回內(nèi)存之中沒這樣處理器就無需等待緩慢的內(nèi)存讀寫了。
??基于高速緩存的存儲(chǔ)交互很好地解決了處理器與內(nèi)存的速度矛盾,但是引入了一個(gè)新的問題:緩存一致性(Cache Coherence)。在多處理器系統(tǒng)中,每個(gè)處理器都有自己的高速緩存,而他們又共享同一主存,如上圖所示:多個(gè)處理器運(yùn)算任務(wù)都涉及同一塊主存,需要一種協(xié)議可以保障數(shù)據(jù)的一致性,這類協(xié)議有MSI、MESI、MOSI及Dragon Protocol等。
??好了,不扯遠(yuǎn)了,通過處理器操作內(nèi)存的這個(gè)過程我們可以類比到我們的線程操作內(nèi)存的過程,總體是為了提高運(yùn)行效率。
??那再回到我們上面的JMM,這里我們還需要提出幾個(gè)概念,方便講解JMM。
-
重排序
??在執(zhí)行程序時(shí)為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序。重排序分三種類型:
??1.編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。線程A線程B本地內(nèi)存Ax=1本地內(nèi)存Bx=1主內(nèi)存x=1步驟1步驟2線程之間通信:線程A向B發(fā)送消息
??2.指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction-Level Parallelism,ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
??3.內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。
??從java源代碼到最終實(shí)際執(zhí)行的指令序列,會(huì)分別經(jīng)歷上面三種重排序:上述的1屬于編譯器重排序,2和3屬于處理器重排序。
??來個(gè)圖理解一下:

-
內(nèi)存可見性
??要說這個(gè)問題的話我們可以回到上面看下圖一,就知道了,線程A,B各自從主內(nèi)存中復(fù)制出了同一個(gè)變量a,但是線程A,B中的副本的操作卻是不互通的,比如線程A改了a的值,然后準(zhǔn)備往主內(nèi)存中刷新,但是此時(shí)B線程提前了從主內(nèi)存中獲取了a的值,那么獲取a的值就是舊數(shù)據(jù),因?yàn)榫€程A修改的值還來得及刷進(jìn)去。
??那么線程A中的操作對(duì)線程B是不是就是不透明的,也就是不可見,要是B能夠知道A改了之后再去主內(nèi)存取一次值,那A的操作對(duì)B就是可見的。
-
順序一致性模型
??首先這是一個(gè)理想中的模型,即不存在,但是卻為我們?cè)O(shè)置內(nèi)存模型提供了參考依據(jù)。
??比如,看下面的代碼:
int a = 3;
int b = 4;
int c = 5;
??很簡單的賦值,當(dāng)我們寫下這串代碼的時(shí)候,我們默認(rèn)執(zhí)行就是a - > b - > c。但事實(shí)上編譯器不這么認(rèn)為,再編譯器看來a,b,c三個(gè)數(shù)據(jù)的賦值之間是沒有任何關(guān)系的,什么意思呢,就是a有沒有被賦值為3完全不影響b能不能被賦值為4,那么同理c也是。那秉承著高效率的原則,處理器完全可以同時(shí)執(zhí)行這三個(gè)變量的賦值工作,是吧,不然現(xiàn)在處理器這么牛不用放著干嘛呢。
??那再看下下面的代碼:
int a = 3;
int b = 4;
int c = a * b;
??我們看到c的值是由a和b的乘積得到的,但是a和b的執(zhí)行順序卻是可以不分前后,但是c的執(zhí)行順序一定要在a和b之后。c對(duì)于a,b的關(guān)系我們可以稱之為數(shù)據(jù)依賴性,就是沒有a和b的賦值,c是無法產(chǎn)生的。
??順序一致性模型就是我們編譯器和處理器參考處理數(shù)據(jù)的依據(jù)。
??說下順序一致性模型的特點(diǎn):
1 : 一個(gè)線程中的所有操作必須按照程序的順序來執(zhí)行。
2 : 所有線程(不管程序是否同步)都只能看到一個(gè)單一的操作執(zhí)行順序。在順序一致性內(nèi)存模型中,每個(gè)操作都必須原子執(zhí)行且立刻對(duì)所有線程可見。
??第一點(diǎn)我們上面也說了,我們通過一張圖看下第二點(diǎn)

??可以看到在同一時(shí)間,只能由一個(gè)線程對(duì)主內(nèi)存進(jìn)行操作。
??假如現(xiàn)在有線程A,B分別執(zhí)行了A1-A2-A3和B1-B2-B3的操作,并且這倆個(gè)線程被執(zhí)行了同步操作,那么我們可以看到的執(zhí)行順序可能是下面這樣:

??可以看到不僅整個(gè)執(zhí)行順序看起來是有序的,單個(gè)線程里面執(zhí)行順序也是有序的。
??假如A,B線程沒有被執(zhí)行同步操作,可以看到可能如下:

??可以看到雖然整體的執(zhí)行是無序的,但就A,B單個(gè)來說,執(zhí)行仍然是有序的,從而為內(nèi)存可見性提供了強(qiáng)力的保證,因?yàn)樵陧樞驁?zhí)行模型下這倆種情況的執(zhí)行順序?qū)λ械木€程來說都是可見的。
??那么實(shí)際中我們的JMM就參考順序一致性模型進(jìn)行設(shè)置
??JMM在同步程序下執(zhí)行
??參考下面的代碼:
class SynchronizedExample {
int a = 0;
boolean flag = false;
public synchronized void reader() {
flag = true;
}
public synchronized void writer()
{
if (flag) {
int i = a;
}
}
}
??上面示例代碼中,假設(shè)A線程執(zhí)行writer()方法后,B線程執(zhí)行reader()方法。這是一個(gè)正確同步的多線程程序。根據(jù)JMM規(guī)范,該程序的執(zhí)行結(jié)果將與該程序在順序一致性模型中的執(zhí)行結(jié)果相同。下面是該程序在兩個(gè)內(nèi)存模型中的執(zhí)行時(shí)序?qū)Ρ葓D:

??參考右邊的順序模型,雖然JMM的執(zhí)行我們發(fā)現(xiàn)臨界區(qū)中的順序被重排了,但是整體的執(zhí)行順序是有序的,這是因?yàn)榕R界區(qū)中的順序不會(huì)影響到外面的執(zhí)行,所以執(zhí)行順序是可以調(diào)整的。
??雖然線程A在臨界區(qū)內(nèi)做了重排序,但由于監(jiān)視器的互斥執(zhí)行的特性,這里的線程B根本無法“觀察”到線程A在臨界區(qū)內(nèi)的重排序。這種重排序既提高了執(zhí)行效率,又沒有改變程序的執(zhí)行結(jié)果。
??從這里我們可以看到JMM在具體實(shí)現(xiàn)上的基本方針:在不改變(正確同步的)程序執(zhí)行結(jié)果的前提下,盡可能的為編譯器和處理器的優(yōu)化打開方便之門。
??JMM在非同步程序下執(zhí)行
??對(duì)于未同步或未正確同步的多線程程序,JMM只提供最小安全性:線程執(zhí)行時(shí)讀取到的值,要么是之前某個(gè)線程寫入的值,要么是默認(rèn)值(0,null,false),JMM保證線程讀操作讀取到的值不會(huì)無中生有(out of thin air)的冒出來。為了實(shí)現(xiàn)最小安全性,JVM在堆上分配對(duì)象時(shí),首先會(huì)清零內(nèi)存空間,然后才會(huì)在上面分配對(duì)象(JVM內(nèi)部會(huì)同步這兩個(gè)操作)。因此,在已清零的內(nèi)存空間(pre-zeroed memory)分配對(duì)象時(shí),域的默認(rèn)初始化已經(jīng)完成了。
??JMM不保證未同步程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果一致。因?yàn)槿绻胍WC執(zhí)行結(jié)果一致,JMM需要禁止大量的處理器和編譯器的優(yōu)化,這對(duì)程序的執(zhí)行性能會(huì)產(chǎn)生很大的影響。而且未同步程序在順序一致性模型中執(zhí)行時(shí),整體是無序的,其執(zhí)行結(jié)果往往無法預(yù)知。保證未同步程序在這兩個(gè)模型中的執(zhí)行結(jié)果一致沒什么意義。未同步程序在JMM中的執(zhí)行時(shí),整體上是無序的,其執(zhí)行結(jié)果無法預(yù)知。未同步程序在兩個(gè)模型中的執(zhí)行特性有下面幾個(gè)差異:
??1.順序一致性模型保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行,而JMM不保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行(比如上面正確同步的多線程程序在臨界區(qū)內(nèi)的重排序),這一點(diǎn)前面圖1-7已經(jīng)闡述,這里就不再贅述。
??2.順序一致性模型保證所有線程只能看到一致的操作執(zhí)行順序,而JMM不保證所有線程能看到一致的操作執(zhí)行順序。這一點(diǎn)前面圖1-4,1-5,1-6部分已經(jīng)闡述,這里就不再贅述。
??3.JMM不保證對(duì)64位的long型和double型變量的讀/寫操作具有原子性,而順序一致性模型保證對(duì)所有的內(nèi)存讀/寫操作都具有原子性。
??第3個(gè)差異與處理器總線的工作機(jī)制密切相關(guān)。在計(jì)算機(jī)中,數(shù)據(jù)通過總線在處理器和內(nèi)存之間傳遞。每次處理器和內(nèi)存之間的數(shù)據(jù)傳遞都是通過一系列步驟來完成的,這一系列步驟稱之為總線事務(wù)。
??工作流程參考下圖:

??需要注意的是在一個(gè)處理器執(zhí)行總線事務(wù)期間,總線會(huì)禁止其它所有的處理器和I/O設(shè)備執(zhí)行內(nèi)存的讀/寫。
??總線的這些工作機(jī)制可以把所有處理器對(duì)內(nèi)存的訪問以串行化的方式來執(zhí)行;在任意時(shí)間點(diǎn),最多只能有一個(gè)處理器能訪問內(nèi)存。這個(gè)特性確保了單個(gè)總線事務(wù)之中的內(nèi)存讀/寫操作具有原子性。
??在一些32位的處理器上,如果要求對(duì)64位數(shù)據(jù)的寫操作具有原子性,會(huì)有比較大的開銷。為了照顧這種處理器,java語言規(guī)范鼓勵(lì)但不強(qiáng)求JVM對(duì)64位的long型變量和double型變量的寫具有原子性。當(dāng)JVM在這種處理器上運(yùn)行時(shí),會(huì)把一個(gè)64位long/ double型變量的寫操作拆分為兩個(gè)32位的寫操作來執(zhí)行。這兩個(gè)32位的寫操作可能會(huì)被分配到不同的總線事務(wù)中執(zhí)行,此時(shí)對(duì)這個(gè)64位變量的寫將不具有原子性。
??當(dāng)單個(gè)內(nèi)存操作不具有原子性,將可能會(huì)產(chǎn)生意想不到后果。請(qǐng)看下面示意圖:

??如上圖所示,假設(shè)處理器A寫一個(gè)long型變量,同時(shí)處理器B要讀這個(gè)long型變量。處理器A中64位的寫操作被拆分為兩個(gè)32位的寫操作,且這兩個(gè)32位的寫操作被分配到不同的寫事務(wù)中執(zhí)行。同時(shí)處理器B中64位的讀操作被分配到單個(gè)的讀事務(wù)中執(zhí)行。當(dāng)處理器A和B按上圖的時(shí)序來執(zhí)行時(shí),處理器B將看到僅僅被處理器A“寫了一半“的無效值。
??注意,在JSR -133(即)之前的舊內(nèi)存模型中,一個(gè)64位long/ double型變量的讀/寫操作可以被拆分為兩個(gè)32位的讀/寫操作來執(zhí)行。從JSR -133內(nèi)存模型開始(即從JDK5開始),僅僅只允許把一個(gè)64位long/ double型變量的寫操作拆分為兩個(gè)32位的寫操作來執(zhí)行,任意的讀操作在JSR -133中都必須具有原子性(即任意讀操作必須要在單個(gè)讀事務(wù)中執(zhí)行)。
??這里提一下,從JDK5開始,java使用新的JSR -133內(nèi)存模型(本文除非特別說明,針對(duì)的都是JSR-133內(nèi)存模型)。JSR-133使用happens-before的概念來闡述操作之間的內(nèi)存可見性。在JMM中,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見,那么這兩個(gè)操作之間必須要存在happens-before關(guān)系。這里提到的兩個(gè)操作既可以是在一個(gè)線程之內(nèi),也可以是在不同線程之間。
??需要我們從happens-before中了解的可以分為以下幾點(diǎn):
??程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before 于該線程中的任意后續(xù)操作。
??監(jiān)視器鎖規(guī)則:對(duì)一個(gè)監(jiān)視器的解鎖,happens-before 于隨后對(duì)這個(gè)監(jiān)視器的加鎖。
??volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,happens-before 于任意后續(xù)對(duì)這個(gè)volatile域的讀。傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
二 : 鎖
??1: ReentrantLock
??2: synchronized
??3: volatile
??通過上面的學(xué)習(xí),我們發(fā)現(xiàn)在多線程并發(fā)的情況下如果希望程序按照我們順序進(jìn)行運(yùn)行,那就需要進(jìn)行同步操作,那實(shí)現(xiàn)多線程之間的同步操作利用到的就是鎖,這里的鎖我們就可以比如我們家用的鎖,比如現(xiàn)在有一個(gè)房子只能住一個(gè)人,一把鎖,但是有好幾個(gè)人有房子的鑰匙,那么當(dāng)這幾個(gè)人去使用房子的時(shí)候,每次就只能一個(gè)人進(jìn)去,不懂鎖概念的我們就可以這樣理解鎖,但是鎖的種類可能有很多種,下面一一介紹。
??ReentrantLock
??從字面意思的翻譯是 : 可重入鎖。
常用方法:
lock()
unlock()
tryLock()
lockInterruptibly
newCondition()
lock(),unlock()
??首先我們來看一個(gè)例子,比如現(xiàn)在一個(gè)商店買東西,還有100件貨物,但是可能有很多的顧客來搶著買,我們用代碼模擬一下,看看:
public class Shop {
private int max = 1000;
public void sell(){
max--;
Log.d("current_tickets", max + "");
}
}
??首先建一個(gè)商店,里面有1000張票,然后再可以賣票,當(dāng)然,每賣一張票,總數(shù)就會(huì)減少,然后再模擬下很多人搶票。
final Shop shop = new Shop();
for (int i = 0; i < 1000; i++) {
new Thread() {
@Override
public void run() {
super.run();
shop.sell();
}
}.start();
}
??這個(gè)場(chǎng)面太混亂了啊,1千人同時(shí)搶1000張票,那我們運(yùn)行程序看下日志:
D/current_tickets: 999
D/current_tickets: 998
...
D/current_tickets: 1
D/current_tickets: 0
??貌似沒啥問題哈,1000張正好全賣給了1000個(gè)人,票一個(gè)不剩,真的是這樣么?
??我們仔細(xì)翻看日志可能會(huì)發(fā)現(xiàn)有這種情況出現(xiàn),當(dāng)然并不是每次都會(huì)出現(xiàn),我們可以多次運(yùn)行,可以看到不同的運(yùn)行結(jié)果,如下:
...
D/current_tickets: 456
D/current_tickets: 456
...
D/current_tickets: 200
D/current_tickets: 200
...
??或者
...
D/current_tickets: 3
D/current_tickets: 2
D/current_tickets: 1
D/current_tickets: 1
D/current_tickets: 0
D/current_tickets: -1
...
??很顯然,在實(shí)際賣票的過程中這種事是不允許發(fā)生的,無論是錯(cuò)誤告訴顧客還有多少票或者票都沒了怎么還能賣。
??這就是一個(gè)線程并發(fā)的場(chǎng)景,一個(gè)人代表一個(gè)線程,1000個(gè)人代表1000個(gè)線程,那么我們要做到如何能夠有秩序的賣票就可以用ReentrantLock實(shí)現(xiàn),這里我們只需要改一下商店的代碼就可以。
public class Shop {
private int max = 1000;
private Lock lock = new ReentrantLock();
public void sell(){
try {
lock.lock();
max--;
Log.d("current_tickets", max + "");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
??可以看到這里用到了lock(),和unlock()倆個(gè)方法,一個(gè)是上鎖,一個(gè)是解鎖,我們?cè)俅芜\(yùn)行看下結(jié)果,可以發(fā)現(xiàn),無論運(yùn)行多少次,結(jié)果始終是正確的,這里L(fēng)ock就起到一個(gè)很重要的作用,當(dāng)有一個(gè)顧客去買票的時(shí)候,買票廳會(huì)禁止下一個(gè)顧客緊跟著買票,告訴已經(jīng)有人在買了,麻煩你等一下,通過這個(gè)方法,就能夠讓售票廳有秩序的賣票,不會(huì)出現(xiàn)烏龍了,當(dāng)然我們需要注意的是lock和unlock必須要成對(duì)出現(xiàn),如果只lock沒有unlock,那么后面的都買不了票了,不管你售票廳還剩多少票。
可重入
??那上面介紹的可重入是啥意思呢,接著舉例子,現(xiàn)在有一戶人家爸爸正在買票,本來只打算買一個(gè)票,正在買票的過程中,但是此時(shí)突然兒子跑過來,跟爸爸說他也要一張票,那么后面的人就沒轍了啊,這是一家人,插隊(duì)也沒辦法,假如此時(shí)老婆也過來,那仍然可以擠進(jìn)來,誰叫這三個(gè)是一家人呢,這里就體現(xiàn)的是可重入,在線程中的體現(xiàn)就是同一個(gè)線程可以重復(fù)訪問一個(gè)鎖,每次訪問給鎖一個(gè)計(jì)數(shù)器加1,每次解鎖再減1,直到計(jì)數(shù)器等于0,后面的人家才可以買票,看看我們的代碼如何實(shí)現(xiàn)。
??仍然先建我們的售票廳
//售票廳代碼
public class Shop {
private int max = 1000;
private Lock lock = new ReentrantLock();
public void sell(){
try {
lock.lock();
max--;
Log.d("current_tickets", Thread.currentThread().getName() + " : " + max);
}catch (Exception e){
e.printStackTrace();
}finally {
}
}
}
??這里的Thread.currentThread().getName()是獲取線程名稱的方法,發(fā)現(xiàn)了沒有,finally里面沒有添加unlock方法,那我們?cè)倏聪氯藗內(nèi)绾钨I票:
//買票代碼
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Thread personOne = new Thread(new Sell(), "personOne");
Thread personTwo = new Thread(new Sell(), "personTwo");
personOne.start();
personTwo.start();
}
private class Sell implements Runnable {
@Override
public void run() {
for (int i = 0; i < 500; i++) {
shop.sell();
}
}
}
??這里我們只有倆戶人家買票,但是一個(gè)人一次性就買了500張(是票販子無疑了),當(dāng)我們運(yùn)行的時(shí)候,發(fā)現(xiàn)運(yùn)行結(jié)果如下:
current_tickets: personOne : 999
...
current_tickets: personOne : 500
??又或者是:
current_tickets: personTwo : 999
...
current_tickets: personTwo : 500
??反正每次運(yùn)行后發(fā)現(xiàn)只有一戶人家可以買票,另一戶人家插都插不上,這就是因?yàn)槲覀兩厦尜I票的地方?jīng)]有調(diào)用unlock,那么當(dāng)開始賣票的時(shí)候,誰先搶到位置,誰開始買票,并且除了當(dāng)前在買票的,其余的都只能干瞪眼了。那么如何讓第一家賣完,接著給別人買呢,我們修改上面的售票廳代碼:
public class Shop {
//售票廳代碼
private int max = 1000;
private Lock lock = new ReentrantLock();
public void sell() {
try {
lock.lock();
max--;
Log.d("current_tickets", Thread.currentThread().getName() + " : " + max);
if (max == 500) {
for (int i = 0; i < 500; i++) {
lock.unlock();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
}
}
}
??我們加了一個(gè)判斷,當(dāng)買了500張票的時(shí)候,再解鎖500次,那么下一個(gè)人就可以接著買票了,當(dāng)然,這里只是給大家展示這個(gè)可重入的概念,切勿模仿這種low的寫法。
trylock(),tryLock(long time, TimeUnit unit)
??倆個(gè)方法的作用都是嘗試獲取鎖,并且都會(huì)返回一個(gè)boolean值,true表示成功獲取到了鎖,false表示沒有。
??tryLock(long time, TimeUnit unit)和trylock()的區(qū)別在于tryLock(long time, TimeUnit unit)會(huì)等待一段指定的時(shí)候,如果超過這個(gè)時(shí)間內(nèi)沒有獲取到鎖,會(huì)返回false,如果在指定時(shí)間內(nèi)獲取成功了,則返回true。
??那么再回到上面的賣票案例中,假如現(xiàn)在只有倆個(gè)人在買票,代碼演示如下,首先建立售票處:
//售票處
public class Shop {
private int max = 1000;
private Lock lock = new ReentrantLock();
public void sell() {
if (lock.tryLock()) {
max--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("current_tickets", Thread.currentThread().getName() + " : " + max);
lock.unlock();
} else {
Log.d("current_tickets", Thread.currentThread().getName() + " : 不等了,我走了");
}
}
}
??再看下如何如何買票的:
//買票
Shop shop = new Shop();
Thread threadOne = new Thread(new Buy(), "threadOne");
Thread threadTwo = new Thread(new Buy(), "threadTwo");
threadOne.start();
threadTwo.start();
private class Buy implements Runnable {
@Override
public void run() {
shop.sell();
}
}
??運(yùn)行查看結(jié)果:
threadTwo : 999
threadOne : 不等了,我走了
??多次運(yùn)行我們可以發(fā)現(xiàn)每次只能又一個(gè)線程可以獲取到鎖,另一個(gè)直接就走 ‘不等了,我走了’ 邏輯,這是因?yàn)橄饶玫芥i的線程睡眠了一秒鐘,但是別人不能等你啊,所以直接就放棄了去獲取鎖。
??那可能有的人比較有耐心啊,那我們就可以使用tryLock(long time, TimeUnit unit)方法,修改上面的售票處代碼:
public class Shop {
private int max = 1000;
private Lock lock = new ReentrantLock();
public void sell() {
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
max--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("current_tickets", Thread.currentThread().getName() + " : " + max);
lock.unlock();
} else {
Log.d("current_tickets", Thread.currentThread().getName() + " : 不等了,我走了");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
??現(xiàn)在就有這樣一個(gè)人,他愿意等,那查看運(yùn)行結(jié)果我們發(fā)現(xiàn)當(dāng)一個(gè)線程獲取鎖后,1秒后解鎖,那另一個(gè)鎖在1秒后就會(huì)接著去獲取鎖,因?yàn)樗敢獾?秒,最終倆個(gè)線程都能成功執(zhí)行,完美。
lockInterruptibly
??我們可以把lockInterruptibly當(dāng)lock或者trylock來看,但是卻帶有了另一種屬性,就是可以響應(yīng)線程當(dāng)前的狀態(tài)有沒有被打斷,比如當(dāng)前線程處于sleep或者wait狀態(tài),調(diào)用線程的interrupt()方法,該線程就會(huì)直接拋出lockInterruptibly異常,這樣線程就不會(huì)一直在那等著,如果線程已經(jīng)在運(yùn)行中調(diào)用這個(gè)interrupt()方法會(huì)怎么樣呢,線程不會(huì)被中斷,但是線程的狀態(tài)會(huì)發(fā)現(xiàn)改變,如果我們調(diào)用線程的isInterrupted()方法,就會(huì)返回true或false告訴我們現(xiàn)在線程的狀態(tài)有沒有被打斷,但是線程不會(huì)拋出異常和打斷運(yùn)行。
??那我們看下如何在買票案例中實(shí)現(xiàn),仍然實(shí)現(xiàn)售票廳,如下:
//售票廳
public class Shop {
private int max = 1000;
private Lock lock = new ReentrantLock();
public void sell() {
try {
lock.lockInterruptibly();
max--;
Log.d("current_tickets", Thread.currentThread().getName() + " : " + max);
Thread.sleep(3000);
lock.unlock();
} catch (InterruptedException e) {
Log.d("current_tickets", Thread.currentThread().getName() + " : 不等了,我走了");
e.printStackTrace();
}
}
}
??然后在看下如何買票:
//買票
Shop shop = new Shop();
Thread threadOne = new Thread(new Buy(), "threadOne");
Thread threadTwo = new Thread(new Buy(), "threadTwo");
threadOne.start();
threadTwo.start();
private class Buy implements Runnable {
@Override
public void run() {
shop.sell();
}
}
??我們執(zhí)行代碼開始運(yùn)行:
current_tickets: threadOne : 999
過幾秒后...
current_tickets: threadTwo : 998
??發(fā)現(xiàn)作用其實(shí)是跟lock一樣的是吧,當(dāng)有一個(gè)線程獲取了對(duì)象的鎖,另外的線程就等著直到鎖被釋放為止。
??但是我們可以手動(dòng)打斷這種狀態(tài),修改買票的代碼,我們只需要threadTwo.start();后面加上threadOne.interrupt()或threadTwo.interrupt()就可以了,記住是或,不是和,運(yùn)行一下就可以看到threadTwo始終走的是 ‘不等了,我走了’ 邏輯,這就是因?yàn)槲覀兇驍嗔藅hreadTwo的等待過程,讓threadTwo直接拋出了異常,于是就不再一直阻塞在那里了。
Condition()
??Condition是在java 1.5中才出現(xiàn)的,它用來替代傳統(tǒng)的Object的wait()、notify()實(shí)現(xiàn)線程間的協(xié)作,相比使用Object的wait()、notify(),使用Condition的await()、signal()這種方式實(shí)現(xiàn)線程間協(xié)作更加安全和高效。因此通常來說比較推薦使用Condition,阻塞隊(duì)列實(shí)際上是使用了Condition來模擬線程間協(xié)作。
??Condition是個(gè)接口,基本的方法就是await()和signal()方,生成一個(gè)Condition的基本代碼是lock.newCondition()調(diào)用Condition的await()和signal()方法,都必須在lock保護(hù)之內(nèi),就是說必須在lock.lock()和lock.unlock之間才可以使用,一個(gè)lock可以生成多個(gè)Condition,從而實(shí)現(xiàn)對(duì)鎖更細(xì)的粒度控制。
??Conditon中的await()對(duì)應(yīng)Object的wait();
??Condition中的signal()對(duì)應(yīng)Object的notify();
??Condition中的signalAll()對(duì)應(yīng)Object的notifyAll()
??Condition通常用于設(shè)計(jì)阻塞隊(duì)列,比如我們給了一個(gè)固定的容器,往里面添加數(shù)據(jù)的時(shí)候發(fā)現(xiàn)容量不夠了,就可以采取阻塞的方式,等到數(shù)據(jù)被取走的時(shí)候,騰出了空間,就可以接著添加數(shù)據(jù)了,看看我們用Condition如何實(shí)現(xiàn)這一效果。
//實(shí)現(xiàn)一個(gè)我們需要的阻塞隊(duì)列工具,包含添加和取數(shù)據(jù)功能
public class BlockingQueue {
private final int MAX = 5;
private Lock lock = new ReentrantLock();
private Condition putCondition;
private Condition getCondition;
private List<String> list = new ArrayList<>();
public BlockingQueue() {
putCondition = lock.newCondition();
getCondition = lock.newCondition();
}
public void add(String data) {
lock.lock();
try {
while (list.size() == MAX) {
try {
putCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(data);
Log.d("BlockingQueue", "add : " + data + " : " + list.size());
getCondition.signal();
} finally {
lock.unlock();
}
}
public String get() {
String result;
lock.lock();
try {
while (list.size() == 0) {
try {
getCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
result = list.get(0);
list.remove(0);
putCondition.signal();
Log.d("BlockingQueue", "get : " + result + " : " + list.size());
} finally {
lock.unlock();
}
return result;
}
}
??可以看到我們?cè)O(shè)置了一個(gè)最大容量為5的集合。
??當(dāng)我們不斷調(diào)用add方法時(shí),我們會(huì)檢查當(dāng)前容量是否已經(jīng)滿了,如果沒滿,則可以繼續(xù)添加,如果滿了,則執(zhí)行putCondition.await();add方法也將被阻塞,數(shù)據(jù)即停止往集合中添加,但是當(dāng)我們調(diào)用get方法是,我們的集合大小則會(huì)減小,并且會(huì)調(diào)用putCondition.signal();則會(huì)通知我們剛才被阻塞的add方法再次執(zhí)行。
??當(dāng)我們調(diào)用get方法時(shí),會(huì)檢查數(shù)據(jù)集合是否含有數(shù)據(jù),如果沒有,則執(zhí)行g(shù)etCondition.await();get方法開始阻塞,直到add方法被調(diào)用,添加了數(shù)據(jù)后,執(zhí)行g(shù)etCondition.signal();從而喚醒get方法。
??然后我們?cè)匍_兩個(gè)線程,一個(gè)用于添加數(shù)據(jù),一個(gè)用于取出數(shù)據(jù),代碼如下:
new Thread() {
@Override
public void run() {
super.run();
for (int i = 0; i < 10; i++) {
blockingQueue.add(i + "");
}
}
}.start();
new Thread() {
@Override
public void run() {
super.run();
for (int i = 0; i < 5; i++) {
blockingQueue.get();
}
}
}.start();
??我們?cè)诳刂婆_(tái)看下輸出結(jié)果
BlockingQueue: add : 0 : 1
BlockingQueue: add : 1 : 2
BlockingQueue: get : 0 : 1
BlockingQueue: get : 1 : 0
BlockingQueue: add : 2 : 1
BlockingQueue: add : 3 : 2
BlockingQueue: add : 4 : 3
BlockingQueue: add : 5 : 4
BlockingQueue: add : 6 : 5
BlockingQueue: get : 2 : 4
BlockingQueue: get : 3 : 3
BlockingQueue: get : 4 : 2
BlockingQueue: add : 7 : 3
BlockingQueue: add : 8 : 4
BlockingQueue: add : 9 : 5
??可以看見add和get方法交替執(zhí)行,但是集合的大小確始終控制在5以內(nèi),這就是因?yàn)槲覀兩厦孀龅淖枞刂?,那如果我們注釋掉第二個(gè)線程,執(zhí)行一下,看一下輸出結(jié)果:
BlockingQueue: add : 0 : 1
BlockingQueue: add : 1 : 2
BlockingQueue: add : 2 : 3
BlockingQueue: add : 3 : 4
BlockingQueue: add : 4 : 5
??就會(huì)更加直觀的感受的add方法被調(diào)用5次后被阻塞。
??假如我現(xiàn)在希望集合滿的時(shí)候,add方法和get方法交替執(zhí)行,如何實(shí)現(xiàn)呢,那我們可以修改BlockingQueue的代碼,如下:
public class BlockingQueue {
private final int MAX = 5;
private Lock lock = new ReentrantLock();
private Condition putCondition;
private Condition getCondition;
private List<String> list = new ArrayList<>();
public BlockingQueue() {
putCondition = lock.newCondition();
getCondition = lock.newCondition();
}
public void add(String data) {
lock.lock();
try {
while (list.size() == MAX) {
try {
getCondition.signal();
putCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(data);
Log.d("BlockingQueue", "add : " + data + " : " + list.size());
} finally {
lock.unlock();
}
}
public String get() {
String result;
lock.lock();
try {
while (list.size() == 0 || list.size() == MAX - 1) {
try {
getCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
result = list.get(0);
list.remove(0);
putCondition.signal();
Log.d("BlockingQueue", "get : " + result + " : " + list.size());
} finally {
lock.unlock();
}
return result;
}
}
??我們修改了get的喚醒條件和阻塞條件,查看輸出結(jié)果:
BlockingQueue: add : 0 : 1
BlockingQueue: add : 1 : 2
BlockingQueue: add : 2 : 3
BlockingQueue: add : 3 : 4
BlockingQueue: add : 4 : 5
BlockingQueue: get : 0 : 4
BlockingQueue: add : 5 : 5
BlockingQueue: get : 1 : 4
BlockingQueue: add : 6 : 5
BlockingQueue: get : 2 : 4
BlockingQueue: add : 7 : 5
BlockingQueue: get : 3 : 4
BlockingQueue: add : 8 : 5
BlockingQueue: get : 4 : 4
BlockingQueue: add : 9 : 5
??可以看到日志正好是我們想要的結(jié)果,當(dāng)數(shù)據(jù)滿的時(shí)候才開始執(zhí)行g(shù)et方法,然后和add方法交替執(zhí)行,有人可能會(huì)問,為啥每次都是add方法先打印啊,要是我就是先執(zhí)行g(shù)et怎么樣,看到我們上面的倆個(gè)線程了么,get和add方法都有可能先執(zhí)行,如果先執(zhí)行了get方法,因?yàn)闆]有數(shù)據(jù),被阻塞了,當(dāng)通過add方法添加過數(shù)據(jù)才被喚醒,所以在我們看來,好像每次都是add方法先執(zhí)行,其實(shí)不然。
??可能有的人會(huì)好奇了,這有啥用啊,為啥要交替執(zhí)行,其實(shí)這里只是展示Condition對(duì)鎖的更加精細(xì)的控制,如果你能夠控制日志按照你的想法想怎么執(zhí)行就怎么執(zhí)行,那也算是對(duì)這一知識(shí)點(diǎn)的熟練掌握吧,比如當(dāng)集合中只要添加了數(shù)據(jù),就讓add和get方法交替執(zhí)行如何實(shí)現(xiàn),這里就不再做演示,同學(xué)們可以自己嘗試一下,畢竟光看是不夠,自己動(dòng)手操作一下,才能有更加直觀的感受。
??事實(shí)上java.util.concurrent包下的BlockingQueue的一些子類就是通過這個(gè)原理實(shí)現(xiàn)的,有興趣的同學(xué)看可以去看看,這里不再贅述。
??synchronized
??這個(gè)同步關(guān)鍵字大概是我們平時(shí)用的次數(shù)最多的了。
?synchronized 包括三種用法:
1: 修飾實(shí)例方法
public synchronized void method() {
}
2: 修飾靜態(tài)方法
public static synchronized void method() {
}
3:修飾代碼塊
//成員鎖,鎖的對(duì)象是變量
public void synMethod(Object o) {
synchronized(o) {
}
}
//實(shí)例對(duì)象鎖,this 代表當(dāng)前實(shí)例
synchronized(this) {
}
//當(dāng)前類的 class 對(duì)象鎖
synchronized(Current.class) {
}
??下面我們用synchronized實(shí)現(xiàn)一個(gè)double check單例,代碼如下:
public class Single {
private static Single single;
private Single() {
}
public static Single getInstance() {
if (single == null) {
synchronized (Single.class) {
if (single == null) {
single = new Single();
}
}
}
return single;
}
}
??想必大家可能也常見這種,那知道為什么需要做倆次if判斷呢,外面的if是為了提高效率,避免多線程執(zhí)行每次都走到synchronized中,降低效率,里面的if不用多說,就是為了初始化實(shí)例對(duì)象。
??volatile
??volatile是java虛擬機(jī)提供的一種輕量級(jí)同步機(jī)制,使用起來很簡單,只需要修飾我們需要使用到的變量即可。
??具有以下特性:
保證可見性
禁止指令重排
??在上面的JMM部分我們提到過可見性和指令重排,被volatile修飾的變量會(huì)被編譯器識(shí)別并添加內(nèi)存屏障,如果你的字段是volatile,Java內(nèi)存模型將在寫操作后插入一個(gè)寫屏障 指令,在讀操作前插入一個(gè)讀屏障指令。
??那么寫屏障一旦你完成寫入,任何訪問這個(gè)字段的線程將 會(huì)得到最新的值。讀屏障在你寫入前,會(huì)保證所有之前發(fā)生的事已經(jīng)發(fā)生,并且任何更新過的數(shù)據(jù)值也是可見的,因?yàn)閮?nèi)存屏障會(huì)把之前的寫入值及時(shí)的刷新到主內(nèi)存,確保下一個(gè)讀操作能獲取到最新的值,從而保證可見性,而在屏障(讀寫)的里外面,是互相隔絕的,不會(huì)出現(xiàn)被虛擬機(jī)優(yōu)化代碼而發(fā)生屏障內(nèi)外代碼執(zhí)行順序發(fā)生變化的情況,從而達(dá)到防止指令重排的作用。
??但是需要注意的是volatile不能保證自增操作,比如i++,因?yàn)閕++可以被編譯器識(shí)別后會(huì)分三步走,讀取,計(jì)算,存儲(chǔ),多并發(fā)情況下,這些步驟可能會(huì)被多個(gè)線程拆開進(jìn)行,因?yàn)榻Y(jié)果可能不是我們需要的。
??例如線程A,B都已經(jīng)從主內(nèi)存中獲取了a的值,如果cpu切換到A中進(jìn)行+1的操作的時(shí)候,雖然由于volatile的修飾,但是線程A的+1的操作仍然沒有同步主內(nèi)存中,因?yàn)榇藭r(shí)a的值還未發(fā)生變化,然后cpu又切換到B執(zhí)行,B也做了+1操作,但是也沒有同步到主內(nèi)存中,因?yàn)閍的值仍然沒有發(fā)生變化,只是做了一個(gè)計(jì)算操作,然后A,B分別計(jì)算出了結(jié)果,然后由于volatile的作用,開始同步主內(nèi)存中的a,但是此時(shí)i最后的結(jié)果可能就只加了1,這就是因?yàn)閕++的操作是可拆分的,所以i++不在volatile的使用范疇內(nèi)。
??實(shí)際上我們的volatile可以運(yùn)用在單例上,看一下下面的代碼:
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
??雙重檢查不用多說了,是為了減小synchronized的性能開銷,避免每次都走一下synchronized操作,但用volatile修飾了我們的單例變量,這樣的操作是為什么呢。
??我們看
singleton = new Singleton();
??這一段操作,實(shí)際上可以分成三個(gè)部分
memory = allocate(); // 1:分配對(duì)象的內(nèi)存空間
ctorInstance(memory); // 2:初始化對(duì)象
instance = memory; // 3:設(shè)置instance指向剛分配的內(nèi)存地址
??我們上面這段代碼被編譯器優(yōu)化過后實(shí)際上看起來和我們想象的就不太一樣,可能如下:
memory = allocate(); // 1:分配對(duì)象的內(nèi)存空間
instance = memory; // 3:設(shè)置instance指向剛分配的內(nèi)存地址
ctorInstance(memory); // 2:初始化對(duì)象
??這樣就會(huì)導(dǎo)致我們的第一層if判斷出現(xiàn)問題,instance可能已經(jīng)不會(huì)空了,但是卻沒有分配內(nèi)存地址,導(dǎo)致了 ‘只初始化了一半’ 這種情況的發(fā)生,但是如果用volatile修飾就可以避免了,因?yàn)関olatile可以防止指令重拍,執(zhí)行順序不會(huì)被打亂,結(jié)果就是我們所需要的了。
三 : 開發(fā)工具類
??1: CyclicBarrier
??2: Countdownlatch
??3: Semaphore
??CyclicBarrier
??字面翻譯的意思是柵欄,在多線程我們可以用它攔截多個(gè)線程執(zhí)行過程,比如上交作業(yè)一樣,每個(gè)同學(xué)寫好了作業(yè)交給了組長,組長會(huì)等所在一組的全部組員上交后才給班長,不會(huì)因?yàn)橹挥幸粋€(gè)人上交了就交給班長,我們從提供的方法看一下:
public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
??這倆個(gè)是構(gòu)造器,parties表示等待到達(dá)的線程數(shù)量,比如一個(gè)組有10個(gè)人,但是組長不管那么多,只要有五個(gè)人上交就給班長,剩余五個(gè)就不用管了,那么就是只要5個(gè)任務(wù)完成,就全部執(zhí)行提交作業(yè)動(dòng)作。barrierAction表示是完成執(zhí)行數(shù)量的任務(wù)后,再額外提前執(zhí)行一個(gè)任務(wù),比如五個(gè)人上交了, 組長會(huì)會(huì)先去吃飯,再給班長,這里的吃飯就是額外的任務(wù)。
??再看另一個(gè)方法:
public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
??await方法的作用就是攔截提交作業(yè)給老師,就像上面說的,不會(huì)因?yàn)橐粋€(gè)組員交了作業(yè),組長就把作業(yè)給班長,我們可以調(diào)用這個(gè)方法,讓當(dāng)前的任務(wù)進(jìn)行等待,如果調(diào)用的是第二個(gè)方法,表示的是等待在愿意接受的時(shí)間,比如一個(gè)組員作業(yè)給了組長,但是組長需要等至少五個(gè)人上交作業(yè),才會(huì)把作業(yè)給班長,但是其中一個(gè)組員就不干了,說只能等到上課之前,上課前你必須得給我把作業(yè)上交,不然影響老師對(duì)我的印象啊。這里的上課之前就是我們這里輸入的等待時(shí)間,表示等待某段愿意的時(shí)間,當(dāng)然了,如果提前就有5個(gè)同學(xué)上交了,那這個(gè)等待時(shí)間就沒用了,主要是一個(gè)防止作用。
??下面看看我們?nèi)绾问褂肅yclicBarrier演示我們交作業(yè)這一過程
CyclicBarrier cyclicBarrier;
cyclicBarrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
Log.d("cyclicBarrier", "等下,我吃個(gè)飯先!");
}
});
for (int i = 0; i < 10; i++) {
new CommitTask(cyclicBarrier).start();
}
??這里表示等待五個(gè)同學(xué)交作業(yè)就先吃飯
public class CommitTask extends Thread {
private CyclicBarrier cyclicBarrier;
public CommitTask(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
super.run();
Log.d("cyclicBarrier", Thread.currentThread().getName() + " : 我交作業(yè)了");
try {
cyclicBarrier.await();
} catch (BrokenBarrierException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("cyclicBarrier", Thread.currentThread().getName() + " : 終于批改我的作業(yè)了");
}
}
??這里演示了10個(gè)同學(xué)交作業(yè),但是只要有五個(gè)同學(xué)上交就給班長給老師批改的具體過程,我們查看下日志:
cyclicBarrier: Thread-3 : 我交作業(yè)了
cyclicBarrier: Thread-4 : 我交作業(yè)了
cyclicBarrier: Thread-5 : 我交作業(yè)了
cyclicBarrier: Thread-6 : 我交作業(yè)了
cyclicBarrier: Thread-7 : 我交作業(yè)了
cyclicBarrier: 等下,我吃個(gè)飯先!
cyclicBarrier: Thread-7 : 終于批改我的作業(yè)了
cyclicBarrier: Thread-3 : 終于批改我的作業(yè)了
cyclicBarrier: Thread-4 : 終于批改我的作業(yè)了
cyclicBarrier: Thread-5 : 終于批改我的作業(yè)了
cyclicBarrier: Thread-6 : 終于批改我的作業(yè)了
cyclicBarrier: Thread-8 : 我交作業(yè)了
cyclicBarrier: Thread-9 : 我交作業(yè)了
cyclicBarrier: Thread-10 : 我交作業(yè)了
cyclicBarrier: Thread-11 : 我交作業(yè)了
cyclicBarrier: Thread-12 : 我交作業(yè)了
cyclicBarrier: 等下,我吃個(gè)飯先!
cyclicBarrier: Thread-12 : 終于批改我的作業(yè)了
cyclicBarrier: Thread-8 : 終于批改我的作業(yè)了
cyclicBarrier: Thread-9 : 終于批改我的作業(yè)了
cyclicBarrier: Thread-10 : 終于批改我的作業(yè)了
cyclicBarrier: Thread-11 : 終于批改我的作業(yè)了
??很直觀了,等到五個(gè)人交齊后先吃的飯,然后再給老師批改作業(yè),然后等下次作業(yè)來了,仍然先吃飯,再批改,另外的
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
??同學(xué)們可以自行嘗試,這里就不再做演示,其實(shí)查看CyclicBarrier的源碼可以發(fā)現(xiàn),使用到了ReentrantLock和Condition,這倆個(gè)類的作用上面我也已經(jīng)提到過,總體代碼量也不多,就500行左右,感興趣的同學(xué)可以自行研究下如何不使用CyclicBarrier也能實(shí)現(xiàn)上面的交作業(yè)演示效果。
??CountDownLatch
??這個(gè)我們可以看作是用來給線程計(jì)數(shù)判斷的工具,就拿上面交作業(yè)的例子,每交一個(gè)作業(yè),都會(huì)計(jì)一次數(shù),但是CountDownLatch的計(jì)數(shù)是逐漸減小的。我們看下api提供的方法:
public CountDownLatch(int count)
??只提供了一個(gè)構(gòu)造器,這里的count就表示計(jì)數(shù)的總量,每次計(jì)算都會(huì)減小一個(gè),直到減小到0,觸發(fā)我們需要觸發(fā)的事件,再看其他方法:
public void await()
public boolean await(long timeout, TimeUnit unit)
??這倆個(gè)方法好理解,就放在我們需要觸發(fā)事件的線程里面就可以,等到計(jì)數(shù)到0的時(shí)候就會(huì)觸發(fā)事件,第二個(gè)方法的時(shí)間表示等待指定時(shí)間,和我們上面cyclicBarrier的等待一樣,不會(huì)一直等下去,時(shí)間一過,自動(dòng)觸發(fā),再看一個(gè)方法:
public void countDown()
??這個(gè)方法就是用來給我們的總數(shù)進(jìn)行減小的,每調(diào)用一次,總數(shù)減小1,直到-1,但是到0就會(huì)觸發(fā)我們的事件,下面我們演示收作業(yè)這一過程:
CountDownLatch cdl = new CountDownLatch(10);
WorkCorrecte workCorrecte = new WorkCorrecte(cdl);
for (int i = 0; i < 10; i++) {
CommitWork commitWork = new CommitWork(cdl);
commitWork.start();
}
workCorrecte.start();
??這里表示我們有10個(gè)作業(yè)要收,再往下看:
/**
* 展示組員提交作業(yè)過程
* */
class CommitWork extends Thread {
private CountDownLatch countDownLatch;
public CommitWork(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
Log.d("countDownLatch", "組長等等我,我交下作業(yè),還有 " + (countDownLatch.getCount() - 1) + "個(gè)同學(xué)沒交");
countDownLatch.countDown();
}
}
/**
* 展示組長提交作業(yè)過程
* */
private class WorkCorrecte extends Thread {
private CountDownLatch countDownLatch;
public WorkCorrecte(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
super.run();
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("countDownLatch", "都交完了吧,我給老師了");
}
}
??我們要達(dá)到的效果就是全部組員交過了作業(yè)才能給老師,我們看下日志:
countDownLatch: 組長等等我,我交下作業(yè),還有 9個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 8個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 7個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 6個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 5個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 4個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 3個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 2個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 1個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 都交完了吧,我給老師了
??和我們想要的效果很一致,有的同學(xué)可能有疑問,如果修改CommitWork類中調(diào)用countDownLatch.countDown()的位置會(huì)怎么樣呢,其實(shí)這個(gè)問題就是考慮的就是是不是每個(gè)自線程必須執(zhí)行完,最后才能觸發(fā)我們的事件呢?其實(shí)不是的,我們要完成這一效果,就必須把 countDownLatch.countDown()的調(diào)用放在這些線程的最后,我們可以驗(yàn)證一下是不是這樣,修改一下CommitWork類,如下:
/**
* 展示組員提交作業(yè)過程
*/
class CommitWork extends Thread {
private CountDownLatch countDownLatch;
public CommitWork(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
countDownLatch.countDown();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("countDownLatch", "組長等等我,我交下作業(yè),還有 " + countDownLatch.getCount() + "個(gè)同學(xué)沒交");
}
}
??再看下我們的日志效果:
countDownLatch: 都交完了吧,我給老師了
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
??很明顯,只要計(jì)數(shù)器到了0,就會(huì)觸發(fā)我們的事件,不管調(diào)用countDownLatch.countDown()的線程有沒有走完,這里提到這一點(diǎn)主要是為了和我們上面剛說過的CyclicBarrier工具類功能區(qū)別,一個(gè)會(huì)阻塞調(diào)用的線程,一個(gè)不會(huì),免得大家混淆,另外的倆個(gè)參數(shù)的await這里就不再演示了,同學(xué)們可以自行嘗試。
??Semaphore
??semaphore也是一個(gè)用計(jì)數(shù)完成功能的線程控制類,可以設(shè)置訪問資源指定數(shù)量的線程,我們看下提供的方法:
public Semaphore(int permits);
public Semaphore(int permits,boolean fair);
??這里的permits表示的是指定限制線程的數(shù)量,fair表示搶占資源的方式為公平或非公平,大家可能有點(diǎn)懵,慢慢往下看。
···
semaphore.acquire();
//to do something...
semaphore.release();
···
??acquire的作用是獲取資源的通行證,而release則是釋放獲取到的通行證,這倆個(gè)方法總是成對(duì)出現(xiàn)的,我們每調(diào)用一次acquire,semaphore的計(jì)數(shù)就會(huì)加1,直到達(dá)到的我們限制數(shù)量,別的線程再想進(jìn)來就不可以了,除非我們調(diào)用release方法,釋放出一個(gè)通行證,別的線程才可以繼續(xù)進(jìn)來,這一點(diǎn)跟前面說的synchroized作用很像,不同的是synchroized只能一次一個(gè)線程訪問,而semaphore可以限制指定數(shù)量的線程訪問,并且提供了更多的操作方法,我們可以把semaphore當(dāng)作synchroized的升級(jí)版。
??仍然用我們收作業(yè)來比喻,一個(gè)組長去收作業(yè)時(shí),一次只能從一對(duì)組員桌子前收,然后到下一對(duì)桌子,那我們演示一下這個(gè)過程:
public class Work {
Semaphore semaphore;
public void main(String[] arg) {
semaphore = new Semaphore(2);
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
getBook();
}
}).start();
}
}
public void getBook() {
try {
semaphore.acquire();
Log.d("semaphore", "是誰?收走了我的作業(yè)? " + Thread.currentThread().getName());
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
}
??通過semaphore.acquire()和semaphore.release()將我們收取作業(yè)的部分給包裹住,并且中間處理了5秒,運(yùn)行可以發(fā)現(xiàn),每隔5秒才會(huì)有倆個(gè)線程進(jìn)入執(zhí)行收作業(yè)的代碼,這里的semaphore就很好的限制了訪問資源的線程數(shù)量。
??Semaphore類提供的方法很多,這里為了節(jié)省篇幅不做過多展示了,下面提供簡要注釋:
/ 從此信號(hào)量獲取一個(gè)許可,在提供一個(gè)許可前一直將線程阻塞,否則線程被中斷。
void acquire()
// 從此信號(hào)量獲取給定數(shù)目的許可,在提供這些許可前一直將線程阻塞,或者線程已被中斷。
void acquire(int permits)
// 從此信號(hào)量中獲取許可,在有可用的許可前將其阻塞。
void acquireUninterruptibly()
// 從此信號(hào)量獲取給定數(shù)目的許可,在提供這些許可前一直將線程阻塞。
void acquireUninterruptibly(int permits)
// 返回此信號(hào)量中當(dāng)前可用的許可數(shù)。
int availablePermits()
// 獲取并返回立即可用的所有許可。
int drainPermits()
// 返回一個(gè) collection,包含可能等待獲取的線程。
protected Collection<Thread> getQueuedThreads()
// 返回正在等待獲取的線程的估計(jì)數(shù)目。
int getQueueLength()
// 查詢是否有線程正在等待獲取。
boolean hasQueuedThreads()
// 如果此信號(hào)量的公平設(shè)置為 true,則返回 true。
boolean isFair()
// 根據(jù)指定的縮減量減小可用許可的數(shù)目。
protected void reducePermits(int reduction)
// 釋放一個(gè)許可,將其返回給信號(hào)量。
void release()
// 釋放給定數(shù)目的許可,將其返回到信號(hào)量。
void release(int permits)
// 返回標(biāo)識(shí)此信號(hào)量的字符串,以及信號(hào)量的狀態(tài)。
String toString()
// 僅在調(diào)用時(shí)此信號(hào)量存在一個(gè)可用許可,才從信號(hào)量獲取許可。
boolean tryAcquire()
// 僅在調(diào)用時(shí)此信號(hào)量中有給定數(shù)目的許可時(shí),才從此信號(hào)量中獲取這些許可。
boolean tryAcquire(int permits)
// 如果在給定的等待時(shí)間內(nèi)此信號(hào)量有可用的所有許可,并且當(dāng)前線程未被中斷,則從此信號(hào)量獲取給定數(shù)目的許可。
boolean tryAcquire(int permits, long timeout, TimeUnit unit)
// 如果在給定的等待時(shí)間內(nèi),此信號(hào)量有可用的許可并且當(dāng)前線程未被中斷,則從此信號(hào)量獲取一個(gè)許可。
boolean tryAcquire(long timeout, TimeUnit unit)
四 : 原子操作
??1: 原子變量
??原子變量
??還記得我們之前說過的自增操作不屬于原子操作么,在多線程下進(jìn)行的操作是不安全的,簡單演示一下:
public class Work {
private int i = 0;
public void calculate() {
for (int i = 0; i < 10000; i++) {
i++;
}
}
}
??最后發(fā)現(xiàn)i的值并不等于10000,即使用了volatile進(jìn)行修飾,同時(shí)也是不行的,因?yàn)関olatile不能保證自增操作是安全的,上面也已經(jīng)提到過,這里不再贅述。
??那還有什么簡便的方法呢,原子變量橫空出世,如下:
AtomicInteger
AtomicLong
AtomicBoolean
AtomicArray
AtomicReference
...
??當(dāng)我們想讓一個(gè)int變量在多線程下是安全的話就直接用AtomicInteger修飾,放到多線程環(huán)境下,我們發(fā)現(xiàn)變量是安全的了,如下:
public class Work {
private AtomicInteger i = new AtomicInteger(0);
public void calculate() {
for (int i = 0; i < 10000; i++) {
i.incrementAndGet();
}
}
}
??嗯,使用起來還是非常簡單的,就修飾加初始化,然后就隨便多線程搞,nice。
未完待續(xù)...