1、線程安全
多個(gè)線程對(duì)同一個(gè)共享變量進(jìn)行讀寫操作時(shí)可能產(chǎn)生不可預(yù)見的結(jié)果,這就是線程安全問(wèn)題。
線程安全的核心點(diǎn)就是共享變量,只有在共享變量的情況下才會(huì)有線程安全問(wèn)題。這里說(shuō)的共享變量,是指多個(gè)線程都能訪問(wèn)的變量,一般包括成員變量和靜態(tài)變量,方法內(nèi)定義的局部變量不屬于共享變量的范圍。
線程安全問(wèn)題示例:
import lombok.extern.slf4j.Slf4j;
/**
* @Author FengJian
* @Date 2021/1/27 10:59
* @Version 1.0
*/
@Slf4j(topic = "c.ThreadSafeTest")
public class ThreadSafeTest {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1"){
@Override
public void run() {
for (int i = 0;i < 5000;i++){
count++;
}
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
for (int i = 0;i < 5000;i++){
count--;
}
}
};
t1.start();
t2.start();
/**
* join方法:使main線程與t1、t2線程同步執(zhí)行,即t1、t2線程都執(zhí)行完,main線程才會(huì)繼續(xù)執(zhí)行(但t1、t2之間依然是并行執(zhí)行的)
* 主要是為了等待兩個(gè)線程執(zhí)行完后,在main線程打印count的值
*/
t1.join();
t2.join();
log.debug("count的值為:{}",count);
}
}
運(yùn)行上述代碼三次的結(jié)果如下:
[main] DEBUG c.ThreadSafeTest - count的值為:-904
[main] DEBUG c.ThreadSafeTest - count的值為:-2206
[main] DEBUG c.ThreadSafeTest - count的值為:73
在上述代碼中,線程t1中count進(jìn)行5000次自增操作,而線程t2中count則進(jìn)行5000次自減操作。在兩個(gè)線程都運(yùn)行結(jié)束后,按照預(yù)期結(jié)果,count的值應(yīng)為0。但由打印結(jié)果可知,count的值并不為0,且每次運(yùn)行的結(jié)果都不一樣。這就是多線程對(duì)共享變量進(jìn)行操作出現(xiàn)的不可預(yù)見的結(jié)果,即常說(shuō)的線程安全問(wèn)題。
而線程安全,則指的是在多線程環(huán)境下,程序可以始終執(zhí)行正確的行為,符合預(yù)期的邏輯。具體到上述代碼,就是不論執(zhí)行多少次,在t1、t2線程執(zhí)行完畢后,count的值都應(yīng)該始終符合預(yù)期的結(jié)果0。上述代碼明顯是線程不安全的。
2、出現(xiàn)線程安全的原因
線程安全是使用多線程必定會(huì)面臨的問(wèn)題,導(dǎo)致線程不安全的主要原因有以下三點(diǎn):
- ①原子性:一個(gè)或者多個(gè)操作在 CPU 執(zhí)行的過(guò)程中被中斷
- ②可見性:一個(gè)線程對(duì)共享變量的修改,另外一個(gè)線程不能立刻看到
- ③有序性:序執(zhí)行的順序沒有按照代碼的先后順序執(zhí)行
2.1、原子性
2.1.1 什么是原子性問(wèn)題
原子性問(wèn)題,其實(shí)說(shuō)的是原子性操作。即一個(gè)或多個(gè)操作,應(yīng)該是一個(gè)不可分的整體,這些操作要么全部執(zhí)行并且不被打斷,要么就都不執(zhí)行。
以上述代碼中的count的自增(count++)和自減(count--)為例。
count++和count--看似只有一行代碼,但實(shí)際上這一行代碼在編譯后的字節(jié)碼指令以及在JVM執(zhí)行的對(duì)應(yīng)操作如下:
count++:
getstatic count //獲取靜態(tài)變量count的值
iconst_1 //準(zhǔn)備常量1
iadd //自增
putstatic count //將修改后的值存入靜態(tài)變量count
count--:
getstatic count //獲取靜態(tài)變量count的值
iconst_1 //準(zhǔn)備常量1
isub //自減
putstatic count //將修改后的值存入靜態(tài)變量count
由此可知,count自增或自減的操作,并不是一個(gè)原子操作,即中間過(guò)程是有可能被打斷的。
count自增自減操作需要四個(gè)步驟(指令)才能完成,這意味著如果這執(zhí)行這四個(gè)步驟的某一步時(shí),線程發(fā)生了上下文切換,那么自增自減操作將被打斷暫停。
如果使用單線程來(lái)執(zhí)行自增自減操作,這實(shí)際上并無(wú)問(wèn)題:

