《Java線程與并發(fā)編程實(shí)踐》學(xué)習(xí)筆記3(初識(shí)線程同步)


(最近剛來到簡書平臺(tái),以前在CSDN上寫的一些東西,也在逐漸的移到這兒來,有些篇幅是很早的時(shí)候?qū)懴碌?,因此可能?huì)看到一些內(nèi)容雜亂的文章,對(duì)此深感抱歉,以下為正文)


引子

在平常的開發(fā)當(dāng)中,我們往往要使用到多線程編程技術(shù)。當(dāng)線程之間沒有交互的時(shí)候,這種情況下程序?qū)?huì)變得比較簡單。如果發(fā)生了交互,那么就必須考慮到多線程之間的安全問題,本篇來初步認(rèn)識(shí)Java中如何使用同步的特性來保證線程的安全。

正文

線程中存在的問題

Java對(duì)線程的支持自然增強(qiáng)了其應(yīng)用能力,但同時(shí)也增加了Java的復(fù)雜性,使得我們在開發(fā)的過程中必須要更加小心,否則多線程的程序中很有可能會(huì)出現(xiàn)一些難以察覺的bug。一般這些bug是由競態(tài)條件、數(shù)據(jù)競爭以及緩存變量造成的。

競態(tài)條件

在多線程程序中,如果程序的準(zhǔn)確性取決于相對(duì)時(shí)間或者調(diào)度器調(diào)度線程運(yùn)行的順序時(shí),這樣就產(chǎn)生了競態(tài)條件。在這里我們首先要明確的是競態(tài)條件的產(chǎn)生并不是意味著程序直接出錯(cuò)了,而是指存在了出錯(cuò)的可能,因?yàn)檎{(diào)度器調(diào)度線程的未知性,如果當(dāng)前調(diào)度順序是按照理想順序執(zhí)行的話,那么程序運(yùn)行就不會(huì)出現(xiàn)問題,反正則有可能出現(xiàn)問題,下面通過一些例子來描述竟態(tài)條件的發(fā)生。

package com.newway.syntest;

public class Test {

    private static int a = 10,b = 0;
    private static Thread t,t1;

    public static void main(String[] args) {
        t = new Thread(()->{
            if(a==10){
                try {
                    t1.join();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                b = a / 10;
            }
        });

        t1 = new Thread(()->{
            a += 10;
        });

        t.start();
        t1.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("a:"+a+" b:"+b);

    }
}

執(zhí)行上述代碼后,可以在控制臺(tái)看到如下打印:


控制臺(tái)打印

上面的例子中,筆者通過join方法強(qiáng)制模擬了一種競態(tài)條件產(chǎn)生的情況,我們創(chuàng)建了兩個(gè)線程t和t1,并且類中包含了兩個(gè)實(shí)例變量a和b,a初始值為0,b初始值為1,線程t中的操作是判斷值a是否等于10,如果等于,則將a值除以10然后賦值給b,t1線程則是將a的值加上10然后重新賦值給a。在單線程情況下,t線程執(zhí)行后b的值為1,但在多線程的情況下則不然。比如程序先執(zhí)行t線程,此時(shí)a的值為10,當(dāng)通過if判斷,準(zhǔn)備為b進(jìn)行賦值時(shí),此時(shí)調(diào)度器轉(zhuǎn)到執(zhí)行t1線程,將a的值改為了20,然后重新返回t線程繼續(xù)執(zhí)行,此時(shí)a的值已經(jīng)是20,為b賦值后,b的值為2與理論值1不符,這就是競態(tài)條件產(chǎn)生的錯(cuò)誤。

上面模擬的場景事實(shí)上是競態(tài)條件中的一個(gè)經(jīng)典例子check-then-act,在這種競態(tài)條件下,多線程有可能造成我們使用一種過時(shí)的觀測狀態(tài)來決定著我們之后的動(dòng)作,就如上述的例子一樣,線程t中是通過判斷a的值是否為10來決定之后的操作,當(dāng)通過判斷后,其它線程在t線程執(zhí)行操作前改變了a的值,然后t線程繼續(xù)執(zhí)行,此時(shí)a的值是20,理論上此時(shí)a的值應(yīng)該無法通過檢測,但因?yàn)橹霸赼的值未改變的時(shí)候已經(jīng)通過了檢測,盡管a的值已經(jīng)更新了,但線程t仍然使用著過去的狀態(tài)運(yùn)行,程序繼續(xù)執(zhí)行后續(xù)的操作,從而得到了b值為2這種錯(cuò)誤的狀況。

除了check-then-act這種類型,還有一種比較常見的類型就是read-modify-write 。從字面意思可以看出,該種情況的理論運(yùn)行順序是先讀取數(shù)據(jù)狀態(tài),然后修改數(shù)據(jù)狀態(tài),最后更新數(shù)據(jù)狀態(tài)。通過這三個(gè)不可分割操作從而能得到更新后的結(jié)果。但在多線程中,如果你不加小心,這三個(gè)操作很有可能被分割開來,從而造成程序出現(xiàn)錯(cuò)誤。同樣,下面通過一個(gè)經(jīng)典的小例子來描述這種情況。

package com.newway.syntest;

public class Test {

    private static int count = 0;

    public static void main(String[] args) {
        Runnable r = ()->{
            count ++;
        };

        for(int i = 0 ; i < 1000 ; i++){
            Thread t = new Thread(r);
            t.start();
        }
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }
}

在上述的例子中,我們有一個(gè)實(shí)例變量count,之后我們啟動(dòng)了1000個(gè)線程,線程中的操作只有count++,理論上這些操作如果是順序操作的話,那么最后count的結(jié)果必定是1000,然而從控制臺(tái)的輸出可以看出,事實(shí)并不是這樣。


控制臺(tái)輸出

其實(shí)造成這樣結(jié)果的原因就在于count++這個(gè)操作上,乍一看,該操作似乎只是分一步執(zhí)行,似乎是符合原子性操作的,多線程的運(yùn)行應(yīng)該不會(huì)影響到程序運(yùn)行的結(jié)果才對(duì),其實(shí)count++整體是分為三個(gè)操作步驟的,首先它讀取了count的值,然后對(duì)其進(jìn)行了修改,將count的值加1,最后將最新的count的值重新賦值給了count變量。

在多線程的運(yùn)行環(huán)境下,因?yàn)椴恢勒{(diào)度器在運(yùn)行狀態(tài)下的調(diào)度情況,這三個(gè)操作是有可能被分開的,假設(shè)線程1獲取了當(dāng)前count的值為1,此時(shí)線程2執(zhí)行,也獲取了當(dāng)前count的值為1,然后線程1繼續(xù)執(zhí)行為count的值加1并重新賦值給count,count值為2,然后線程2繼續(xù)執(zhí)行,因?yàn)橹耙呀?jīng)讀取過count的值為1,此時(shí)加1,count值為2,相當(dāng)于還原了線程1的操作,如果正常運(yùn)行的話,此時(shí)的值應(yīng)該為3,所以最終控制臺(tái)打印的count的值不是1000就是因?yàn)榘l(fā)生了這種情況,這也是競態(tài)條件的經(jīng)典例子。

數(shù)據(jù)競爭

數(shù)據(jù)競爭也是多線程編程中引起出錯(cuò)的原因之一,我們也很容易把數(shù)據(jù)競爭和競態(tài)條件弄混淆。數(shù)據(jù)競爭指的是兩條或兩條以上的線程并發(fā)地訪問同一塊內(nèi)存區(qū)域,并且至少有一條線程是為了修改內(nèi)存中的數(shù)據(jù),如果我們沒有人為協(xié)調(diào)好多線程對(duì)內(nèi)存區(qū)域的訪問,那么此時(shí)線程的訪問順序就是不確定的,這樣就會(huì)產(chǎn)生數(shù)據(jù)競爭。下面通過一個(gè)小例子來描述一下數(shù)據(jù)競爭的產(chǎn)生:

package com.newway.syntest;

import java.util.concurrent.atomic.AtomicInteger;

public class Test {

    private static Object lock;
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) {
        Runnable r = ()->{
            lock = getInstance();
        };
        for(int i = 0 ; i < 10000 ; i++){
            Thread t = new Thread(r);
            t.start();
        }
        System.out.println("創(chuàng)造Object對(duì)象執(zhí)行了"+count+"次");
    }

    public static Object getInstance() {
        if (lock == null) {
            lock = new Object();
            count.addAndGet(1);
        }
        return lock;
    }
}

