1. 程式人生 > >Java並發編程:什麽是線程安全,以及並發必須知道的幾個概念

Java並發編程:什麽是線程安全,以及並發必須知道的幾個概念

文章 線程 指令 imp 例子 參考 影響 網上 並發編程

廢話

眾所周知,在Java的知識體系中,並發編程是非常重要的一環,也是面試的必問題,一個好的Java程序員是必須對並發編程這塊有所了解的。為了追求成為一個好的Java程序員,我決定從今天開始死磕Java的並發編程,盡量彌補自己在這方面的知識缺陷。

並發必須知道的概念

在深入學習並發編程之前,我們需要了解幾個基本的概念。

同步和異步

同步和異步用請求返回調用的方式來理解相對簡單。

同步:可以理解為發出一個請求後,必須等待返回結果才能執行下面的操作。

異步:請求發出後,不需要等待返回結果,可以繼續執行後續操作,異步請求更像是在另一個 “空間” 中處理請求的結果,這個過程不會影響請求方的其他操作。

舉個生活中的例子,比如我們去實體店買衣服,挑選完款式後下單讓售貨員去倉庫拿貨,在售貨員拿貨的過程你需要在店裏等待,直到售貨員把衣服交給你後才算購物成功,這就相當於同步的過程。

不過,如果是在網上購物的話,我們只需下單並完成支付,對我們來說整個購物過程就算完成了。網上的商家接到訂單會幫我們加緊安排送貨,這段時間我們可以去做其他的事,比如去外面打個籃球之類的。等送貨上門並簽收商品就完事了,這個過程就相當於異步。

並發和並行

並發和並行的功能很相似,兩者都可以表示多個任務一起執行的情況,但本質上兩者其實是有區別的。

嚴格意義上來說,並行的多任務是真實的同時執行,而並發更多的情況是任務之間交替執行,系統不停的在多個任務間切換執行,也就是 “串行” 執行。

最直接的例子的就是我們的計算機系統,在單核CPU時代,系統表面上能同時進行多任務處理,比如聽歌的同時又瀏覽網頁,但真實環境中這些任務不可能是真實並行的,因為一個CPU一次只能執行一條指令,這種情況就是並發,系統看似能處理多任務是因為不停的切換任務,但因為時間非常短,所以在我們的感官來說就是同時進行的。而計算機系統真實的並行是隨著多核CPU的出現才有的。

臨界區

臨界區表示公共資源或是共享數據,可以被多個線程使用。但是每次只能有一個線程使用它,一旦臨界區的資源被占用,其他線程就必須等到資源釋放後才能繼續使用該資源。在Java程序開發中,對於這樣的資源一般都需要做同步的操作,例如下面的這段代碼,用的就是synchronized關鍵字來對臨界區資源進行同步

public class SyncTest implements Runnable {
    
    //臨界區資源
    public static SyncTest instance = new SyncTest();

    @Override
    public void run() {
        synchronized (instance) {
            
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new SyncTest());
        Thread t2 = new Thread(new SyncTest());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

阻塞和非阻塞

阻塞和非阻塞通常用來形容多線程間的相互影響。比如一個線程占用了臨界區的資源,那麽其他需要這個資源的線程就必須等待。等待的過程會使線程掛起,也就是阻塞。如果臨界區的資源一直不釋放的話,那麽其他阻塞的線程就都不能工作了。

非阻塞則相反,強調的是線程之間並不互相妨礙,所有的線程都會不斷嘗試向前執行。

死鎖、饑餓和活鎖

這三種情況表示的是多線程間的活躍狀態,對於線程來說,以上的情況都是 “非友好” 的狀態。

1、死鎖一般是指兩個或者兩個以上的線程互相持有對方所需的資源,並且永遠在等待對方釋放的一種阻塞狀態。例如有兩個線程A和B同時共享臨界區的資源C,當A占用C時,B處於阻塞狀態,然而A的釋放需要用到B的資源,這樣一來,就變成了A一直在等待B,B也一直在等待A,互相之間永遠在等待對方釋放的狀態。

一般來說,死鎖的發生是由於程序的設計不合理導致,而且死鎖很難解決,最好的方式就是預防

2、饑餓是指某一個或者多個線程因為種種原因無法獲得所需的資源,導致一直無法執行。比如它的線程優先級太低,而高優先級的線程不斷搶占它所需的資源,導致低優先級資源無法工作。

3、活鎖的情況是線程一種非常有趣的情況,在生活中我們可能會碰到這樣的情況,那就是出門的時候可能會遇到有人要進門,你打算讓他先進門,他又打算讓你先出門,結果,兩個人都互相退後了,然後你打算先出門時對方也向前一步,來來回回就一直卡在門口。當然,這種事情正常人很快就能解決,但如果是線程碰到就沒那麽幸運了。

如果兩個線程占用著公共的資源,並且秉承著 “謙讓” 的原則,主動把資源讓給他人使用,你讓我也讓,這樣就造成資源在兩個線程間不斷跳動但線程之間都拿不到資源的情況,這樣的情況就是活鎖了。

線程安全

線程安全指的是多線程的安全。如果一段程序可以保證被多線程訪問後仍能保持正確性,那麽程序就是線程安全的。一般來說,線程安全註重的是多線程開發中的共享數據的安全。就比如下面這段代碼:

public class ThreadSafety implements Runnable{
    //共享數據
    public static int i = 0;

    public  void increase(){
        for (int j= 0;j<10; j++){
            i++;
        }
    }

    @Override
    public void run() {
        increase();
    }
    
    public static void main(String[] args) throws Exception{
        ThreadSafety demo = new ThreadSafety();
        Thread t1 = new Thread();
        Thread t2 = new Thread();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
    
}

兩個線程 t1 和 t2 同時開啟,執行run方法,在我們的預想中,如果是線程安全的話,那麽main的執行結果應該是20,但是因為 i 是共享數據,而程序沒有對 i 的操作做同步的處理,最終運行的結果並不是20,所以這種情況就不是線程安全的情況。

解決的辦法也比較簡單,可以利用synchronized關鍵字來修飾方法或代碼塊,這部分的知識也是並發編程中非常重要的一塊,當然,本文就不探究了,之後單獨寫篇文章出來細說。

參考:《實戰Java:高並發程序設計》

Java並發編程:什麽是線程安全,以及並發必須知道的幾個概念