上圖為單線程執(zhí)行count自增自減的一次過(guò)程,可以看出在沒有線程上下文切換的情況下,即使自增自減不是原子操作,count的最后結(jié)果都會(huì)是0。
但在多線程環(huán)境下,就會(huì)出現(xiàn)問(wèn)題了:

可以看到由于自增自減不是原子操作,因此在線程t1執(zhí)行自增過(guò)程中,如果進(jìn)行上下文切換,則將導(dǎo)致線程t1還沒來(lái)得及把count = 1 寫入主存,count的值就被t2線程讀取,所以在最后,線程t2自減得出的值-1寫入主存后,會(huì)被線程t1覆蓋,變?yōu)?。
這結(jié)果明顯是不符合我們的預(yù)期的,實(shí)際上,上述圖片展示的只是一種可能的結(jié)果。還有可能是t2寫入count的步驟是最后執(zhí)行的,那么最后count的值將為-1。
這就是由于非原子操作帶來(lái)的多線程訪問(wèn)共享變量出現(xiàn)不符合預(yù)期的結(jié)果,即由于原子性帶來(lái)的線程安全問(wèn)題。
上面示例中兩個(gè)線程t1、t2分別執(zhí)行count++和count--出現(xiàn)的問(wèn)題,就是由于原子性帶來(lái)的線程安全問(wèn)題。
2.1.2、原子性問(wèn)題解決辦法
解決辦法就是將count++和count--的操作變?yōu)樵硬僮?,Java中的實(shí)現(xiàn)方法是:
- ①上鎖:使用synchronized
只需要?jiǎng)?chuàng)建一個(gè)對(duì)象作為鎖,并在訪問(wèn)count時(shí)用synchronized進(jìn)行加鎖即可。
static int count = 0;
static Object lock = new Object(); //鎖對(duì)象
synchronized(lock){
count++;
}
synchronized(lock){
count--;
}
上鎖后,執(zhí)行自增自減的示意圖如下:

由于鎖的存在,則保證了不持有鎖的t2線程會(huì)被阻塞,直到t1線程執(zhí)行自增完畢,并釋放鎖。在這一過(guò)程中,雖然依舊存在線程的上下文切換,但是t2線程是無(wú)法對(duì)共享變量count進(jìn)行操作的,因此保證了t1線程中count++操作的原子性。
因此使用synchronized鎖可以解決原子性帶來(lái)的線程安全問(wèn)題。
②、循環(huán)CAS操作
其基本思路就是循環(huán)進(jìn)行CAS操作(compare and swap,比較并交換)。即對(duì)共享變量進(jìn)行計(jì)算前,線程會(huì)先將該共享變量保存一份舊值a,計(jì)算完畢后得出結(jié)果值b。在將b從線程的本地內(nèi)存刷新回主內(nèi)存前,會(huì)先比較主內(nèi)存中的值是否和a一致。如果一致,則將b刷新回主內(nèi)存。若不一致,則一直循環(huán)比較,直到主內(nèi)存中的值與a一致,才把共享變量的值設(shè)為b,操作才結(jié)束。
在Java中,使用CAS操作保證原子性的具體實(shí)現(xiàn)就是Lock和原子類(AtomicInteger)。它們都是通過(guò)使用unsafe的compareAndSwap方法實(shí)現(xiàn)CAS操作保證原子性的。
Lock的使用:
static int count = 0;
static Lock lock = new Lock (); //鎖對(duì)象
lock.lock(); //加鎖
count++;
lock.unlock(); //解鎖
lock.lock(); //加鎖
count--;
lock.unlock(); //解鎖
原子類的使用:
static AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); //自增
count.decrementAndGet(); //自減
以上都是Java中可以保證原子操作的具體方法,它們各有優(yōu)缺點(diǎn),要看具體的場(chǎng)景來(lái)選擇最佳的使用,以此來(lái)解決原子性帶來(lái)的線程安全問(wèn)題。
2.2、可見性
2.2.1、什么是可見性問(wèn)題
可見性實(shí)際上指的是內(nèi)存可見性問(wèn)題??偟膩?lái)說(shuō)就是一個(gè)線程對(duì)共享變量的修改,另外一個(gè)線程不能立刻看到,從而產(chǎn)生的線程安全問(wèn)題。
我們知道,CPU要從內(nèi)存中讀取出數(shù)據(jù)來(lái)進(jìn)行計(jì)算,但實(shí)際上CPU并不總是直接從內(nèi)存中讀取數(shù)據(jù)。由于CPU和內(nèi)存間(常稱之為主存)的速度不匹配(CPU的速度比主存快得多),為了有效利用CPU,使用多級(jí)cache的機(jī)制,如圖