執(zhí)行上述代碼可以從控制臺(tái)看到如下打?。?/p>

控制臺(tái)輸出

上述的例子其實(shí)是想實(shí)現(xiàn)一個(gè)簡單的單例模式,如果是在單線程環(huán)境下,那么Object對(duì)象只會(huì)被創(chuàng)建一次。在多線程環(huán)境下,因?yàn)槲覀儧]有對(duì)多線程進(jìn)行約束管理,所以其訪問順序是未知的,假使線程1開始執(zhí)行,因?yàn)榈谝淮螆?zhí)行,所以在執(zhí)行g(shù)etInstance的時(shí)候,判斷出當(dāng)前l(fā)ock為null,所以準(zhǔn)備為lock變量值。此時(shí)線程2執(zhí)行的時(shí)候,當(dāng)它開始執(zhí)行g(shù)etInstance方法的時(shí)候,可能線程1已經(jīng)為lock初始化過了,但也有可能此時(shí)線程1在更改lock狀態(tài)時(shí)還沒開始或還在更改中,那么此時(shí),線程2仍然會(huì)將lock變量認(rèn)為是null,為其初始化,這樣就會(huì)發(fā)生明明是單例模式,卻創(chuàng)建了兩個(gè)Object對(duì)象,這樣數(shù)據(jù)競爭就產(chǎn)生了。

緩存變量

為了提高程序的性能,程序中對(duì)數(shù)據(jù)的讀取、修改并不都是在主存中進(jìn)行的。Java虛擬機(jī)(JVM)和操作系統(tǒng)會(huì)進(jìn)行協(xié)調(diào),在寄存器中或者處理器緩存中進(jìn)行緩存變量,這樣相當(dāng)于每個(gè)線程都有一份屬于自己的操作數(shù)據(jù),這樣對(duì)其進(jìn)行讀取,修改就不再依賴于主存,提高了程序性能,線程操作結(jié)束后,再將修改過后的數(shù)據(jù)同步到主存中。這個(gè)設(shè)計(jì)本身確實(shí)提升了程序的性能,但在多線程的運(yùn)行環(huán)境中,會(huì)有這么個(gè)一問題,當(dāng)多個(gè)線程同時(shí)操作相同的數(shù)據(jù)時(shí),線程本身是修改的是自己拷貝的變量,再?zèng)]有寫入主存中時(shí),其它線程可能無法知道當(dāng)前數(shù)據(jù)的改變,這樣就會(huì)造成數(shù)據(jù)錯(cuò)誤。

用一個(gè)簡單的例子來描述的話可以這么說,假設(shè)我們有一個(gè)變量count,然后我們開啟了兩個(gè)線程對(duì)其進(jìn)行++操作,Java會(huì)為每個(gè)線‘’‘’程開辟出一個(gè)單獨(dú)的工作內(nèi)存,然后會(huì)從主存中將count的值分別拷貝到每個(gè)線程的工作內(nèi)存中,單個(gè)線程在對(duì)count值進(jìn)行操作時(shí),是針對(duì)自己的工作內(nèi)存,之后才會(huì)同步到主存中,兩個(gè)線程可能發(fā)生線程1操作后count值加1,線程2中的count值還是自己工作內(nèi)存中的值并未加1,繼續(xù)執(zhí)行自增操作后將count值同步到主存中,相當(dāng)于重復(fù)了一遍線程1的工作,最后count值相當(dāng)于只自增了一次而并非預(yù)計(jì)的兩次。

初識(shí)同步

從上面的介紹可以看出,多線程編程處處存在風(fēng)險(xiǎn),稍有不慎,就有可能引發(fā)一些意想不到的錯(cuò)誤,JVM中有一個(gè)特性同步可以用來解決上述的多線程的一些問題。
這里我們要先了解一個(gè)概念臨界區(qū),臨界區(qū)就是指必須以串行方式運(yùn)行的一段代碼塊。同步特性就是用于保證多線程運(yùn)行環(huán)境下,同一個(gè)臨界區(qū)在同一時(shí)間只能被一條線程執(zhí)行。
同步特性也保證了數(shù)據(jù)的可見性,每一條線程進(jìn)入臨界區(qū)執(zhí)行的時(shí)候,都會(huì)從主存中讀取所需操作的變量值,從而保證操作時(shí)的變量值已經(jīng)是最新的狀態(tài),這樣就可以避免臨時(shí)的緩存變量而引起的多線程問題。
java中的同步是通過監(jiān)聽器來實(shí)現(xiàn)的。java針對(duì)臨界區(qū)構(gòu)建了一種并發(fā)訪問控制的機(jī)制,將java中的對(duì)象與監(jiān)聽器相關(guān)聯(lián),然后通過獲取和釋放監(jiān)聽器上的鎖來實(shí)現(xiàn)并發(fā)控制。當(dāng)一個(gè)線程持有某監(jiān)聽器的鎖時(shí),當(dāng)其它的線程再準(zhǔn)備獲取該監(jiān)聽器的鎖時(shí),將會(huì)被阻塞。只有當(dāng)持有鎖的線程離開臨界區(qū)后,釋放掉鎖資源后,其它線程才可以繼續(xù)獲取該監(jiān)聽器的鎖。

synchronized關(guān)鍵字的簡單使用

java提供了synchronized關(guān)鍵字來保障臨界區(qū)中的代碼是串行執(zhí)行的。synchronized關(guān)鍵字可以作用在方法上或者代碼塊上。下面將分別通過一個(gè)小例子來分別講述synchronized作用在方法和代碼塊上。

使用同步方法:

package com.newway.syntest;

public class Test {

    private static int id = 0;

    public static void main(String[] args) throws InterruptedException {

        Runnable r = () -> {
            getNextID();
            //id++;
        };

        for (int i = 0; i < 100; i++) {
            Thread t = new Thread(r);
            t.start();
        }

        //此處將主線程睡眠500ms是為了等待上面的100個(gè)線程都執(zhí)行完畢。
        Thread.sleep(500);
        System.out.println(id);

    }

    public synchronized static int getNextID() {
        return id++;
    }

}

多次執(zhí)行上述代碼可以在控制臺(tái)看到如下打?。?/p>

控制臺(tái)輸出

可以看出通過使用synchronized關(guān)鍵字修飾的同步方法后,得到的id值和預(yù)估的一樣,不會(huì)出現(xiàn)之前之前的id值小于100的情況。
如果將Runnable對(duì)象r上下文中調(diào)用的同步方法getNextID注釋掉,并且將id自增操作的注釋打開,再次多次執(zhí)行,可以發(fā)現(xiàn)id的值無法保證每次都等于100,可能出現(xiàn)小于100的情況。
使用同步方法時(shí),鎖會(huì)與調(diào)用該方法的實(shí)例對(duì)象相關(guān)聯(lián),如果同步方法是類方法的話,那么鎖對(duì)象會(huì)與調(diào)用該方法的類的class對(duì)象相關(guān)聯(lián),在上面的例子中就是與Test.class對(duì)象相關(guān)聯(lián)。

使用同步代碼塊:

synchronized關(guān)鍵字除了能用來修飾方法,還可以用來修飾代碼塊,格式如下:

synchronized(object){
    /*statements*/
}

傳入的Object類型參數(shù)將作為監(jiān)聽器的鎖使用。下面將使用同步代碼塊的方式重復(fù)上個(gè)例子的實(shí)現(xiàn):

package com.newway.syntest;

public class Test {

    private static int id = 0;
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Runnable r = () -> {
            synchronized (lock) {
                id++;
            }
        };
        for (int i = 0; i < 1000; i++) {
            Thread t = new Thread(r);
            t.start();
        }

        //這里讓主線程睡眠了500ms是為了讓上面的1000個(gè)線程先執(zhí)行完畢
        Thread.sleep(500);
        System.out.println(id);
    }

}

多次執(zhí)行上述代碼都可以可以在控制臺(tái)看到如下打?。?/p>

