Java多線程:線程間通信之volatile與sychronized


由前文Java內存模型我們熟悉了Java的內存工作模式和線程間的交互規范,本篇從應用層面講解Java線程間通信。

Java為線程間通信提供了三個相關的關鍵字volatile, synchronized和final。對於final,我們在博文Java中static關鍵字和final關鍵字中已經介紹。

1. volatile

1.1. 定義

由volatile定義的變量其特殊性在於:

一個線程對變量的寫一定對之后對這個變量的讀的線程可見。

換言之

一個線程對volatile變量的讀一定能看見它之前最后一個線程對這個變量的寫。

1.2. 機理

volatile意味着可見性,在講解volatile的機理前,我先給下面的這個例子:

package com.cielo.main;

/** * Created by 63289 on 2017/3/31. */
class MyThread extends Thread {
    private boolean isRunning = true;
    public boolean isRunning() {
        return isRunning;
    }
    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }
    @Override
    public void run() {
        System.out.println("進入到run方法中了");
        while (isRunning == true) {
        }
        System.out.println("線程執行完成了");
    }
}
public class RunThread{
    public static void main(String[] args) {
        try {
            MyThread thread = new MyThread();
            thread.start();
            Thread.sleep(1000);
            thread.setRunning(false);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在這個例子中,主線程啟動了子線程,子線程成功進入run方法,輸出”進入到run方法中”,只有由於isRunning==true,無限循環。此時,sleep一秒后的主線程想要改變isRunning的值,它將isRunning變量讀取到它的內存空間進行修改后,寫入主內存,但由於子線程一直在私有棧中讀取isRunning變量,沒有在主內存中讀取isRunning變量,因此不會退出循環。

如果我們把isRunning賦值行改為:

private volatile boolean isRunning = true;
將其用volatile修飾,則強制該變量從主內存中讀取。

這樣我們也就明白了volatile的實現機理,即:

  1. 當一個線程要使用volatile變量時,它會直接從主內存中讀取,而不使用自己工作內存中的副本。

  2. 當一個線程對一個volatile變量寫時,它會將變量的值刷新到共享內存(主內存)中。

1.3. 特性:不會被重排序

從Java內存模型一篇中,我們簡單了解了重排序,這里不會被重排序主要指語句重排序。

我們考慮到下面這個例子,有A,B兩個線程

線程A:加載配置文件,將配置元素初始化,之后標識初始化成功。

Map configOptions ;
char[] configText;

volatile boolean initialized = false;

//線程A首先從文件中讀取配置信息,調用process...處理配置信息,處理完成了將initialized 設置為true
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfig(configText, configOptions);//負責將配置信息configOptions 成功初始化
initialized = true;
線程B:等待初始化標識為true,之后開始工作。

while(!initialized)
{
    sleep();
}

//使用配置信息干活
doSomethingWithConfig();

很簡單的一個例子,在編譯器中,如果進行重排序,則會有將initialized=true這一行先執行的可能,如果這件事發生的話,線程B就會先運行,進而使用了沒有加載配置文件的Object。而如果initialized變量使用了volatile修飾,則編譯器不會將該變量的相關代碼進行重排序。(當然,這里的例子只是為了直觀,實際情況編譯器的重排序會更加復雜)

1.4. 非原子性

使用volatile時,我們要清楚,volatile是非原子性的。

原子性即是指,對於一個操作,其操作的內容只有全部執行/全不執行兩個狀態,不存在中間態。而volatile並不能鎖定某組操作,防止其他線程的干擾,即沒有規定原子性,因而volatile是非原子性的。或者說,volatile是非線程安全的。

綜上,如果我們想要使用一個原子性的修飾符來控制操作,即在操作變量時鎖定變量,我們就需要另一個修飾詞synchronized。

2. synchronized

2.1. 定義

synchronized作用的代碼范圍對於不同線程是互斥的,並且線程在釋放鎖的時候會將共享變量的值刷新到共享內存中。

2.2. synchronized與voliatile區別

  1. 使用:voliatile 用於修飾變量,synchronized可以修飾對象,類,方法,代碼塊,語句。

  2. 原子性:voliatile只保證變量的可見性,不能用於同步變量,即不保證原子性,多線程並發訪問voliatile修飾的變量時也不會產生阻塞。synchronized是原子性的,只有鎖定了變量的線程才能進入臨界區,從而保證臨界區的所有語句全部執行。多線程並發訪問sychronized修飾的變量會產生阻塞。

  3. 機理:

當線程對volatile變量讀時,會把工作內存中值置為無效。當線程對sychronized變量讀時,會在該線程鎖定變量時把工作內存中值置為無效。

當線程對voliatile變量寫時,會把值刷新到主內存中。當線程對sychronized變量寫時,會在變量解鎖時把值刷新到主內存中。

2.3. 注意

  1. 無論synchronized加在方法上還是對象上,其修飾的都是對象,而不是方法或者某個代碼塊代碼語句。

  2. 每個對象只有一個鎖與之相關聯。

  3. 實現同步需要很大的系統開銷來做控制,不要做無謂的鎖定。

2.4. synchronized的作用域

synchronized的作用域只有兩種。實際上,synchronized直接作用於內存中的一個內存塊,因此,可以通過鎖定內存塊來鎖定一個實例變量或者鎖定一個靜態區域。

  1. 某個對象實例內

synchronized aMethod(){}可以防止多個線程同時訪問這個對象的synchronized方法,如果對象有多個synchronized方法,則只要一個線程訪問了任何一個synchronized方法,其他線程不能同時訪問任何一個該對象的synchronized方法(synchronized作用於對象,且每個對象只有一個鎖)。

顯然,不同對象的synchronized方法則不會互相影響(synchronized作用於對象)。

  1. 某個類的范圍

又或者說作用於靜態方法/靜態代碼塊。synchronized static aMethod(){}防止多個線程同時訪問這個類中的synchronized static方法,它可以對類的所有實例對象起作用。

2.5. synchronized應用

2.5.1. synchronized方法

每個實例對應一個lock,線程獲得該含有synchronized方法的實例的鎖才可以執行,否則阻塞。方法一旦執行,則一直到方法返回才可以釋放鎖。此后被阻塞的線程才能獲得該鎖。對於一個實例,其聲明為synchronized的方法顯然只有一個能處於執行狀態。從而避免了類訪問變量的沖突。

synchronized同步的開銷很大,如果synchronized作用於一個比較大的方法上,顯然是不合算的。

2.5.2. synchronized代碼塊

synchronized代碼塊形式如下:

        synchronized (synchronizedObject){
            //Some thing
        }

代碼塊內部代碼必須在獲得synchronizedObject的鎖時才能執行。需要重點說的是synchronized(this),這也是比較常用的代碼塊。

synchronized的效果類似於在方法前修飾,只是修飾的范圍縮小成代碼塊。兩個線程同時訪問一個變量時,如果一個線程在執行synchronized的代碼,那么該實例被鎖定,另一個線程如果要訪問該實例被synchronized作用的范圍,則會被阻塞。

此外,如果不使用this作為鎖,而是只是想讓一段代碼同步,可以臨時創建如下鎖:

    private byte[] lock=new byte[0];

從操作碼上講,創建一個長度為0的數組對象是最經濟的,只需要3條操作碼。

2.5.3. synchronized靜態方法

synchronized修飾靜態方法時或者在普通方法中以類為對象如下形式:

class StaticSynchronized{
    public void aMethod{
        synchronized (StaticSynchronized.class){
            //Some thing
        }
    }
}

為synchronized靜態方法。

注意的是,對於同一個類,其static和實例方法如果都用synchronized修飾,其作用的必然不是同一個對象(顯然)。

2.5.4. synchronized對象

比較簡單粗暴的實現方式,直接把對象鎖定,思路也很清晰。Java負責跟蹤被加鎖的對象,該鎖定對象的線程每次給對象加鎖時對象的計數器+1,每次解鎖時計數器-1,如果對象的計數器為0,那么解除該線程的鎖定。

3. 參考文章

如何使用 volatile, synchronized, final 進行線程間通信

JAVA多線程之volatile 與 synchronized 的比較

Java synchronized詳解


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM