文章目錄
一、多線程的生命周期及五種基本狀態(tài)
二、Java多線程的創(chuàng)建及啟動(dòng)
1.繼承Thread類,重寫該類的run()方法
2.通過實(shí)現(xiàn)Runnable接口創(chuàng)建線程類
3.通過Callable和Future接口創(chuàng)建線程
三、Java內(nèi)存模型概念
四、內(nèi)存間的交互操作
五、volatile和synchronized的區(qū)別
寫在前面:提起多線程大部門同學(xué)可能都會(huì)皺起眉頭不知道多線程到底是什么、什么時(shí)候可以用到、用的時(shí)候是不是有共享變量問題等等一大堆問題。本篇文章將分為兩部分第一部分是講解多線程基礎(chǔ)、第二部分講解Java內(nèi)存模型。

一、多線程的生命周期及五種基本狀態(tài)
Java多線程生命周期,首先看下面這張經(jīng)典的圖,圖中基本上囊括了Java中多線程重要知識(shí)點(diǎn)。

Java線程具有五中基本狀態(tài)
新建狀態(tài)(New):當(dāng)線程對(duì)象對(duì)創(chuàng)建后,即進(jìn)入了新建狀態(tài),如:Thread t = new MyThread();
就緒狀態(tài)(Runnable):當(dāng)調(diào)用線程對(duì)象的start()方法(t.start();),線程即進(jìn)入就緒狀態(tài)。處于就緒狀態(tài)的線程,只是說明此線程已經(jīng)做好了準(zhǔn)備,隨時(shí)等待CPU調(diào)度執(zhí)行,并不是說執(zhí)行了t.start()此線程立即就會(huì)執(zhí)行;
運(yùn)行狀態(tài)(Running):當(dāng)CPU開始調(diào)度處于就緒狀態(tài)的線程時(shí),此時(shí)線程才得以真正執(zhí)行,即進(jìn)入到運(yùn)行狀態(tài)。注:就緒狀態(tài)是進(jìn)入到運(yùn)行狀態(tài)的唯一入口,也就是說,線程要想進(jìn)入運(yùn)行狀態(tài)執(zhí)行,首先必須處于就緒狀態(tài)中;
-
阻塞狀態(tài)(Blocked):處于運(yùn)行狀態(tài)中的線程由于某種原因,暫時(shí)放棄對(duì)CPU的使用權(quán),停止執(zhí)行,此時(shí)進(jìn)入阻塞狀態(tài),直到其進(jìn)入到就緒狀態(tài),才有機(jī)會(huì)再次被CPU調(diào)用以進(jìn)入到運(yùn)行狀態(tài)。根據(jù)阻塞產(chǎn)生的原因不同,阻塞狀態(tài)又可以分為三種:
1.等待阻塞:運(yùn)行狀態(tài)中的線程執(zhí)行wait()方法,使本線程進(jìn)入到等待阻塞狀態(tài);
2.同步阻塞 – 線程在獲取synchronized同步鎖失敗(因?yàn)殒i被其它線程所占用),它會(huì)進(jìn)入同步阻塞狀態(tài);
3.其他阻塞 – 通過調(diào)用線程的sleep()或join()或發(fā)出了I/O請(qǐng)求時(shí),線程會(huì)進(jìn)入到阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時(shí)、join()等待線程終止或者超時(shí)、或者I/O處理完畢時(shí),線程重新轉(zhuǎn)入就緒狀態(tài)。
死亡狀態(tài)(Dead):線程執(zhí)行完了或者因異常退出了run()方法,該線程結(jié)束生命周期。
二、Java多線程的創(chuàng)建及啟動(dòng)
Java中線程的創(chuàng)建常見有如三種基本形式
1.繼承Thread類,重寫該類的run()方法
繼承Thread類,重寫該類的run()方法
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0 ;i < 50;i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
for (int i = 0;i<50;i++) {
//調(diào)用Thread類的currentThread()方法獲取當(dāng)前線程
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 10) {
new MyThread().start();
new MyThread().start();
}
}
}
}
運(yùn)行結(jié)果:
...
main 48
main 49
Thread-0:0
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
Thread-1:0
...
這是代碼運(yùn)行后的結(jié)果,從圖中可以看出:
1、有三個(gè)線程:main、Thread-0 、Thread-1
2、Thread-0 、Thread-1兩個(gè)線程輸出的成員變量 i 的值不連續(xù)(這里的 i 是實(shí)例變量而不是局部變量)。因?yàn)椋和ㄟ^繼承Thread類實(shí)現(xiàn)多線程時(shí),每個(gè)線程的創(chuàng)建都要?jiǎng)?chuàng)建不同的子類對(duì)象,導(dǎo)致Thread-0 、Thread-1兩個(gè)線程不能共享成員變量 i ;
3、線程的執(zhí)行是搶占式,并沒有說Thread-0 或者Thread-1一直占用CPU(這也與線程優(yōu)先級(jí)有關(guān),這里Thread-0 、Thread-1線程優(yōu)先級(jí)相同,關(guān)于線程優(yōu)先級(jí)的知識(shí)這里不做展開)
2.通過實(shí)現(xiàn)Runnable接口創(chuàng)建線程類
定義一個(gè)類實(shí)現(xiàn)Runnable接口;創(chuàng)建該類的實(shí)例對(duì)象obj;將obj作為構(gòu)造器參數(shù)傳入Thread類實(shí)例對(duì)象,這個(gè)對(duì)象才是真正的線程對(duì)象
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0 ;i < 50 ;i++) {
System.out.println(Thread.currentThread().getName()+":" +i);
}
}
public static void main(String[] args) {
for (int i = 0;i < 50;i++) {
System.out.println(Thread.currentThread().getName() + ":" +i);
if (i == 10) {
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable).start();
new Thread(myRunnable).start();
}
}
//java8 labdam方式
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
},"線程3").start();
}
}
運(yùn)行結(jié)果:
...
main:46
main:47
main:48
main:49
Thread-0:28
Thread-0:29
Thread-0:30
Thread-1:30
...
1、線程1和線程2輸出的成員變量i是連續(xù)的,也就是說通過這種方式創(chuàng)建線程,可以使多線程共享線程類的實(shí)例變量,因?yàn)檫@里的多個(gè)線程都使用了同一個(gè)target實(shí)例變量。但是,當(dāng)你使用我上述的代碼運(yùn)行的時(shí)候,你會(huì)發(fā)現(xiàn),其實(shí)結(jié)果有些并不連續(xù),這是因?yàn)槎鄠€(gè)線程訪問同一資源時(shí),如果資源沒有加鎖,那么會(huì)出現(xiàn)線程安全問題(這是線程同步的知識(shí),這里不展開);
2、java8 可以使用lambda方式創(chuàng)建多線程。
3.通過Callable和Future接口創(chuàng)建線程
創(chuàng)建Callable接口實(shí)現(xiàn)類,并實(shí)現(xiàn)call()方法,該方法將作為線程執(zhí)行體,且該方法有返回值,再創(chuàng)建Callable實(shí)現(xiàn)類的實(shí)例;使用FutureTask類來包裝Callable對(duì)象,該FutureTask對(duì)象封裝了該Callable對(duì)象的call()方法的返回值;使用FutureTask對(duì)象作為Thread對(duì)象的target創(chuàng)建并啟動(dòng)新線程;調(diào)用FutureTask對(duì)象的get()方法來獲得子線程執(zhí)行結(jié)束后的返回值
public class MyCallable implements Callable<Integer> {
private int i = 0;
@Override
public Integer call() throws Exception {
int sum = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
sum += i;
}
return sum;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 創(chuàng)建MyCallable對(duì)象
Callable<Integer> myCallable = new MyCallable();
//使用FutureTask來包裝MyCallable對(duì)象
FutureTask<Integer> ft = new FutureTask<Integer>(myCallable);
for (int i = 0;i<50;i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
if (i == 30) {
Thread thread = new Thread(ft);
thread.start();
}
}
System.out.println("主線程for循環(huán)執(zhí)行完畢..");
Integer integer = ft.get();
System.out.println("sum = "+ integer);
}
}
call()方法的返回值類型與創(chuàng)建FutureTask對(duì)象時(shí)<>里的類型一致。
三、Java內(nèi)存模型概念
在并發(fā)編程中,我們需要處理兩個(gè)關(guān)鍵問題:線程之間如何通信及線程之間如何同步(這里的線程是指并發(fā)執(zhí)行的活動(dòng)實(shí)體)。通信是指線程之間以何種機(jī)制來交換信息。在命令式編程中,線程之間的通信機(jī)制有兩種:共享內(nèi)存和消息傳遞。
在共享內(nèi)存的并發(fā)模型里,線程之間共享程序的公共狀態(tài),線程之間通過寫-讀內(nèi)存中的公共狀態(tài)來隱式進(jìn)行通信。在消息傳遞的并發(fā)模型里,線程之間沒有公共狀態(tài),線程之間必須通過明確的發(fā)送消息來顯式進(jìn)行通信。
堆內(nèi)存在線程之間共享(本文使用“共享變量”這個(gè)術(shù)語代指實(shí)例域,靜態(tài)域和數(shù)組元素)。局部變量(Local variables),方法定義參數(shù)(java語言規(guī)范稱之為formal method parameters)和異常處理器參數(shù)(exception handler parameters)不會(huì)在線程之間共享,它們不會(huì)有內(nèi)存可見性問題,也不受內(nèi)存模型的影響。
主內(nèi)存和工作內(nèi)存解釋
主內(nèi)存(main memory): 類的實(shí)例所存在的區(qū)域,所有的實(shí)例都存在主存儲(chǔ)器內(nèi),并且實(shí)例的字段也位于這里。主存儲(chǔ)器為所有的線程所共享,主內(nèi)存主要對(duì)應(yīng)于Java堆中對(duì)象的實(shí)例數(shù)據(jù)部分。
工作內(nèi)存(working memory): 每個(gè)線程各自獨(dú)立所擁有的作業(yè)區(qū),在working memory中,存有main memory中的部分拷貝,稱之為工作拷貝(working copy)。
Java線程之間的通信由Java內(nèi)存模型(本文簡稱為JMM)控制,JMM決定一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)另一個(gè)線程可見。從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存(main memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(local memory),本地內(nèi)存中存儲(chǔ)了該線程以讀/寫共享變量的副本。本地內(nèi)存是JMM的一個(gè)抽象概念,并不真實(shí)存在。它涵蓋了緩存,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化。Java內(nèi)存模型的抽象示意圖如下:

從上圖來看,線程A與線程B之間如要通信的話,必須要經(jīng)歷下面2個(gè)步驟:
- 首先,線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去。
- 然后,線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量。
下面通過示意圖來說明這兩個(gè)步驟:

如上圖所示,本地內(nèi)存A和B有主內(nèi)存中共享變量x的副本。假設(shè)初始時(shí),這三個(gè)內(nèi)存中的x值都為0。
1、線程A在執(zhí)行時(shí),把更新后的x值(假設(shè)值為1)臨時(shí)存放在自己的本地內(nèi)存A中。當(dāng)線程A和線程B需要通信時(shí),線程A首先會(huì)把自己本地內(nèi)存中修改后的x值刷新到主內(nèi)存中,此時(shí)主內(nèi)存中的x值變?yōu)榱?。
2、線程B到主內(nèi)存中去讀取線程A更新后的x值,此時(shí)線程B的本地內(nèi)存的x值也變?yōu)榱?。
從整體來看,這兩個(gè)步驟實(shí)質(zhì)上是線程A在向線程B發(fā)送消息,而且這個(gè)通信過程必須要經(jīng)過主內(nèi)存。JMM通過控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互,來為java程序員提供內(nèi)存可見性保證。
四、內(nèi)存間的交互操作
主內(nèi)存與工作內(nèi)存之間的交互操作定義了8種原子性操作。具體如下:
lock(鎖定):作用于主內(nèi)存的變量,將一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)
unlock(解鎖):作用于主內(nèi)存的變量,將一個(gè)處于鎖定狀態(tài)的變量釋放出來
read(讀取):作用于主內(nèi)存的變量,把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中
load(載入):作用于工作內(nèi)存的變量,把read傳輸?shù)淖兞恐捣湃牖蛘呖截惖焦ぷ鲀?nèi)存的變量副本
use(使用):作用于工作內(nèi)存的變量,表示線程引用工作內(nèi)存中的變量值,將工作內(nèi)存中的一個(gè)變量的值傳遞給執(zhí)行引擎
assign(賦值):作用于工作內(nèi)存的變量,表示線程將指定的值賦值給工作內(nèi)存中的某個(gè)變量。
store(存儲(chǔ)):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送給主內(nèi)存中
write(寫入):作用于主內(nèi)存的變量,將store傳遞的變量值放入到主內(nèi)存中對(duì)應(yīng)的變量里
下面圖片能幫我們加深印象

