由前文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的實現機理,即:
-
當一個線程要使用volatile變量時,它會直接從主內存中讀取,而不使用自己工作內存中的副本。
-
當一個線程對一個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區別
-
使用:voliatile 用於修飾變量,synchronized可以修飾對象,類,方法,代碼塊,語句。
-
原子性:voliatile只保證變量的可見性,不能用於同步變量,即不保證原子性,多線程並發訪問voliatile修飾的變量時也不會產生阻塞。synchronized是原子性的,只有鎖定了變量的線程才能進入臨界區,從而保證臨界區的所有語句全部執行。多線程並發訪問sychronized修飾的變量會產生阻塞。
-
機理:
當線程對volatile變量讀時,會把工作內存中值置為無效。當線程對sychronized變量讀時,會在該線程鎖定變量時把工作內存中值置為無效。
當線程對voliatile變量寫時,會把值刷新到主內存中。當線程對sychronized變量寫時,會在變量解鎖時把值刷新到主內存中。
2.3. 注意
-
無論synchronized加在方法上還是對象上,其修飾的都是對象,而不是方法或者某個代碼塊代碼語句。
-
每個對象只有一個鎖與之相關聯。
-
實現同步需要很大的系統開銷來做控制,不要做無謂的鎖定。
2.4. synchronized的作用域
synchronized的作用域只有兩種。實際上,synchronized直接作用於內存中的一個內存塊,因此,可以通過鎖定內存塊來鎖定一個實例變量或者鎖定一個靜態區域。
- 某個對象實例內
synchronized aMethod(){}可以防止多個線程同時訪問這個對象的synchronized方法,如果對象有多個synchronized方法,則只要一個線程訪問了任何一個synchronized方法,其他線程不能同時訪問任何一個該對象的synchronized方法(synchronized作用於對象,且每個對象只有一個鎖)。
顯然,不同對象的synchronized方法則不會互相影響(synchronized作用於對象)。
- 某個類的范圍
又或者說作用於靜態方法/靜態代碼塊。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 進行線程間通信