控制臺(tái)輸出

通過上面的例子我們可以看出,通過synchronized關(guān)鍵字我們可以實(shí)現(xiàn)多線程環(huán)境下,對(duì)臨界區(qū)的同步管理。

活躍性問題

通過上面的篇幅我們了解了Java提供了synchronized關(guān)鍵字來幫助我們實(shí)現(xiàn)了線程的同步。但多線程編程并不僅僅是使用synchronized關(guān)鍵字就能解決所有問題了,暫且不談synchronized關(guān)鍵字對(duì)于系統(tǒng)資源的開銷比較大,單單就是其本身如果不正確使用的話也會(huì)產(chǎn)生很多的問題。
下面要引入一個(gè)概念:活躍性?;钴S性這個(gè)概念本身沒有明確的定義,可以理解為某件正確的事情最終會(huì)放生。而當(dāng)某些操作導(dǎo)致程序無法繼續(xù)執(zhí)行下去的時(shí)候,這時(shí)候就發(fā)生了活躍性問題。在多線程編程時(shí),我們需要時(shí)刻謹(jǐn)防活躍性問題。
在單線程的應(yīng)用程序中,無線循環(huán)就是這樣一個(gè)例子,程序無法繼續(xù)執(zhí)行下去。在多線程應(yīng)用程序中,同樣會(huì)發(fā)生活躍性問題,主要引起多線程活躍性問題的原因主要有:死鎖、活鎖、餓死。

死鎖問題

在并發(fā)編程中,死鎖問題是一種非常常見的邏輯錯(cuò)誤。死鎖的產(chǎn)生也是比較容易理解的,簡單點(diǎn)兒說就是線程1在等待線程2釋放持有的資源,線程2在等待線程1釋放持有的資源,兩者互相等待從而導(dǎo)致程序無法繼續(xù)執(zhí)行下去。
當(dāng)然,死鎖的問題并不是無法避免的,只要我們采用正確的方式,還是可以輕易地避免的。死鎖的產(chǎn)生需要4個(gè)必要的條件:

  • 互斥條件:指進(jìn)程對(duì)所分配到的資源進(jìn)行排它性使用,即在一段時(shí)間內(nèi)某資源只由一個(gè)進(jìn)程占用。如果此時(shí)還有其它進(jìn)程請求資源,則請求者只能等待,直至占有資源的進(jìn)程用畢釋放。
  • 請求和保持條件:指進(jìn)程已經(jīng)保持至少一個(gè)資源,但又提出了新的資源請求,而該資源已被其它進(jìn)程占有,此時(shí)請求進(jìn)程阻塞,但又對(duì)自己已獲得的其它資源保持不放。
  • 不剝奪條件:指進(jìn)程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時(shí)由自己釋放。
  • 環(huán)路等待條件:指在發(fā)生死鎖時(shí),必然存在一個(gè)進(jìn)程——資源的環(huán)形鏈,即進(jìn)程集合{P0,P1,P2,···,Pn}中的P0正在等待一個(gè)P1占用的資源;P1正在等待P2占用的資源,……,Pn正在等待已被P0占用的資源。
    我們只要使上述的4個(gè)必要條件中任意一個(gè)條件不成立,那么死鎖問題就自然被打破了。下面就結(jié)合一些實(shí)例代碼能更進(jìn)一步地了解死鎖問題。
package com.newway.syntest;

public class ThreadSynTest3 {

    private static Object lock1 = new Object();
    private static Object lock2 =  new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized(lock1){
                System.out.println("t1 get lock1 :" + Thread.holdsLock(lock1));
                System.out.println("t1 wait lock2 :" + Thread.holdsLock(lock2));
                synchronized (lock2) {
                    System.out.println("t1 get lock2 :" + Thread.holdsLock(lock2));
                }
            }
        });

        Thread t2 = new Thread(()->{
            synchronized (lock2) {
                System.out.println("t2 get lock2 :" + Thread.holdsLock(lock2));
                System.out.println("t2 wait lock1 :" + Thread.holdsLock(lock1));
                synchronized (lock1) {
                    System.out.println("t2 get lock1 :" + Thread.holdsLock(lock1));
                }
            }
        });

        t1.start();
        t2.start();
    }

}