上圖所示是一個(gè)雙核心的CPU系統(tǒng)架構(gòu),每個(gè)核心都有自己的控制器和運(yùn)算器,也都有自己的一級(jí)緩存,還有可能有所有CPU核心共享的二級(jí)緩存,每個(gè)核心都可以獨(dú)立運(yùn)行線程。
因此,CPU讀取數(shù)據(jù)的順序是:寄存器-高速緩存-主存。主存中的部分?jǐn)?shù)據(jù),會(huì)先拷貝一份放到cache中,當(dāng)CPU計(jì)算時(shí),會(huì)直接從cache中讀取數(shù)據(jù),計(jì)算完畢后再將計(jì)算結(jié)果放置到cache中,最后在主存中刷新計(jì)算結(jié)果。所以每個(gè)CPU都會(huì)擁有一份拷貝。
以上只是CPU訪問(wèn)內(nèi)存,進(jìn)行計(jì)算的基本方式。實(shí)際上,不同的硬件,訪問(wèn)過(guò)程會(huì)存在不同程度的差異。比如,不同的計(jì)算機(jī),CPU和主存間可能會(huì)存在三級(jí)緩存、四級(jí)緩存、五級(jí)緩存等等的情況。
為了屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,實(shí)現(xiàn)讓 Java 程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問(wèn)效果,定義了Java的內(nèi)存模型(Java Memory Model,JMM)。
JMM 的主要目標(biāo)是定義程序中各個(gè)變量的訪問(wèn)規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到主存和從主存中取出變量這樣的底層細(xì)節(jié)。這里的變量指的是能夠被多個(gè)線程共享的變量,它包括了實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,方法內(nèi)的局部變量和方法的參數(shù)為線程私有,不受JMM的影響。
Java的內(nèi)存模型如下:

Java內(nèi)存模型中的本地內(nèi)存,對(duì)應(yīng)的就是CPU結(jié)構(gòu)圖中的cache1或者cache2。它實(shí)際上并不真實(shí)存在,其包含了緩存、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器的優(yōu)化。
JMM規(guī)定:將所有共享變量放到主內(nèi)存中,當(dāng)線程使用變量時(shí),會(huì)把其中的變量復(fù)制到自己的本地內(nèi)存,線程讀寫時(shí)操作的是本地內(nèi)存中的變量副本。一個(gè)線程不能訪問(wèn)其他線程的本地內(nèi)存。
這樣的情況下,如果有一個(gè)變量i在線程A、B的本地內(nèi)存中都有一份副本。此時(shí),若線程A想修改i的值,在線程A將修改后的值放入到本地內(nèi)存,但又未刷新回主內(nèi)存時(shí),如果線程B讀取變量i的值,則讀到的是未修改時(shí)的值,這就造成了讀寫共享變量出現(xiàn)不可預(yù)期的結(jié)果,產(chǎn)生線程安全問(wèn)題。
有代碼如下:
/**
* @Author FengJian
* @Date 2021/2/21 23:47
* @Version 1.0
*/
@Slf4j(topic = "c.ThreadSafeTest")
public class ThreadSafe02 {
private static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread My_Thread = new Thread(new Runnable() {
@Override
public void run() {
while (run) {
}
}
}, "My_Thread");
My_Thread.start(); //啟動(dòng)My_Thread線程
log.debug(Thread.currentThread().getName()+"正在休眠@"+new SimpleDateFormat("hh:mm:ss").format(new Date())+"--"+run);
Thread.sleep(1000); //主線程休眠1s
run = false; //改變My_Thread線程運(yùn)行條件
log.debug(Thread.currentThread().getName()+"正在運(yùn)行@"+new SimpleDateFormat("hh:mm:ss").format(new Date())+"--"+run);
}
}
從運(yùn)行結(jié)果發(fā)現(xiàn),即使在主線程中修改了共享變量run的值,My_Thread線程依然在循環(huán)并不會(huì)停止:

其原因就是main線程對(duì)共享變量run的修改,另外一個(gè)線程My_Thread并不能立刻看到:

這就是由于內(nèi)存可見性帶來(lái)的多線程訪問(wèn)共享變量出現(xiàn)不符合預(yù)期的結(jié)果,即由于可見性帶來(lái)的線程安全問(wèn)題。
2.2.2、可見性問(wèn)題解決辦法
解決辦法就是保證共享變量的可見性,具體實(shí)現(xiàn)就是任何對(duì)共享變量的訪問(wèn)都要從共享內(nèi)存(主內(nèi)存)中獲取。在Java中的實(shí)現(xiàn)方法是:
- ①加鎖,synchronized和Lock都可以保證
線程在加鎖時(shí),會(huì)清空本地內(nèi)存中共享變量的值,共享變量的使用需要從主內(nèi)存中重新獲取。而在釋放鎖資源時(shí),則必須先把此共享變量同步回主內(nèi)存中。
由于鎖的存在,未持有鎖的線程并不能操作共享變量,而當(dāng)阻塞的線程獲得鎖時(shí),主內(nèi)存中共享變量的值已經(jīng)刷新過(guò)了,因此線程修改共享變量對(duì)其他線程是可見的。這保證了共享變量的可見性,可以解決內(nèi)存可見性產(chǎn)生的線程安全問(wèn)題。
- ②使用volatile修飾共享變量
當(dāng)一個(gè)變量被聲明為volitale時(shí),線程在寫入變量時(shí),不會(huì)把值緩存本地內(nèi)存,而是會(huì)立即把值刷新回主存,而當(dāng)要讀取該共享變量時(shí),線程則會(huì)先清空本地內(nèi)存中的副本值,從主存中重新獲取。這些也都保證了內(nèi)存的可見性。
優(yōu)先使用volatile關(guān)鍵字來(lái)解決可見性問(wèn)題,加鎖消耗的資源更多。
2.3、有序性
2.3.1、什么是有序性問(wèn)題
有序性,實(shí)際上是指令的重排序問(wèn)題。
我們知道,CPU的執(zhí)行速度是比內(nèi)存要快出很多個(gè)數(shù)量級(jí)的。CPU為了執(zhí)行效率,會(huì)把CPU指令進(jìn)行重新排序。即我們編寫的Java代碼并不一定按照順序一行一行的往下執(zhí)行,處理器會(huì)根據(jù)需要重新排序這些指令,稱為指令并行重排序。
同時(shí),JIT編譯器也會(huì)在代碼編譯的時(shí)候?qū)Υa進(jìn)行重新整理,最大限度的去優(yōu)化代碼的執(zhí)行效率,稱為編譯器的重排序。
而又由于處理器與主存之間會(huì)使用緩存和讀/寫緩沖機(jī)制,因此從主存加載和存儲(chǔ)操作也有可能是經(jīng)過(guò)指令重排序的,稱為內(nèi)存系統(tǒng)重排序。
綜上所述,在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器常常會(huì)對(duì)指令進(jìn)行重排序,再加上主內(nèi)存和處理器的緩存,Java源碼經(jīng)過(guò)層層的重排序,最后才得出最終結(jié)果。

