日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

【JAVA并發(fā)第四篇】線程安全

 行者花雕 2021-04-27

1、線程安全

多個線程對同一個共享變量進(jìn)行讀寫操作時可能產(chǎn)生不可預(yù)見的結(jié)果,這就是線程安全問題。

線程安全的核心點(diǎn)就是共享變量,只有在共享變量的情況下才會有線程安全問題。這里說的共享變量,是指多個線程都能訪問的變量,一般包括成員變量和靜態(tài)變量,方法內(nèi)定義的局部變量不屬于共享變量的范圍。

線程安全問題示例:

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線程才會繼續(xù)執(zhí)行(但t1、t2之間依然是并行執(zhí)行的)
         * 主要是為了等待兩個線程執(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次自減操作。在兩個線程都運(yùn)行結(jié)束后,按照預(yù)期結(jié)果,count的值應(yīng)為0。但由打印結(jié)果可知,count的值并不為0,且每次運(yùn)行的結(jié)果都不一樣。這就是多線程對共享變量進(jìn)行操作出現(xiàn)的不可預(yù)見的結(jié)果,即常說的線程安全問題。

而線程安全,則指的是在多線程環(huán)境下,程序可以始終執(zhí)行正確的行為,符合預(yù)期的邏輯。具體到上述代碼,就是不論執(zhí)行多少次,在t1、t2線程執(zhí)行完畢后,count的值都應(yīng)該始終符合預(yù)期的結(jié)果0。上述代碼明顯是線程不安全的。

2、出現(xiàn)線程安全的原因

線程安全是使用多線程必定會面臨的問題,導(dǎo)致線程不安全的主要原因有以下三點(diǎn):

①原子性:一個或者多個操作在 CPU 執(zhí)行的過程中被中斷
②可見性:一個線程對共享變量的修改,另外一個線程不能立刻看到
③有序性:序執(zhí)行的順序沒有按照代碼的先后順序執(zhí)行

2.1、原子性

2.1.1 什么是原子性問題

原子性問題,其實(shí)說的是原子性操作。即一個或多個操作,應(yīng)該是一個不可分的整體,這些操作要么全部執(zhí)行并且不被打斷,要么就都不執(zhí)行。

以上述代碼中的count的自增(count++)和自減(count--)為例。

count++count--看似只有一行代碼,但實(shí)際上這一行代碼在編譯后的字節(jié)碼指令以及在JVM執(zhí)行的對應(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自增或自減的操作,并不是一個原子操作,即中間過程是有可能被打斷的。

count自增自減操作需要四個步驟(指令)才能完成,這意味著如果這執(zhí)行這四個步驟的某一步時,線程發(fā)生了上下文切換,那么自增自減操作將被打斷暫停。

如果使用單線程來執(zhí)行自增自減操作,這實(shí)際上并無問題:
在這里插入圖片描述

上圖為單線程執(zhí)行count自增自減的一次過程,可以看出在沒有線程上下文切換的情況下,即使自增自減不是原子操作,count的最后結(jié)果都會是0。

但在多線程環(huán)境下,就會出現(xiàn)問題了:

在這里插入圖片描述
可以看到由于自增自減不是原子操作,因此在線程t1執(zhí)行自增過程中,如果進(jìn)行上下文切換,則將導(dǎo)致線程t1還沒來得及把count = 1 寫入主存,count的值就被t2線程讀取,所以在最后,線程t2自減得出的值-1寫入主存后,會被線程t1覆蓋,變?yōu)?。

這結(jié)果明顯是不符合我們的預(yù)期的,實(shí)際上,上述圖片展示的只是一種可能的結(jié)果。還有可能是t2寫入count的步驟是最后執(zhí)行的,那么最后count的值將為-1。

這就是由于非原子操作帶來的多線程訪問共享變量出現(xiàn)不符合預(yù)期的結(jié)果,即由于原子性帶來的線程安全問題。

上面示例中兩個線程t1、t2分別執(zhí)行count++和count--出現(xiàn)的問題,就是由于原子性帶來的線程安全問題。

2.1.2、原子性問題解決辦法

解決辦法就是將count++和count--的操作變?yōu)樵硬僮鳎琂ava中的實(shí)現(xiàn)方法是:

①上鎖:使用synchronized

只需要創(chuàng)建一個對象作為鎖,并在訪問count時用synchronized進(jìn)行加鎖即可。

 static int count = 0;
 static Object lock = new Object(); //鎖對象
 
 synchronized(lock){ 
     count++;
 }

 synchronized(lock){ 
     count--;
 }

上鎖后,執(zhí)行自增自減的示意圖如下:

在這里插入圖片描述
由于鎖的存在,則保證了不持有鎖的t2線程會被阻塞,直到t1線程執(zhí)行自增完畢,并釋放鎖。在這一過程中,雖然依舊存在線程的上下文切換,但是t2線程是無法對共享變量count進(jìn)行操作的,因此保證了t1線程中count++操作的原子性。

因此使用synchronized鎖可以解決原子性帶來的線程安全問題。

②、循環(huán)CAS操作

其基本思路就是循環(huán)進(jìn)行CAS操作(compare and swap,比較并交換)。即對共享變量進(jìn)行計算前,線程會先將該共享變量保存一份舊值a,計算完畢后得出結(jié)果值b。在將b從線程的本地內(nèi)存刷新回主內(nèi)存前,會先比較主內(nèi)存中的值是否和a一致。如果一致,則將b刷新回主內(nèi)存。若不一致,則一直循環(huán)比較,直到主內(nèi)存中的值與a一致,才把共享變量的值設(shè)為b,操作才結(jié)束。

在Java中,使用CAS操作保證原子性的具體實(shí)現(xiàn)就是Lock和原子類(AtomicInteger)。它們都是通過使用unsafe的compareAndSwap方法實(shí)現(xiàn)CAS操作保證原子性的。

Lock的使用:

static int count = 0;
static Lock lock = new Lock (); //鎖對象

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),要看具體的場景來選擇最佳的使用,以此來解決原子性帶來的線程安全問題。

2.2、可見性

2.2.1、什么是可見性問題

可見性實(shí)際上指的是內(nèi)存可見性問題??偟膩碚f就是一個線程對共享變量的修改,另外一個線程不能立刻看到,從而產(chǎn)生的線程安全問題。

在上一篇筆記【JAVA并發(fā)第三篇】線程間通信 中的通過共享內(nèi)存進(jìn)行通信實(shí)際上講的就是內(nèi)存可見性問題。這里再從線程安全的角度講述一遍。

我們知道,CPU要從內(nèi)存中讀取出數(shù)據(jù)來進(jìn)行計算,但實(shí)際上CPU并不總是直接從內(nèi)存中讀取數(shù)據(jù)。由于CPU和內(nèi)存間(常稱之為主存)的速度不匹配(CPU的速度比主存快得多),為了有效利用CPU,使用多級cache的機(jī)制,如圖
在這里插入圖片描述
上圖所示是一個雙核心的CPU系統(tǒng)架構(gòu),每個核心都有自己的控制器和運(yùn)算器,也都有自己的一級緩存,還有可能有所有CPU核心共享的二級緩存,每個核心都可以獨(dú)立運(yùn)行線程。

因此,CPU讀取數(shù)據(jù)的順序是:寄存器-高速緩存-主存。主存中的部分?jǐn)?shù)據(jù),會先拷貝一份放到cache中,當(dāng)CPU計算時,會直接從cache中讀取數(shù)據(jù),計算完畢后再將計算結(jié)果放置到cache中,最后在主存中刷新計算結(jié)果。所以每個CPU都會擁有一份拷貝。

以上只是CPU訪問內(nèi)存,進(jìn)行計算的基本方式。實(shí)際上,不同的硬件,訪問過程會存在不同程度的差異。比如,不同的計算機(jī),CPU和主存間可能會存在三級緩存、四級緩存、五級緩存等等的情況。

為了屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,實(shí)現(xiàn)讓 Java 程序在各種平臺下都能達(dá)到一致的內(nèi)存訪問效果,定義了Java的內(nèi)存模型(Java Memory Model,JMM)。

JMM 的主要目標(biāo)是定義程序中各個變量的訪問規(guī)則,即在虛擬機(jī)中將變量存儲到主存和從主存中取出變量這樣的底層細(xì)節(jié)。這里的變量指的是能夠被多個線程共享的變量,它包括了實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對象的元素,方法內(nèi)的局部變量和方法的參數(shù)為線程私有,不受JMM的影響。

Java的內(nèi)存模型如下:

在這里插入圖片描述
Java內(nèi)存模型中的本地內(nèi)存,對應(yīng)的就是CPU結(jié)構(gòu)圖中的cache1或者cache2。它實(shí)際上并不真實(shí)存在,其包含了緩存、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器的優(yōu)化。

JMM規(guī)定:將所有共享變量放到主內(nèi)存中,當(dāng)線程使用變量時,會把其中的變量復(fù)制到自己的本地內(nèi)存,線程讀寫時操作的是本地內(nèi)存中的變量副本。一個線程不能訪問其他線程的本地內(nèi)存。

這樣的情況下,如果有一個變量i在線程A、B的本地內(nèi)存中都有一份副本。此時,若線程A想修改i的值,在線程A將修改后的值放入到本地內(nèi)存,但又未刷新回主內(nèi)存時,如果線程B讀取變量i的值,則讀到的是未修改時的值,這就造成了讀寫共享變量出現(xiàn)不可預(yù)期的結(jié)果,產(chǎ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();  //啟動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)并不會停止:
在這里插入圖片描述
其原因就是main線程對共享變量run的修改,另外一個線程My_Thread并不能立刻看到:

在這里插入圖片描述
這就是由于內(nèi)存可見性帶來的多線程訪問共享變量出現(xiàn)不符合預(yù)期的結(jié)果,即由于可見性帶來的線程安全問題。

2.2.2、可見性問題解決辦法

解決辦法就是保證共享變量的可見性,具體實(shí)現(xiàn)就是任何對共享變量的訪問都要從共享內(nèi)存(主內(nèi)存)中獲取。在Java中的實(shí)現(xiàn)方法是:

①加鎖,synchronized和Lock都可以保證

線程在加鎖時,會清空本地內(nèi)存中共享變量的值,共享變量的使用需要從主內(nèi)存中重新獲取。而在釋放鎖資源時,則必須先把此共享變量同步回主內(nèi)存中。

由于鎖的存在,未持有鎖的線程并不能操作共享變量,而當(dāng)阻塞的線程獲得鎖時,主內(nèi)存中共享變量的值已經(jīng)刷新過了,因此線程修改共享變量對其他線程是可見的。這保證了共享變量的可見性,可以解決內(nèi)存可見性產(chǎn)生的線程安全問題。

②使用volatile修飾共享變量

當(dāng)一個變量被聲明為volitale時,線程在寫入變量時,不會把值緩存本地內(nèi)存,而是會立即把值刷新回主存,而當(dāng)要讀取該共享變量時,線程則會先清空本地內(nèi)存中的副本值,從主存中重新獲取。這些也都保證了內(nèi)存的可見性。

優(yōu)先使用volatile關(guān)鍵字來解決可見性問題,加鎖消耗的資源更多。

2.3、有序性

2.3.1、什么是有序性問題

有序性,實(shí)際上是指令的重排序問題。

我們知道,CPU的執(zhí)行速度是比內(nèi)存要快出很多個數(shù)量級的。CPU為了執(zhí)行效率,會把CPU指令進(jìn)行重新排序。即我們編寫的Java代碼并不一定按照順序一行一行的往下執(zhí)行,處理器會根據(jù)需要重新排序這些指令,稱為指令并行重排序。

同時,JIT編譯器也會在代碼編譯的時候?qū)Υa進(jìn)行重新整理,最大限度的去優(yōu)化代碼的執(zhí)行效率,稱為編譯器的重排序。

而又由于處理器與主存之間會使用緩存和讀/寫緩沖機(jī)制,因此從主存加載和存儲操作也有可能是經(jīng)過指令重排序的,稱為內(nèi)存系統(tǒng)重排序。

綜上所述,在執(zhí)行程序時,為了提高性能,編譯器和處理器常常會對指令進(jìn)行重排序,再加上主內(nèi)存和處理器的緩存,Java源碼經(jīng)過層層的重排序,最后才得出最終結(jié)果。

在這里插入圖片描述

由圖可知,從Java源碼到最后的執(zhí)行指令,會經(jīng)歷3種重排序的優(yōu)化。若有ava代碼如下:

int a = 2; //A
int b = 3; //B
int c = a*b; //C

經(jīng)過上述3種重排序后,語句A和語句B的執(zhí)行順序是可能互換的,并且這種互換并不影響代碼的正確性。但是我們發(fā)現(xiàn)語句C則不能和A、B互換,否則得出的結(jié)果將不正確,因?yàn)樗麄冎g存在著數(shù)據(jù)依賴關(guān)系,即語句C的數(shù)據(jù)依賴A和B得出。

由此,我們可以發(fā)現(xiàn),以上3種指令的重排序并不能隨意排序,他們需要遵守一定的規(guī)則,以保證程序的正確性。

①as-if-serial語義

as-if-serial語義是指:不管怎么樣重排序,單線程程序的執(zhí)行結(jié)果都不能被改變。即不會對存在數(shù)據(jù)依賴關(guān)系的操作進(jìn)行重排序。

編譯器、處理器進(jìn)行指令重排序優(yōu)化時都必須遵守as-if-serial語義。即在單線程的情況下,指令重排序只能對不影響處理結(jié)果的部分進(jìn)行重排序。

以上述語句A、B、C為例,存在數(shù)據(jù)依賴關(guān)系的語句C和A或B不能被重排序:

在這里插入圖片描述
as-if-serial語義把單線程程序保護(hù)起來了,遵守該語義的編譯器、處理器等使我們編寫單線程有一個錯覺:單線程程序是按照源代碼的順序來執(zhí)行的。實(shí)際上在由于as-if-serial語義的存在,我們編寫單線程時,完全可以認(rèn)為源代碼是按照順序執(zhí)行的,因?yàn)榧词勾a被進(jìn)行了重排序,其結(jié)果也不會改變,同時單線程中也無需擔(dān)心內(nèi)存可見性問題。

as-if-serial語義的核心思想是:不會對存在數(shù)據(jù)依賴關(guān)系的操作進(jìn)行重排序。

那么數(shù)據(jù)依賴類型有哪些呢?如下表所示:

類型 示例 說明
寫后讀 a = 1; b = a 寫一個變量后再讀該變量
寫后寫 a = 1; a = 2 寫一個變量后再寫該變量
讀后寫 a = b; b = 2 讀一個變量后再寫該變量

以上三種依賴關(guān)系,一旦重排序兩個操作的執(zhí)行順序,其結(jié)果就會改變,所以依照as-if-serial語義,Java在單線程的情況下不會對這三種依賴關(guān)系進(jìn)行重排序(多線程情況不符合此情況)。

as-if-serial語義是基于數(shù)據(jù)依賴關(guān)系的,但它無法保證多線程環(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)存可見性的問題(即線程t2修改了a和finish,但t1可能看不到的緩存問題)。在此前提下如果成功打印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語義,可以對這兩條語句進(jìn)行重排序,因此會出現(xiàn)finish = true;的指令比a = 2;先執(zhí)行的情況。