執(zhí)行上述代碼,可以在控制臺(tái)看到如下打?。?/p>

控制臺(tái)打印

從控制臺(tái)可以看出,線程t1,t2都在等待對(duì)方釋放對(duì)應(yīng)的資源鎖,從而導(dǎo)致程序無法繼續(xù)執(zhí)行。這是一種最簡單的死鎖,我們可以輕易地看出,在實(shí)際場景中,一些死鎖問題就相對(duì)地比較隱蔽,不是那么容易的就能找出,我們可能需要通過分析線程轉(zhuǎn)儲(chǔ)來分析死鎖信息。
在平常的開發(fā)中,我們很少會(huì)在一個(gè)方法里顯示地調(diào)用兩個(gè)鎖,但我們可能會(huì)隱式地調(diào)用外部的同步方法,這樣本質(zhì)上雖然仍然是在一個(gè)方法里得到了兩個(gè)鎖,但死鎖的隱蔽程度大大增加了,讓人不是那么容易地發(fā)現(xiàn)。下面通過一個(gè)小例子來展示這種情況:

package com.newway.syntest;

public class ThreadSynTest04 {
    private static A a = new A();
    private static B b = new B();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            a.method1(b);
        });

        Thread t2 = new Thread(() -> {
            b.method4(a);
        });

        t1.start();
        t2.start();
    }

}

class A {
    public synchronized void method1(B b) {
        System.out.println("A invoke method1");
        b.method3();
    }

    public synchronized void method2() {
        System.out.println("A invoke method2");
    }
}

class B {
    public synchronized void method3() {
        System.out.println("B invoke method3");
    }

    public synchronized void method4(A a) {
        System.out.println("B invoke method4");
        a.method2();
    }
}

執(zhí)行上述代碼,可以在控制臺(tái)看到如下打?。?/p>

控制臺(tái)打印