由圖可知,從Java源碼到最后的執(zhí)行指令,會(huì)經(jīng)歷3種重排序的優(yōu)化。若有ava代碼如下:
int a = 2; //A
int b = 3; //B
int c = a*b; //C
經(jīng)過(guò)上述3種重排序后,語(yǔ)句A和語(yǔ)句B的執(zhí)行順序是可能互換的,并且這種互換并不影響代碼的正確性。但是我們發(fā)現(xiàn)語(yǔ)句C則不能和A、B互換,否則得出的結(jié)果將不正確,因?yàn)樗麄冎g存在著數(shù)據(jù)依賴關(guān)系,即語(yǔ)句C的數(shù)據(jù)依賴A和B得出。
由此,我們可以發(fā)現(xiàn),以上3種指令的重排序并不能隨意排序,他們需要遵守一定的規(guī)則,以保證程序的正確性。
- ①as-if-serial語(yǔ)義
as-if-serial語(yǔ)義是指:不管怎么樣重排序,單線程程序的執(zhí)行結(jié)果都不能被改變。即不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作進(jìn)行重排序。
編譯器、處理器進(jìn)行指令重排序優(yōu)化時(shí)都必須遵守as-if-serial語(yǔ)義。即在單線程的情況下,指令重排序只能對(duì)不影響處理結(jié)果的部分進(jìn)行重排序。
以上述語(yǔ)句A、B、C為例,存在數(shù)據(jù)依賴關(guān)系的語(yǔ)句C和A或B不能被重排序:

as-if-serial語(yǔ)義把單線程程序保護(hù)起來(lái)了,遵守該語(yǔ)義的編譯器、處理器等使我們編寫單線程有一個(gè)錯(cuò)覺:?jiǎn)尉€程程序是按照源代碼的順序來(lái)執(zhí)行的。實(shí)際上在由于as-if-serial語(yǔ)義的存在,我們編寫單線程時(shí),完全可以認(rèn)為源代碼是按照順序執(zhí)行的,因?yàn)榧词勾a被進(jìn)行了重排序,其結(jié)果也不會(huì)改變,同時(shí)單線程中也無(wú)需擔(dān)心內(nèi)存可見性問(wèn)題。
as-if-serial語(yǔ)義的核心思想是:不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作進(jìn)行重排序。
那么數(shù)據(jù)依賴類型有哪些呢?如下表所示:

以上三種依賴關(guān)系,一旦重排序兩個(gè)操作的執(zhí)行順序,其結(jié)果就會(huì)改變,所以依照as-if-serial語(yǔ)義,Java在單線程的情況下不會(huì)對(duì)這三種依賴關(guān)系進(jìn)行重排序(多線程情況不符合此情況)。
as-if-serial語(yǔ)義是基于數(shù)據(jù)依賴關(guān)系的,但它無(wú)法保證多線程環(huán)境下,重排序之后程序執(zhí)行結(jié)果的正確性。
有代碼如下:
/**
* @Author FengJian
* @Date 2021/2/24 16:44
* @Version 1.0
*/
@Slf4j(topic = "c.HappensBeforeTest")
public class HappensBeforeTest {
static int a = 0;
static boolean finish = false;
public static void main(String[] args) {
Thread t1 = new Thread("t1"){
@Override
public void run() {
if(finish){
log.debug("a*a:"+a*a);
}
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
a = 2;
finish = true;
}
};
t2.start();
t1.start();
}
}
關(guān)于上述代碼,我們先忽略內(nèi)存可見性的問(wèn)題(即線程t2修改了a和finish,但t1可能看不到的緩存問(wèn)題)。在此前提下如果成功打印a*a的值,那么結(jié)果應(yīng)該為4。
但實(shí)際上a*a打印的結(jié)果還可能為0,這是由于指令重排序的存在導(dǎo)致的。
在線程t2中,由于a = 2;和finish = true;沒有數(shù)據(jù)依賴關(guān)系,依照as-if-serial語(yǔ)義,可以對(duì)這兩條語(yǔ)句進(jìn)行重排
序,因此會(huì)出現(xiàn)finish = true;的指令比a = 2;先執(zhí)行的情況。
如果在先執(zhí)行finish = true;,而a = 2;沒有執(zhí)行時(shí)發(fā)生線程上下文切換,輪到線程t1執(zhí)行,那么t1線程中的if語(yǔ)句條件為真,而a的值依然為初始值0,則a*a的結(jié)果為0。