如果在先執(zhí)行finish = true;,而a = 2;沒有執(zhí)行時發(fā)生線程上下文切換,輪到線程t1執(zhí)行,那么t1線程中的if語句條件為真,而a的值依然為初始值0,則a*a的結(jié)果為0。

在這里插入圖片描述

可以看出,即使在假設(shè)沒有內(nèi)存可見性問題的前提下,上述代碼的結(jié)果也是不可預(yù)期的,因此上述代碼也是線程不安全的,其原因就是重排序破壞了多線程程序的語義。

②happens-before規(guī)則

既然是重排序出現(xiàn)問題,那么解決思路就是禁止重排序。但是也要注意不能全部禁用重排序,重排序的目的是為了提升執(zhí)行效率,如果全部禁用那么Java程序的性能將會很差。所以,應(yīng)該做到的是部分禁用,Java的內(nèi)存模型提供了一個可用于多線程環(huán)境,也適用于單線程環(huán)境的規(guī)則:happens-before規(guī)則。

happens-before規(guī)則的定義如下:A happens-before B,那么操作A的執(zhí)行結(jié)果對操作B是可見的,且操作A的執(zhí)行順序排在操作B之前。這里的操作A和操作B可以在同一個線程中,也可以在不同線程中。

注意:執(zhí)行順序只是happens-before向開發(fā)人員做的保證,實(shí)際上在處理器和編譯器上執(zhí)行時并不一定按照操作A排在操作B之前執(zhí)行。
如果重排序之后,依然可以保證與先A后B的執(zhí)行結(jié)果一樣,那么進(jìn)行重排序也是可以的。也就是說,符合happens-before的操作,只要不改變執(zhí)行結(jié)果,處理器和編譯器怎么優(yōu)化(重排序)都行。
只是我們開發(fā)人員可以直接認(rèn)為操作A的執(zhí)行順序排在操作B之前。

happens-before保證操作A的執(zhí)行結(jié)果對B可見,依靠這個原則,可以解決多線程環(huán)境下內(nèi)存可見性和有序性問題。

回到代碼:

/**線程t1**/
if(finish){ 
   a*a;
}

/**線程t2**/
a = 2;
finish = true;

一共有四個操作a = 2;、finish = true;、if(finish)、a*a;,想要上述代碼達(dá)到線程安全(即打印都正確輸出4),只需要:

在這里插入圖片描述
即在t2線程計算a*a;if(finish);之前,需要知道t1線程中a = 2;finish = true;(t2線程對t1線程的結(jié)果可見)。

要達(dá)到這一目的,就需要上圖中,①和②所示的happens-before關(guān)系。

那要如何達(dá)到呢?這就需要了解happens-before的六大具體規(guī)則了(兩個操作,只需要符合其中任何一條就可以認(rèn)為是happens-before關(guān)系):

  • ①程序順序規(guī)則:一個線程中的每個操作,按照程序順序,前面的操作 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ī)則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖。
synchronized (lock) { //加鎖
// x是共享變量,初始值=10
if (x < 12) {
x = 12; 
  }  
} //解鎖

若有兩個線程A、B,先后執(zhí)行這段代碼。則線程A執(zhí)行完畢后X = 12并釋放鎖。而線程B獲得鎖后,進(jìn)入代碼塊,在if中取X值判斷是否小于12。

