Java多線程之線程同步

volatile

先看個例子

class Test {
		// 定義一個全局變量
    private boolean isRun = true;
 
	  // 從主線程調用發起
    public void process() {
        test();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        stop();
    }
		// 啟動一個子線程循環讀取isRun
    private void test() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (isRun) {
									// 疑問,如果我這裡有一些打印的語句或者線程睡眠的語句,子線程在
									// 主線程將isRun改為false的時候,就會跳出死循環,反之,如果循環體
									// 內是空的,就算在主線程改瞭isRun的值,也無法及時跳出循環,why?
									// 當然,如果將isRun變量使用volatile修飾就沒有此問題
                }
            }
        }).start();
    }
 
    private void stop() {
        isRun = false;
    }
}

有一點是一定的,就是子線程訪問isRun的時候會拷貝一份放到自己的線程(工作內存)裡,這樣在讀寫的時候可能就不會和外面isRun的值實時是匹配上的。所以就會出現意想不到的問題。

所以我們使用volatile修飾,這樣當有多線程同時訪問一個變量時,都會自動同步一下。顯然這樣會帶來一定的性能損失,但是如果確實需要還是要這麼做的。

但是,有一個問題來瞭,使用volatile一定能就可解決多線程同步的問題瞭嗎?那我們看下面這個例子:

class TestSynchronize {
 
		// 使用volatile修飾的變量
    private volatile int x = 0;
 
    private void add() {
        x++;
    }
 
    public void test() {
				// 啟動第一個線程,進行100萬次自加
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=0; i< 1_000_000; i++) {
                    add();
                }
                System.out.println("第一個線程x=" + x);
            }
        }).start();
				// 啟動第二個線程,進行100萬次自加
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=0; i< 1_000_000; i++) {
                    add();
                }
                System.out.println("第二個線程x=" + x);
            }
        }).start();
    }
}

我們希望的結果是,最後一個執行完的線程應該是在2_000_000,但是隻要你實際測下就發現並不是這樣,因為volatile隻能保證可見性,但是隻要涉及多線程我們一定還聽說過原子性這個概念。什麼是可見性:

可見性:對於多個線程都在訪問的變量,當有個線程在修改的時候,它會保證會將修改的值更新到內存中,而不是隻在工作線程中修改,這樣當別的線程訪問的時候也會去內存中取最新的值,這樣就能保證訪問到的值是最新的。

那什麼又是原子性呢:

原子性:就是一個操作或者多個操作要麼都執行,要麼都不執行,不會存在執行一半會被打斷。

在Java中,對基本數據類型變量的讀取和賦值操作是原子性的。但是上述代碼中的x++;顯然不是原子操作,可以拆解為:

int temp = x + 1;
x = temp;

那麼這就為多線程操作帶來不確定性,

1、開始x初始值為0,

2、當線程A調用add()函數時,執行到temp=x+1;這一行時被中斷瞭,

3、此時切換到線程B的add()函數,線程B完整執行完兩行代碼後,x = 1瞭,

4、這個時候線程B又完整的執行瞭一遍add方法,那麼x=2瞭,

5、此時發生瞭線程切換,切換到A執行,A接著上次的執行的語句,temp = 1瞭,接下來執行x = temp;語句將1賦值給瞭x。

可是本來x都被B線程加到2瞭,這下又回去瞭,經歷A和B線程一共三次add()操作,結果x的值隻是1。

這就解釋瞭上面那段代碼中,兩個線程分別加瞭100萬次後,結果最後一個執行完的線程打印的卻並不是200萬。原因就是add()裡面的操作並不是原子性的,而volatile隻能保證可見性,不能保證原子性

當然,僅針對上面的按理我們可以將int x = 0;換一種類型聲明,比如使用AtomicInteger x = new AtomicInteger(0);然後將x++改成x.incrementAndGet();這樣也能保證原子性,確保多線程操作後數據是符合期望的。

除瞭針對基本數據類型的,還有對引用操作原子化的,AtomicReference<V>

synchronized

當synchronized修飾一個方法時,那麼同一時間隻有一個線程可以訪問此方法,如果有多個方法都被synchronized修飾的話,當一個線程訪問瞭其中一個方法,別的線程就無法訪問其他被synchronized修飾的方法。

相當於有一個監視器,當一個線程訪問某個方法,其他線程想訪問別的方法時,需要和同一個監視器做確認,這麼做看起來不太合理,其實也是合理的,比如有兩方法都可能對同一個變量做操作,兩個線程能同時訪問兩個方法,這樣數據還是會發生錯亂。

當然,我們就有兩個方法支持同步訪問的場景的,隻要我們自己確認兩個方法不會存在數據上的錯亂,我們可以為每個方法指定自己的監視器,在默認情況下是當前類的對象(this)。

我們分別為setName();和其他兩個方法指定瞭不同的monitor(監視器),這樣當線程A訪問上面兩個方法的時候,線程B想訪問方法setName也是不受影響的:

接下來我們看我們經常寫的另一個例子,單例模式:

class TestInstance {
    private TestInstance(){}
    
    private static TestInstance sInstance;
    
    public static TestInstance newInstance() {
				**// ② 這裡判空的目的?**
        if (sInstance == null) {
						**// ① 為什麼鎖加在這裡?**
            synchronized (TestInstance.class) {
								**// ③ 這裡判空的目的?**
                if (sInstance == null) {
                    sInstance = new TestInstance();
                }
            }
        }
        return sInstance;
    }
}

我們來依次搞清楚上面的三個問題,

①鎖為什麼加在裡面而不是在方法上加鎖,因為加鎖後會帶來性能上的損失的,單例對象隻會創建一次,沒必要在實例已經有的時候獲取單例時還加鎖,對性能是浪費。

②第一個判空的目的就是在已經創建過實例之後的獲取操作,不用再經過synchronized判斷,這樣更快。

③最後一個判空就是防止多個線程都會調到創建實例的操作。

到此這篇關於Java多線程之線程同步的文章就介紹到這瞭,更多相關Java線程同步內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: