- JUC(java.util.concurrent)
- 進(jìn)程和線程
- 進(jìn)程:后臺(tái)運(yùn)行的程序(我們打開的一個(gè)軟件璧针,就是進(jìn)程)
- 線程:程序執(zhí)行的最小單元,它依托進(jìn)程存在(同在一個(gè)軟件內(nèi)誉己,同時(shí)運(yùn)行窗口徽鼎,就是線程)
- 并發(fā)和并行
- 并發(fā):同時(shí)訪問某個(gè)東西,就是并發(fā)
- 并行:一起做某些事情,就是并行
- 進(jìn)程和線程
- JUC下的三個(gè)包
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.locks
- java.util.concurrent
1. volatile 理解
Volatile在日常的單線程環(huán)境是應(yīng)用不到的
- Volatile是Java虛擬機(jī)提供的
輕量級
的同步機(jī)制(三大特性)- 保證可見性
- 不保證原子性
- 禁止指令重排
JMM是什么
JMM是Java內(nèi)存模型故硅,也就是Java Memory Model,簡稱JMM,本身是一種抽象的概念桐经,實(shí)際上并不存在,它描述的是一組規(guī)則或規(guī)范浙滤,通過這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段阴挣,靜態(tài)字段和構(gòu)成數(shù)組對象的元素)的訪問方式
JMM關(guān)于同步的規(guī)定:
- 線程解鎖前,必須把共享變量的值刷新回主內(nèi)存
- 線程解鎖前纺腊,必須讀取主內(nèi)存的最新值畔咧,到自己的工作內(nèi)存
- 加鎖和解鎖是同一把鎖
由于JVM運(yùn)行程序的實(shí)體是線程,而每個(gè)線程創(chuàng)建時(shí)JVM都會(huì)為其創(chuàng)建一個(gè)工作內(nèi)存(有些地方稱為椧灸ぃ空間)誓沸,工作內(nèi)存是每個(gè)線程的私有數(shù)據(jù)區(qū)域,而Java內(nèi)存模型中規(guī)定所有變量都存儲(chǔ)在主內(nèi)存壹粟,主內(nèi)存是共享內(nèi)存區(qū)域拜隧,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內(nèi)存中進(jìn)行,首先要將變量從主內(nèi)存拷貝到自己的工作內(nèi)存空間虹蓄,然后對變量進(jìn)行操作犀呼,操作完成后再將變量寫會(huì)主內(nèi)存
,不能直接操作主內(nèi)存中的變量薇组,各個(gè)線程中的工作內(nèi)存中存儲(chǔ)著主內(nèi)存中的變量副本拷貝外臂,因此不同的線程間無法訪問對方的工作內(nèi)存,線程間的通信(傳值)必須通過主內(nèi)存來完成律胀,其簡要訪問過程:
[圖片上傳失敗...(image-d706ea-1589011343184)]
數(shù)據(jù)傳輸速率:硬盤 < 內(nèi)存 < < cache < CPU
上面提到了兩個(gè)概念:主內(nèi)存 和 工作內(nèi)存
主內(nèi)存:就是計(jì)算機(jī)的內(nèi)存宋光,也就是經(jīng)常提到的8G內(nèi)存,16G內(nèi)存
-
工作內(nèi)存:但我們實(shí)例化 new student炭菌,那么 age = 25 也是存儲(chǔ)在主內(nèi)存中
- 當(dāng)同時(shí)有三個(gè)線程同時(shí)訪問 student中的age變量時(shí)罪佳,那么每個(gè)線程都會(huì)拷貝一份,到各自的工作內(nèi)存黑低,從而實(shí)現(xiàn)了變量的拷貝
[圖片上傳失敗...(image-ed4316-1589011343184)]
即:JMM內(nèi)存模型的可見性赘艳,指的是當(dāng)主內(nèi)存區(qū)域中的值被某個(gè)線程寫入更改后,其它線程會(huì)馬上知曉更改后的值克握,并重新得到更改后的值蕾管。
JMM的特性
JMM的三大特性,volatile只保證了兩個(gè)菩暗,即可見性和有序性掰曾,不滿足原子性
- 可見性
- 原子性
- 有序性
可見性代碼驗(yàn)證
但我們對于成員變量沒有添加任何修飾時(shí),是無法感知其它線程修改后的值
package com.moxi.interview.study.thread;
/**
* Volatile Java虛擬機(jī)提供的輕量級同步機(jī)制
*
* 可見性(及時(shí)通知)
* 不保證原子性
* 禁止指令重排
*
* @author: 陌溪
* @create: 2020-03-09-15:58
*/
import java.util.concurrent.TimeUnit;
/**
* 假設(shè)是主物理內(nèi)存
*/
class MyData {
int number = 0;
public void addTo60() {
this.number = 60;
}
}
/**
* 驗(yàn)證volatile的可見性
* 1. 假設(shè)int number = 0停团, number變量之前沒有添加volatile關(guān)鍵字修飾
*/
public class VolatileDemo {
public static void main(String args []) {
// 資源類
MyData myData = new MyData();
// AAA線程 實(shí)現(xiàn)了Runnable接口的旷坦,lambda表達(dá)式
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
// 線程睡眠3秒,假設(shè)在進(jìn)行運(yùn)算
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改number的值
myData.addTo60();
// 輸出修改后的值
System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
}, "AAA").start();
while(myData.number == 0) {
// main線程就一直在這里等待循環(huán)佑稠,直到number的值不等于零
}
// 按道理這個(gè)值是不可能打印出來的秒梅,因?yàn)橹骶€程運(yùn)行的時(shí)候,number的值為0讶坯,所以一直在循環(huán)
// 如果能輸出這句話番电,說明AAA線程在睡眠3秒后,更新的number的值辆琅,重新寫入到主內(nèi)存漱办,并被main線程感知到了
System.out.println(Thread.currentThread().getName() + "\t mission is over");
/**
* 最后輸出結(jié)果:
* AAA come in
* AAA update number value:60
* 最后線程沒有停止,并行沒有輸出 mission is over 這句話婉烟,說明沒有用volatile修飾的變量娩井,是沒有可見性
*/
}
}
輸出結(jié)果為
[圖片上傳失敗...(image-413bfa-1589011343184)]
最后線程沒有停止,并行沒有輸出 mission is over 這句話似袁,說明沒有用volatile修飾的變量洞辣,是沒有可見性
當(dāng)我們修改MyData類中的成員變量時(shí)咐刨,并且添加volatile關(guān)鍵字修飾
/**
* 假設(shè)是主物理內(nèi)存
*/
class MyData {
/**
* volatile 修飾的關(guān)鍵字,是為了增加 主線程和線程之間的可見性扬霜,只要有一個(gè)線程修改了內(nèi)存中的值定鸟,其它線程也能馬上感知
*/
volatile int number = 0;
public void addTo60() {
this.number = 60;
}
}
最后輸出的結(jié)果為:
[圖片上傳失敗...(image-a3c200-1589011343184)]
主線程也執(zhí)行完畢了损俭,說明volatile修飾的變量怀喉,是具備JVM輕量級同步機(jī)制的,能夠感知其它線程的修改后的值率翅。
2. volatile 不保證原子性
前言
通過前面對JMM的介紹材原,我們知道沸久,各個(gè)線程對主內(nèi)存中共享變量的操作都是各個(gè)線程各自拷貝到自己的工作內(nèi)存進(jìn)行操作后在寫回到主內(nèi)存中的。
這就可能存在一個(gè)線程AAA修改了共享變量X的值余蟹,但是還未寫入主內(nèi)存時(shí)卷胯,另外一個(gè)線程BBB又對主內(nèi)存中同一共享變量X進(jìn)行操作,但此時(shí)A線程工作內(nèi)存中共享變量X對線程B來說是不可見威酒,這種工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象就造成了可見性問題窑睁。
原子性
不可分割,完整性兼搏,也就是說某個(gè)線程正在做某個(gè)具體業(yè)務(wù)時(shí)卵慰,中間不可以被加塞或者被分割,需要具體完成佛呻,要么同時(shí)成功,要么同時(shí)失敗病线。
數(shù)據(jù)庫也經(jīng)常提到事務(wù)具備原子性
代碼測試
為了測試volatile是否保證原子性吓著,我們創(chuàng)建了20個(gè)線程,然后每個(gè)線程分別循環(huán)1000次送挑,來調(diào)用number++的方法
MyData myData = new MyData();
// 創(chuàng)建10個(gè)線程绑莺,線程里面進(jìn)行1000次循環(huán)
for (int i = 0; i < 20; i++) {
new Thread(() -> {
// 里面
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
}
}, String.valueOf(i)).start();
}
最后通過 Thread.activeCount(),來感知20個(gè)線程是否執(zhí)行完畢惕耕,這里判斷線程數(shù)是否大于2纺裁,為什么是2?因?yàn)槟J(rèn)是有兩個(gè)線程的司澎,一個(gè)main線程欺缘,一個(gè)gc線程
// 需要等待上面20個(gè)線程都計(jì)算完成后,在用main線程取得最終的結(jié)果值
while(Thread.activeCount() > 2) {
// yield表示不執(zhí)行
Thread.yield();
}
然后在線程執(zhí)行完畢后挤安,我們在查看number的值谚殊,假設(shè)volatile保證原子性的話,那么最后輸出的值應(yīng)該是
20 * 1000 = 20000,
完整代碼如下所示:
/**
* Volatile Java虛擬機(jī)提供的輕量級同步機(jī)制
*
* 可見性(及時(shí)通知)
* 不保證原子性
* 禁止指令重排
*
* @author: 陌溪
* @create: 2020-03-09-15:58
*/
import java.util.concurrent.TimeUnit;
/**
* 假設(shè)是主物理內(nèi)存
*/
class MyData {
/**
* volatile 修飾的關(guān)鍵字蛤铜,是為了增加 主線程和線程之間的可見性嫩絮,只要有一個(gè)線程修改了內(nèi)存中的值丛肢,其它線程也能馬上感知
*/
volatile int number = 0;
public void addTo60() {
this.number = 60;
}
/**
* 注意,此時(shí)number 前面是加了volatile修飾
*/
public void addPlusPlus() {
number ++;
}
}
/**
* 驗(yàn)證volatile的可見性
* 1剿干、 假設(shè)int number = 0蜂怎, number變量之前沒有添加volatile關(guān)鍵字修飾
* 2、添加了volatile置尔,可以解決可見性問題
*
* 驗(yàn)證volatile不保證原子性
* 1杠步、原子性指的是什么意思?
*/
public class VolatileDemo {
public static void main(String args []) {
MyData myData = new MyData();
// 創(chuàng)建10個(gè)線程撰洗,線程里面進(jìn)行1000次循環(huán)
for (int i = 0; i < 20; i++) {
new Thread(() -> {
// 里面
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
}
}, String.valueOf(i)).start();
}
// 需要等待上面20個(gè)線程都計(jì)算完成后篮愉,在用main線程取得最終的結(jié)果值
// 這里判斷線程數(shù)是否大于2,為什么是2差导?因?yàn)槟J(rèn)是有兩個(gè)線程的试躏,一個(gè)main線程,一個(gè)gc線程
while(Thread.activeCount() > 2) {
// yield表示不執(zhí)行
Thread.yield();
}
// 查看最終的值
// 假設(shè)volatile保證原子性设褐,那么輸出的值應(yīng)該為: 20 * 1000 = 20000
System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
}
}
最終結(jié)果我們會(huì)發(fā)現(xiàn)颠蕴,number輸出的值并沒有20000,而且是每次運(yùn)行的結(jié)果都不一致的助析,這說明了volatile修飾的變量不保證原子性
第一次:
[圖片上傳失敗...(image-6f9485-1589011343184)]
第二次:
[圖片上傳失敗...(image-e2cd8a-1589011343184)]
第三次:
[圖片上傳失敗...(image-59aaab-1589011343184)]
為什么出現(xiàn)數(shù)值丟失
[圖片上傳失敗...(image-80c1b6-1589011343184)]
各自線程在寫入主內(nèi)存的時(shí)候犀被,出現(xiàn)了數(shù)據(jù)的丟失,而引起的數(shù)值缺失的問題
下面我們將一個(gè)簡單的number++操作外冀,轉(zhuǎn)換為字節(jié)碼文件一探究竟
public class T1 {
volatile int n = 0;
public void add() {
n++;
}
}
轉(zhuǎn)換后的字節(jié)碼文件
public class com.moxi.interview.study.thread.T1 {
volatile int n;
public com.moxi.interview.study.thread.T1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field n:I
9: return
public void add();
Code:
0: aload_0
1: dup
2: getfield #2 // Field n:I
5: iconst_1
6: iadd
7: putfield #2 // Field n:I
10: return
}
這里查看字節(jié)碼的操作寡键,是用到了IDEA的javap命令
我們首先,使用IDEA提供的External Tools雪隧,來擴(kuò)展javap命令
[圖片上傳失敗...(image-51f7bd-1589011343184)]
完成上述操作后西轩,我們在需要查看字節(jié)碼的文件下,右鍵選擇 External Tools即可
[圖片上傳失敗...(image-ed92a8-1589011343184)]
如果出現(xiàn)了找不到指定類脑沿,那是因?yàn)槲覀儎?chuàng)建的是spring boot的maven項(xiàng)目藕畔,我們之前需要執(zhí)行mvn package命令,進(jìn)行打包操作庄拇,將其編譯成class文件
移動(dòng)到底部注服,有一份字節(jié)碼指令對照表,方便我們進(jìn)行閱讀
下面我們就針對 add() 這個(gè)方法的字節(jié)碼文件進(jìn)行分析
public void add();
Code:
0: aload_0
1: dup
2: getfield #2 // Field n:I
5: iconst_1
6: iadd
7: putfield #2 // Field n:I
10: return
我們能夠發(fā)現(xiàn) n++這條命令措近,被拆分成了3個(gè)指令
- 執(zhí)行
getfield
從主內(nèi)存拿到原始n - 執(zhí)行
iadd
進(jìn)行加1操作 - 執(zhí)行
putfileld
把累加后的值寫回主內(nèi)存
假設(shè)我們沒有加 synchronized
那么第一步就可能存在著溶弟,三個(gè)線程同時(shí)通過getfield命令,拿到主存中的 n值熄诡,然后三個(gè)線程可很,各自在自己的工作內(nèi)存中進(jìn)行加1操作,但他們并發(fā)進(jìn)行 iadd
命令的時(shí)候凰浮,因?yàn)橹荒芤粋€(gè)進(jìn)行寫我抠,所以其它操作會(huì)被掛起苇本,假設(shè)1線程,先進(jìn)行了寫操作菜拓,在寫完后瓣窄,volatile的可見性,應(yīng)該需要告訴其它兩個(gè)線程纳鼎,主內(nèi)存的值已經(jīng)被修改了俺夕,但是因?yàn)樘炝耍渌鼉蓚€(gè)線程贱鄙,陸續(xù)執(zhí)行 iadd
命令劝贸,進(jìn)行寫入操作,這就造成了其他線程沒有接受到主內(nèi)存n的改變逗宁,從而覆蓋了原來的值映九,出現(xiàn)寫丟失,這樣也就讓最終的結(jié)果少于20000
如何解決
因此這也說明瞎颗,在多線程環(huán)境下 number ++ 在多線程環(huán)境下是非線程安全的件甥,解決的方法有哪些呢?
- 在方法上加入 synchronized
public synchronized void addPlusPlus() {
number ++;
}
運(yùn)行結(jié)果:
[圖片上傳失敗...(image-583e81-1589011343184)]
我們能夠發(fā)現(xiàn)引入synchronized關(guān)鍵字后哼拔,保證了該方法每次只能夠一個(gè)線程進(jìn)行訪問和操作引有,最終輸出的結(jié)果也就為20000
其它解決方法
上面的方法引入synchronized,雖然能夠保證原子性倦逐,但是為了解決number++譬正,而引入重量級的同步機(jī)制,有種 殺雞焉用牛刀
除了引用synchronized關(guān)鍵字外檬姥,還可以使用JUC下面的原子包裝類导帝,即剛剛的int類型的number,可以使用AtomicInteger來代替
/**
* 創(chuàng)建一個(gè)原子Integer包裝類穿铆,默認(rèn)為0
*/
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic() {
// 相當(dāng)于 atomicInter ++
atomicInteger.getAndIncrement();
}
然后同理,繼續(xù)剛剛的操作
// 創(chuàng)建10個(gè)線程斋荞,線程里面進(jìn)行1000次循環(huán)
for (int i = 0; i < 20; i++) {
new Thread(() -> {
// 里面
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
myData.addAtomic();
}
}, String.valueOf(i)).start();
}
最后輸出
// 假設(shè)volatile保證原子性荞雏,那么輸出的值應(yīng)該為: 20 * 1000 = 20000
System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
System.out.println(Thread.currentThread().getName() + "\t finally atomicNumber value: " + myData.atomicInteger);
下面的結(jié)果,一個(gè)是引入synchronized平酿,一個(gè)是使用了原子包裝類AtomicInteger
[圖片上傳失敗...(image-b47bfe-1589011343184)]
字節(jié)碼指令表
為了方便閱讀JVM字節(jié)碼文件凤优,我從網(wǎng)上找了一份字節(jié)碼指令表
引用:https://segmentfault.com/a/1190000008722128
3. volatile 禁止指令重排
計(jì)算機(jī)在執(zhí)行程序時(shí),為了提高性能蜈彼,編譯器和處理器常常會(huì)對指令重排筑辨,一般分為以下三種:
源代碼 -> 編譯器優(yōu)化的重排 -> 指令并行的重排 -> 內(nèi)存系統(tǒng)的重排 -> 最終執(zhí)行指令
單線程環(huán)境里面確保最終執(zhí)行結(jié)果和代碼順序的結(jié)果一致
處理器在進(jìn)行重排序時(shí),必須要考慮指令之間的數(shù)據(jù)依賴性
多線程環(huán)境中線程交替執(zhí)行幸逆,由于編譯器優(yōu)化重排的存在棍辕,兩個(gè)線程中使用的變量能否保證一致性是無法確定的暮现,結(jié)果無法預(yù)測。
指令重排 - example 1
public void mySort() {
int x = 11;
int y = 12;
x = x + 5;
y = x * x;
}
按照正常單線程環(huán)境楚昭,執(zhí)行順序是 1 2 3 4
但是在多線程環(huán)境下栖袋,可能出現(xiàn)以下的順序:
- 2 1 3 4
- 1 3 2 4
上述的過程就可以當(dāng)做是指令的重排,即內(nèi)部執(zhí)行順序抚太,和我們的代碼順序不一樣
但是指令重排也是有限制的塘幅,即不會(huì)出現(xiàn)下面的順序
- 4 3 2 1
因?yàn)樘幚砥髟谶M(jìn)行重排時(shí)候,必須考慮到指令之間的數(shù)據(jù)依賴性
因?yàn)椴襟E 4:需要依賴于 y的申明尿贫,以及x的申明电媳,故因?yàn)榇嬖跀?shù)據(jù)依賴,無法首先執(zhí)行
例子
int a,b,x,y = 0
線程1 | 線程2 |
---|---|
x = a; | y = b; |
b = 1; | a = 2; |
x = 0; y = 0 |
因?yàn)樯厦娴拇a庆亡,不存在數(shù)據(jù)的依賴性匾乓,因此編譯器可能對數(shù)據(jù)進(jìn)行重排
線程1 | 線程2 |
---|---|
b = 1; | a = 2; |
x = a; | y = b; |
x = 2; y = 1 |
這樣造成的結(jié)果,和最開始的就不一致了身冀,這就是導(dǎo)致重排后钝尸,結(jié)果和最開始的不一樣,因此為了防止這種結(jié)果出現(xiàn)搂根,volatile就規(guī)定禁止指令重排珍促,為了保證數(shù)據(jù)的一致性
指令重排 - example 2
比如下面這段代碼
/**
* ResortSeqDemo
*
* @author: 陌溪
* @create: 2020-03-10-16:08
*/
public class ResortSeqDemo {
int a= 0;
boolean flag = false;
public void method01() {
a = 1;
flag = true;
}
public void method02() {
if(flag) {
a = a + 5;
System.out.println("reValue:" + a);
}
}
}
我們按照正常的順序,分別調(diào)用method01() 和 method02() 那么剩愧,最終輸出就是 a = 6
但是如果在多線程環(huán)境下猪叙,因?yàn)榉椒? 和 方法2,他們之間不能存在數(shù)據(jù)依賴的問題仁卷,因此原先的順序可能是
a = 1;
flag = true;
a = a + 5;
System.out.println("reValue:" + a);
但是在經(jīng)過編譯器穴翩,指令,或者內(nèi)存的重排后锦积,可能會(huì)出現(xiàn)這樣的情況
flag = true;
a = a + 5;
System.out.println("reValue:" + a);
a = 1;
也就是先執(zhí)行 flag = true后芒帕,另外一個(gè)線程馬上調(diào)用方法2,滿足 flag的判斷丰介,最終讓a + 5背蟆,結(jié)果為5,這樣同樣出現(xiàn)了數(shù)據(jù)不一致的問題
為什么會(huì)出現(xiàn)這個(gè)結(jié)果:多線程環(huán)境中線程交替執(zhí)行哮幢,由于編譯器優(yōu)化重排的存在带膀,兩個(gè)線程中使用的變量能否保證一致性是無法確定的,結(jié)果無法預(yù)測橙垢。
這樣就需要通過volatile來修飾垛叨,來保證線程安全性
Volatile針對指令重排做了啥
Volatile實(shí)現(xiàn)禁止指令重排優(yōu)化,從而避免了多線程環(huán)境下程序出現(xiàn)亂序執(zhí)行的現(xiàn)象
首先了解一個(gè)概念柜某,內(nèi)存屏障(Memory Barrier)又稱內(nèi)存柵欄嗽元,是一個(gè)CPU指令敛纲,它的作用有兩個(gè):
- 保證特定操作的順序
- 保證某些變量的內(nèi)存可見性(利用該特性實(shí)現(xiàn)volatile的內(nèi)存可見性)
由于編譯器和處理器都能執(zhí)行指令重排的優(yōu)化,如果在指令間插入一條Memory Barrier則會(huì)告訴編譯器和CPU还棱,不管什么指令都不能和這條Memory Barrier指令重排序载慈,也就是說 通過插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化
。 內(nèi)存屏障另外一個(gè)作用是刷新出各種CPU的緩存數(shù)珍手,因此任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本办铡。
[圖片上傳失敗...(image-93dc00-1589011343184)]
也就是過在Volatile的寫 和 讀的時(shí)候,加入屏障琳要,防止出現(xiàn)指令重排的
線程安全獲得保證
工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象導(dǎo)致的可見性問題
- 可通過synchronized或volatile關(guān)鍵字解決寡具,他們都可以使一個(gè)線程修改后的變量立即對其它線程可見
對于指令重排導(dǎo)致的可見性問題和有序性問題
- 可以使用volatile關(guān)鍵字解決,因?yàn)関olatile關(guān)鍵字的另一個(gè)作用就是禁止重排序優(yōu)化
4. volatile 的應(yīng)用
單例模式DCL代碼
首先回顧一下稚补,單線程下的單例模式代碼
/**
* SingletonDemo(單例模式)
*
* @author: 陌溪
* @create: 2020-03-10-16:40
*/
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo () {
System.out.println(Thread.currentThread().getName() + "\t 我是構(gòu)造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if(instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
// 這里的 == 是比較內(nèi)存地址
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
}
}
最后輸出的結(jié)果
[圖片上傳失敗...(image-fae0eb-1589011343184)]
但是在多線程的環(huán)境下童叠,我們的單例模式是否還是同一個(gè)對象了
/**
* SingletonDemo(單例模式)
*
* @author: 陌溪
* @create: 2020-03-10-16:40
*/
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo () {
System.out.println(Thread.currentThread().getName() + "\t 我是構(gòu)造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if(instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
從下面的結(jié)果我們可以看出,我們通過SingletonDemo.getInstance() 獲取到的對象课幕,并不是同一個(gè)厦坛,而是被下面幾個(gè)線程都進(jìn)行了創(chuàng)建,那么在多線程環(huán)境下乍惊,單例模式如何保證呢杜秸?
[圖片上傳失敗...(image-480a34-1589011343184)]
解決方法1
引入synchronized關(guān)鍵字
public synchronized static SingletonDemo getInstance() {
if(instance == null) {
instance = new SingletonDemo();
}
return instance;
}
輸出結(jié)果
[圖片上傳失敗...(image-c6540a-1589011343184)]
我們能夠發(fā)現(xiàn),通過引入Synchronized關(guān)鍵字润绎,能夠解決高并發(fā)環(huán)境下的單例模式問題
但是synchronized屬于重量級的同步機(jī)制撬碟,它只允許一個(gè)線程同時(shí)訪問獲取實(shí)例的方法,但是為了保證數(shù)據(jù)一致性莉撇,而減低了并發(fā)性呢蛤,因此采用的比較少
解決方法2
通過引入DCL Double Check Lock 雙端檢鎖機(jī)制
就是在進(jìn)來和出去的時(shí)候,進(jìn)行檢測
public static SingletonDemo getInstance() {
if(instance == null) {
// 同步代碼段的時(shí)候棍郎,進(jìn)行檢測
synchronized (SingletonDemo.class) {
if(instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
最后輸出的結(jié)果為:
[圖片上傳失敗...(image-a5db72-1589011343184)]
從輸出結(jié)果來看其障,確實(shí)能夠保證單例模式的正確性,但是上面的方法還是存在問題的
DCL(雙端檢鎖)機(jī)制不一定是線程安全的涂佃,原因是有指令重排的存在静秆,加入volatile可以禁止指令重排
原因是在某一個(gè)線程執(zhí)行到第一次檢測的時(shí)候,讀取到 instance 不為null巡李,instance的引用對象可能沒有完成實(shí)例化。因?yàn)?instance = new SingletonDemo()扶认;可以分為以下三步進(jìn)行完成:
- memory = allocate(); // 1侨拦、分配對象內(nèi)存空間
- instance(memory); // 2、初始化對象
- instance = memory; // 3辐宾、設(shè)置instance指向剛剛分配的內(nèi)存地址狱从,此時(shí)instance != null
但是我們通過上面的三個(gè)步驟膨蛮,能夠發(fā)現(xiàn),步驟2 和 步驟3之間不存在 數(shù)據(jù)依賴關(guān)系季研,而且無論重排前 還是重排后敞葛,程序的執(zhí)行結(jié)果在單線程中并沒有改變,因此這種重排優(yōu)化是允許的与涡。
- memory = allocate(); // 1惹谐、分配對象內(nèi)存空間
- instance = memory; // 3、設(shè)置instance指向剛剛分配的內(nèi)存地址驼卖,此時(shí)instance != null氨肌,但是對象還沒有初始化完成
- instance(memory); // 2、初始化對象
這樣就會(huì)造成什么問題呢酌畜?
也就是當(dāng)我們執(zhí)行到重排后的步驟2怎囚,試圖獲取instance的時(shí)候,會(huì)得到null桥胞,因?yàn)閷ο蟮某跏蓟€沒有完成恳守,而是在重排后的步驟3才完成,因此執(zhí)行單例模式的代碼時(shí)候贩虾,就會(huì)重新在創(chuàng)建一個(gè)instance實(shí)例
指令重排只會(huì)保證串行語義的執(zhí)行一致性(單線程)催烘,但并不會(huì)關(guān)系多線程間的語義一致性
所以當(dāng)一條線程訪問instance不為null時(shí),由于instance實(shí)例未必已初始化完成整胃,這就造成了線程安全的問題
所以需要引入volatile颗圣,來保證出現(xiàn)指令重排的問題,從而保證單例模式的線程安全性
private static volatile SingletonDemo instance = null;
最終代碼
/**
* SingletonDemo(單例模式)
*
* @author: 陌溪
* @create: 2020-03-10-16:40
*/
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo () {
System.out.println(Thread.currentThread().getName() + "\t 我是構(gòu)造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if(instance == null) {
// 同步代碼段的時(shí)候屁使,進(jìn)行檢測
synchronized (SingletonDemo.class) {
if(instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
// // 這里的 == 是比較內(nèi)存地址
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}