可以看出,即使在假設(shè)沒有內(nèi)存可見性問(wèn)題的前提下,上述代碼的結(jié)果也是不可預(yù)期的,因此上述代碼也是線程不安全的,其原因就是重排序破壞了多線程程序的語(yǔ)義。
- ②happens-before規(guī)則
既然是重排序出現(xiàn)問(wèn)題,那么解決思路就是禁止重排序。但是也要注意不能全部禁用重排序,重排序的目的是為了提升執(zhí)行效率,如果全部禁用那么Java程序的性能將會(huì)很差。所以,應(yīng)該做到的是部分禁用,Java的內(nèi)存模型提供了一個(gè)可用于多線程環(huán)境,也適用于單線程環(huán)境的規(guī)則:happens-before規(guī)則。
happens-before規(guī)則的定義如下:A happens-before B,那么操作A的執(zhí)行結(jié)果對(duì)操作B是可見的,且操作A的執(zhí)行順序排在操作B之前。這里的操作A和操作B可以在同一個(gè)線程中,也可以在不同線程中。
注意:執(zhí)行順序只是happens-before向開發(fā)人員做的保證,實(shí)際上在處理器和編譯器上執(zhí)行時(shí)并不一定按照操作A排在操作B之前執(zhí)行。
如果重排序之后,依然可以保證與先A后B的執(zhí)行結(jié)果一樣,那么進(jìn)行重排序也是可以的。也就是說(shuō),符合happens-before的操作,只要不改變執(zhí)行結(jié)果,處理器和編譯器怎么優(yōu)化(重排序)都行。
只是我們開發(fā)人員可以直接認(rèn)為操作A的執(zhí)行順序排在操作B之前。
happens-before保證操作A的執(zhí)行結(jié)果對(duì)B可見,依靠這個(gè)原則,可以解決多線程環(huán)境下內(nèi)存可見性和有序性問(wèn)題。
回到代碼:
/**線程t1**/
if(finish){
a*a;
}
/**線程t2**/
a = 2;
finish = true;
一共有四個(gè)操作a = 2;、finish = true;、if(finish)、a*a;,想要上述代碼達(dá)到線程安全(即打印都正確輸出4),只需要:

即在t2線程計(jì)算a*a;和if(finish);之前,需要知道t1線程中a = 2;和finish = true;(t2線程對(duì)t1線程的結(jié)果可見)。
要達(dá)到這一目的,就需要上圖中,①和②所示的happens-before關(guān)系。
那要如何達(dá)到呢?這就需要了解happens-before的六大具體規(guī)則了(兩個(gè)操作,只需要符合其中任何一條就可以認(rèn)為是happens-before關(guān)系):
- ①程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,按照程序順序,前面的操作 happens-before 于該線程中的任意后續(xù)操作。
以上述代碼為例:
/**線程t2**/
a = 2; //操作1
finish = true; //操作2
/**線程t1**/
if(finish ); //操作3
a*a; //操作4
操作1 happens-before 操作2
操作3 happens-before 操作4
- ②監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖,happens-before于隨后對(duì)這個(gè)鎖的加鎖。
synchronized (lock) { //加鎖
// x是共享變量,初始值=10
if (x < 12) {
x = 12;
}
} //解鎖
若有兩個(gè)線程A、B,先后執(zhí)行這段代碼。則線程A執(zhí)行完畢后X = 12并釋放鎖。而線程B獲得鎖后,進(jìn)入代碼塊,在if中取X值判斷是否小于12。
此時(shí) 線程A中X=12的操作 happens-before 線程B中取X值判斷的操作(即線程B能看到線程A中執(zhí)行的X=12的結(jié)果)
- ③volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀。
volatile int x = 10;
/**線程t1**/
x = 11; //操作1
/**線程t2**/
int y = x; //操作2
操作1 happens-before 操作2
- ④傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- ⑤start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動(dòng)線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。
- ⑥join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。
以上就是happens-before的六大常用規(guī)則(全部有八種,但后面兩種應(yīng)該很少用到)
2.3.2、有序性問(wèn)題解決辦法
解決有序性問(wèn)題,實(shí)際上就是要運(yùn)用以上提到的兩種規(guī)則,as-if-serial語(yǔ)義解決了單線程程序的有序性問(wèn)題,而happens-before關(guān)系則能解決多線程程序的有序性問(wèn)題。
再回顧一下原始代碼,這是一段存在有序性問(wèn)題線程不安全的代碼,我們要利用happens-before關(guān)系解決有序性問(wèn)題:
public class HappensBeforeTest {
static int a = 0;
static boolean finish = false;
public static void main(String[] args) {
Thread t1 = new Thread("t1"){
@Override
public void run() {
if(finish){
log.debug("a*a:"+a*a);
}
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
a = 2;
finish = true;
}
};
t2.start();
t1.start();
}
}
提取一下關(guān)鍵的操作,如下嗷:
/**線程t1**/
if(finish){
a*a;
}
/**線程t2**/
a = 2;
finish = true;
我們的目標(biāo)是運(yùn)用happens-before的六大常用規(guī)則達(dá)到如下圖的happens-before關(guān)系,以實(shí)現(xiàn)上訴代碼的線程安全
解決辦法如下:
-
①、方法一:運(yùn)用volatile修飾變量
使用到happens-before規(guī)則中的程序順序規(guī)則、volatile變量規(guī)則和傳遞性。
首先,按照程序順序規(guī)則,可以知道如下的happens-before關(guān)系:
線程t1 線程t2
if(finish) happens-before a*a; a = 2; happens-before finish = true;
這由線程中的代碼很容易就能得出。接下來(lái)運(yùn)用volatile變量規(guī)則,需要用volatile修飾一個(gè)變量,我們選變量finish。即初始化時(shí)代碼改為為volatile static boolean finish = false;。
那么根據(jù)volatile變量規(guī)則,可知對(duì)finish的寫要happens-before于對(duì)finish的讀。
因此給finish加上volatile關(guān)鍵字后,就可以達(dá)到如下效果:

volatile關(guān)鍵字不僅可以保證內(nèi)存可見性問(wèn)題,同時(shí)依照happens-before的volatile變量規(guī)則,對(duì)于volatile修飾的變量,要保證對(duì)該變量寫的結(jié)果要對(duì)讀的操作可見,因此volatile禁止對(duì)有讀寫操作的volatile修飾的變量進(jìn)行重排序。
也就是說(shuō),volatile關(guān)鍵字不僅可以解決可見性問(wèn)題,還可以解決有序性問(wèn)題。
最后,通過(guò)傳遞性??芍?/strong>

可知,圖示的三和五,就是我們的目標(biāo)。到此,我們利用happens-before關(guān)系保證了代碼的可見性和有序性問(wèn)題。
雖然分析的過(guò)程比較長(zhǎng),但是在原代碼中,我們實(shí)際上只改動(dòng)了一行代碼。即將static boolean finish = false;改為volatile static boolean finish = false;而已,就可以使我們的代碼改變線程安全的。
這就是運(yùn)用volatile修飾變量來(lái)解決線程安全的辦法。volatile直接通過(guò)禁止相關(guān)的重排序來(lái)達(dá)到有序性的目的。
-
②、方法二:加鎖,synchronized
這個(gè)應(yīng)該比較容易理解,對(duì)相關(guān)代碼加鎖后,同一時(shí)刻就只有一個(gè)線程在執(zhí)行,也就相當(dāng)于對(duì)相關(guān)變量的操作,是保證有序的。
不過(guò)synchronized并不像volatile一樣禁止指令重排序,實(shí)際上synchronized塊內(nèi)部的代碼指令依然是可以進(jìn)行重排序優(yōu)化的。
3、小結(jié)
多個(gè)線程對(duì)同一個(gè)共享變量進(jìn)行讀寫操作時(shí)就可能產(chǎn)生不可預(yù)見的結(jié)果,就是線程安全問(wèn)題。其重點(diǎn)是多線程對(duì)共享變量進(jìn)行讀和寫,如果只有讀,并不會(huì)有線程安全問(wèn)題。
線程安全的原因有:①線程切換帶來(lái)的原子性問(wèn)題②緩存帶來(lái)的可見性問(wèn)題③指令重排序帶來(lái)的原子性問(wèn)題。
線程安全的解決辦法:①對(duì)于原子性問(wèn)題,使用鎖synchronized和Lock、或者使用原子類(AtomicInteger等)②對(duì)于可見性問(wèn)題:使用鎖synchronized和Lock,或者使用volatile關(guān)鍵字③對(duì)于有序性問(wèn)題:使用鎖synchronized和Lock,或者使用volatile關(guān)鍵字