從控制臺(tái)可以看出,程序產(chǎn)生了死鎖,阻塞住無法繼續(xù)向下執(zhí)行。這里就是因?yàn)樵谕椒椒ㄖ姓{(diào)用了其它的同步方法,從而隱式地獲得了鎖,從而造成了死鎖現(xiàn)象。在平常的開發(fā)當(dāng)中,我們經(jīng)常會(huì)調(diào)用別人的方法,這里值得注意一下。(筆者這里舉的例子沒有很好的實(shí)際意義,僅僅是為了實(shí)現(xiàn)效果)

從理論上來說,避免死鎖最簡單的方式,就是阻止同步方法或者同步快調(diào)用其它的同步方法和同步塊,但是通常的開發(fā)環(huán)境下,這明顯是不現(xiàn)實(shí)的,不說其它的,光是Java自身的API中就存在著不少的同步方法。所以當(dāng)發(fā)生死鎖問題時(shí),我們需要根據(jù)具體情況具體分析。

這里推薦一篇博客,《Java并發(fā)編程實(shí)踐》筆記5——線程活躍性問題及其解決方法。這篇博客詳細(xì)地說明了線程的活躍性問題,并列出了常見的幾種死鎖類型,并給出了相應(yīng)的解決方法,筆者看完后收獲頗多,特在此分享給大家。

活鎖

活鎖表示線程并沒有被阻塞住,而是在重復(fù)地執(zhí)行一個(gè)失敗的操作,以至于程序無法繼續(xù)執(zhí)行下去。

活鎖一般有兩類:單一實(shí)體的活鎖和協(xié)同導(dǎo)致的活鎖。

單一實(shí)體的活鎖的話好比一個(gè)線程從待執(zhí)行的隊(duì)列中取出開始執(zhí)行,但是執(zhí)行失敗,程序重新將它放入到待執(zhí)行的隊(duì)列中,如果該任務(wù)一直執(zhí)行失敗,那么這個(gè)循環(huán)操作將會(huì)一直延續(xù)下去,從而產(chǎn)生了活鎖。

協(xié)同導(dǎo)致導(dǎo)致的活鎖的話就好比在通信中,為了防止發(fā)生沖突,需要沖突檢測機(jī)制,假設(shè)兩個(gè)線程一直發(fā)送檢測信號(hào),每次都發(fā)現(xiàn)信路上有其它的信號(hào),然后重新發(fā)送檢測,這樣就又產(chǎn)生了一個(gè)循環(huán)的操作,從而產(chǎn)生了活鎖。

餓死

餓死是一個(gè)線程長時(shí)間得不到資源從而無法執(zhí)行的現(xiàn)象。可能是線程被調(diào)度器一直延遲訪問,也有可能是其優(yōu)先級(jí)太低總有高優(yōu)先級(jí)的線程優(yōu)于它執(zhí)行從而導(dǎo)致一直無法執(zhí)行。避免餓死的常用方式就是采用隊(duì)列的方式,從而保證每個(gè)線程都有機(jī)會(huì)獲得資源。

volatile和final變量

同步包括了兩種屬性:互斥性和可見性。前面synchronized關(guān)鍵字同時(shí)包含著兩種屬性。同時(shí)java還提供了一個(gè)更為輕量級(jí)的關(guān)鍵字,volatile,該關(guān)鍵字只包含了可見性。該關(guān)鍵字在前面將線程中斷的時(shí)候就已經(jīng)使用過,在筆者關(guān)于Java IO的篇幅中也提到過。
用volatile關(guān)鍵字修飾過的變量,在多線程環(huán)境中,每個(gè)線程將不會(huì)從主存中拷貝出一份副本放入工作內(nèi)存中,每一次使用該值時(shí),都會(huì)從主存中讀取,這樣就保證了數(shù)據(jù)的可見性。
下面用一個(gè)簡單的小例子來演示一下:

package com.newway.syntest;

public class Test {

    private static boolean stop;

    public static void main(String[] args) throws InterruptedException {
        Runnable r = () -> {
            for (;;) {
                if (stop) {
                    return;
                }
            }
        };
        for (int i = 0; i < 100; i++) {
            Thread t = new Thread(r);
            t.start();
        }
        Thread.sleep(50);
        stop = true;
        System.out.println("stop is been true");
    }

}

執(zhí)行上述代碼可以在控制臺(tái)看到如下打印:


控制臺(tái)輸出

當(dāng)boolean型控制開關(guān)stop已經(jīng)被置為true時(shí),我們可以看到仍然有線程在執(zhí)行者,這就是數(shù)據(jù)的可見性問題。如果我們?yōu)閟top變量加上volatile關(guān)鍵字進(jìn)行修飾,即:

private static volatile boolean stop;

再次運(yùn)行程序,可以在控制臺(tái)看到如下打?。?/p>

控制臺(tái)輸出

當(dāng)stop控制變量置為true的時(shí)候,所有的線程都結(jié)束了活動(dòng)。

除了上述的volatile關(guān)鍵字可以保證在多線程環(huán)境下的數(shù)據(jù)可見性,Java中的final關(guān)鍵字同樣能保證在多線程環(huán)境下數(shù)據(jù)是安全的。對(duì)于被final關(guān)鍵字修飾過的數(shù)據(jù),Java提供了一種特殊的線程安全的保證。即便沒有用同步來進(jìn)行約束,它們依然可以被多線程安全地進(jìn)行訪問。被final修飾的對(duì)象可以稱為不可變對(duì)象,它們提供了下列的一些規(guī)則:

  • 不可變對(duì)象絕對(duì)不允許狀態(tài)變更
  • 所有屬性必須聲明成final
  • 對(duì)象必須被恰當(dāng)?shù)貥?gòu)造出來已放引用顯示地脫離構(gòu)造函數(shù)

關(guān)于final關(guān)鍵字對(duì)于線程安全這塊兒的使用,可以用一個(gè)經(jīng)典的單例模式的小代碼來演示,代碼如下:

public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}   

上述代碼就是一個(gè)比較經(jīng)典的單例模式。通過final關(guān)鍵字的使用保證了它的線程的安全性。
最后還提供了一個(gè)專門講述線程安全的網(wǎng)站:Java theory and practice Safe construction techniques

以上為本篇的全部內(nèi)容。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容