此時 線程A中X=12的操作 happens-before 線程B中取X值判斷的操作(即線程B能看到線程A中執(zhí)行的X=12的結(jié)果)
  • ③volatile變量規(guī)則:對一個volatile域的寫,happens-before于任意后續(xù)對這個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()(啟動線程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、有序性問題解決辦法

解決有序性問題,實(shí)際上就是要運(yùn)用以上提到的兩種規(guī)則,as-if-serial語義解決了單線程程序的有序性問題,而happens-before關(guān)系則能解決多線程程序的有序性問題。

再回顧一下原始代碼,這是一段存在有序性問題線程不安全的代碼,我們要利用happens-before關(guā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;

這由線程中的代碼很容易就能得出。接下來運(yùn)用volatile變量規(guī)則,需要用volatile修飾一個變量,我們選變量finish。即初始化時代碼改為為volatile static boolean finish = false;

那么根據(jù)volatile變量規(guī)則,可知對finish的寫要happens-before于對finish的讀。

因此給finish加上volatile關(guān)鍵字后,就可以達(dá)到如下效果:

在這里插入圖片描述
volatile關(guān)鍵字不僅可以保證內(nèi)存可見性問題,同時依照happens-before的volatile變量規(guī)則,對于volatile修飾的變量,要保證對該變量寫的結(jié)果要對讀的操作可見,因此volatile禁止對有讀寫操作的volatile修飾的變量進(jìn)行重排序。

也就是說,volatile關(guān)鍵字不僅可以解決可見性問題,還可以解決有序性問題。

最后,通過傳遞性??芍?/p>

在這里插入圖片描述

可知,圖示的三和五,就是我們的目標(biāo)。到此,我們利用happens-before關(guān)系保證了代碼的可見性和有序性問題。

雖然分析的過程比較長,但是在原代碼中,我們實(shí)際上只改動了一行代碼。即將static boolean finish = false;改為volatile static boolean finish = false;而已,就可以使我們的代碼改變線程安全的。

這就是運(yùn)用volatile修飾變量來解決線程安全的辦法。volatile直接通過禁止相關(guān)的重排序來達(dá)到有序性的目的。

②、方法二:加鎖,synchronized

這個應(yīng)該比較容易理解,對相關(guān)代碼加鎖后,同一時刻就只有一個線程在執(zhí)行,也就相當(dāng)于對相關(guān)變量的操作,是保證有序的。

不過synchronized并不像volatile一樣禁止指令重排序,實(shí)際上synchronized塊內(nèi)部的代碼指令依然是可以進(jìn)行重排序優(yōu)化的。

3、小結(jié)

  1. 多個線程對同一個共享變量進(jìn)行讀寫操作時就可能產(chǎn)生不可預(yù)見的結(jié)果,就是線程安全問題。其重點(diǎn)是多線程對共享變量進(jìn)行讀和寫,如果只有讀,并不會有線程安全問題。
  2. 線程安全的原因有:①線程切換帶來的原子性問題②緩存帶來的可見性問題③指令重排序帶來的原子性問題。
  3. 線程安全的解決辦法:①對于原子性問題,使用鎖synchronized和Lock、或者使用原子類(AtomicInteger等)②對于可見性問題:使用鎖synchronized和Lock,或者使用volatile關(guān)鍵字③對于有序性問題:使用鎖synchronized和Lock,或者使用volatile關(guān)鍵字

由于能力有限,可能存在錯誤,感謝并懇請老鐵們指出。以上內(nèi)容為本人在學(xué)習(xí)過程中所做的筆記。參考的書籍、文章或博客如下:
[1]方騰飛,魏鵬,程曉明. Java并發(fā)編程的藝術(shù)[M].機(jī)械工業(yè)出版社.
[2]霍陸續(xù),薛賓田. Java并發(fā)編程之美[M].電子工業(yè)出版社.
[3]mg驛站. 多線程篇-線程安全-原子性、可見性、有序性解析.知乎.https://zhuanlan.zhihu.com/p/142929863
[4]JAVA bx.Java并發(fā)的原子性、可見性、有序性.知乎.https://zhuanlan.zhihu.com/p/205335197
[5]程序員七哥.happens-before是什么?JMM最最核心的概念,看完你就懂了.知乎.https://zhuanlan.zhihu.com/p/126275344

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多