五、volatile和synchronized的區(qū)別
首先需要理解線程安全的兩個(gè)方面:執(zhí)行控制和內(nèi)存可見。
執(zhí)行控制的目的是控制代碼執(zhí)行(順序)及是否可以并發(fā)執(zhí)行。
內(nèi)存可見控制的是線程執(zhí)行結(jié)果在內(nèi)存中對(duì)其它線程的可見性。根據(jù)Java內(nèi)存模型的實(shí)現(xiàn),線程在具體執(zhí)行時(shí),會(huì)先拷貝主存數(shù)據(jù)到線程本地(CPU緩存),操作完成后再把結(jié)果從線程本地刷到主存。
synchronized關(guān)鍵字解決的是執(zhí)行控制的問題,它會(huì)阻止其它線程獲取當(dāng)前對(duì)象的監(jiān)控鎖,這樣就使得當(dāng)前對(duì)象中被synchronized關(guān)鍵字保護(hù)的代碼塊無法被其它線程訪問,也就無法并發(fā)執(zhí)行。更重要的是,synchronized還會(huì)創(chuàng)建一個(gè)內(nèi)存屏障,內(nèi)存屏障指令保證了所有CPU操作結(jié)果都會(huì)直接刷到主存中,從而保證了操作的內(nèi)存可見性,同時(shí)也使得先獲得這個(gè)鎖的線程的所有操作,都happens-before于隨后獲得這個(gè)鎖的線程的操作。
volatile關(guān)鍵字解決的是內(nèi)存可見性的問題,會(huì)使得所有對(duì)volatile變量的讀寫都會(huì)直接刷到主存,即保證了變量的可見性。這樣就能滿足一些對(duì)變量可見性有要求而對(duì)讀取順序沒有要求的需求。
使用volatile關(guān)鍵字僅能實(shí)現(xiàn)對(duì)原始變量(如boolen、 short 、int 、long等)操作的原子性,但需要特別注意, volatile不能保證復(fù)合操作的原子性。
對(duì)于volatile關(guān)鍵字,當(dāng)且僅當(dāng)滿足以下所有條件時(shí)可使用:
- 對(duì)變量的寫入操作不依賴變量的當(dāng)前值,或者你能確保只有單個(gè)線程更新變量的值。
- 該變量沒有包含在具有其他變量的不變式中
volatile和synchronized的區(qū)別
- volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的,需要從主存中讀;synchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問該變量,其他線程被阻塞住。
- volatile僅能使用在變量級(jí)別;synchronized則可以使用在變量、方法、和類級(jí)別的
- volatile僅能實(shí)現(xiàn)變量的修改可見性,不能保證原子性;而synchronized則可以保證變量的修改可見性和原子性
- volatile不會(huì)造成線程的阻塞;synchronized可能會(huì)造成線程的阻塞。
- volatile標(biāo)記的變量不會(huì)被編譯器優(yōu)化;synchronized標(biāo)記的變量可以被編譯器優(yōu)化。