由前文Java內(nèi)存模型我們熟悉了Java的內(nèi)存工作模式和線程間的交互規(guī)范滤否,本篇從應(yīng)用層面講解Java線程間通信导饲。
Java為線程間通信提供了三個相關(guān)的關(guān)鍵字volatile, synchronized和final。對于final,我們在博文Java中static關(guān)鍵字和final關(guān)鍵字中已經(jīng)介紹。
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("線程執(zhí)行完成了");
}
}
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,無限循環(huán)宣赔。此時预麸,sleep一秒后的主線程想要改變isRunning的值,它將isRunning變量讀取到它的內(nèi)存空間進行修改后儒将,寫入主內(nèi)存吏祸,但由于子線程一直在私有棧中讀取isRunning變量,沒有在主內(nèi)存中讀取isRunning變量钩蚊,因此不會退出循環(huán)贡翘。
如果我們把isRunning賦值行改為:
private volatile boolean isRunning = true;
將其用volatile修飾,則強制該變量從主內(nèi)存中讀取砰逻。
這樣我們也就明白了volatile的實現(xiàn)機理鸣驱,即:
當(dāng)一個線程要使用volatile變量時,它會直接從主內(nèi)存中讀取蝠咆,而不使用自己工作內(nèi)存中的副本踊东。
當(dāng)一個線程對一個volatile變量寫時,它會將變量的值刷新到共享內(nèi)存(主內(nèi)存)中刚操。
1.3. 特性:不會被重排序
從Java內(nèi)存模型一篇中闸翅,我們簡單了解了重排序,這里不會被重排序主要指語句重排序菊霜。
我們考慮到下面這個例子缎脾,有A,B兩個線程
線程A:加載配置文件,將配置元素初始化占卧,之后標(biāo)識初始化成功。
Map configOptions ;
char[] configText;
volatile boolean initialized = false;
//線程A首先從文件中讀取配置信息,調(diào)用process...處理配置信息,處理完成了將initialized 設(shè)置為true
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfig(configText, configOptions);//負責(zé)將配置信息configOptions 成功初始化
initialized = true;
線程B:等待初始化標(biāo)識為true联喘,之后開始工作华蜒。
while(!initialized)
{
sleep();
}
//使用配置信息干活
doSomethingWithConfig();
很簡單的一個例子,在編譯器中豁遭,如果進行重排序叭喜,則會有將initialized=true這一行先執(zhí)行的可能,如果這件事發(fā)生的話蓖谢,線程B就會先運行捂蕴,進而使用了沒有加載配置文件的Object。而如果initialized變量使用了volatile修飾闪幽,則編譯器不會將該變量的相關(guān)代碼進行重排序啥辨。(當(dāng)然,這里的例子只是為了直觀盯腌,實際情況編譯器的重排序會更加復(fù)雜)
1.4. 非原子性
使用volatile時溉知,我們要清楚,volatile是非原子性的。
原子性即是指级乍,對于一個操作舌劳,其操作的內(nèi)容只有全部執(zhí)行/全不執(zhí)行兩個狀態(tài),不存在中間態(tài)玫荣。而volatile并不能鎖定某組操作甚淡,防止其他線程的干擾,即沒有規(guī)定原子性捅厂,因而volatile是非原子性的贯卦。或者說恒傻,volatile是非線程安全的脸侥。
綜上,如果我們想要使用一個原子性的修飾符來控制操作盈厘,即在操作變量時鎖定變量睁枕,我們就需要另一個修飾詞synchronized。
2. synchronized
2.1. 定義
synchronized作用的代碼范圍對于不同線程是互斥的沸手,并且線程在釋放鎖的時候會將共享變量的值刷新到共享內(nèi)存中外遇。
2.2. synchronized與voliatile區(qū)別
使用:voliatile 用于修飾變量,synchronized可以修飾對象契吉,類跳仿,方法,代碼塊捐晶,語句菲语。
原子性:voliatile只保證變量的可見性,不能用于同步變量惑灵,即不保證原子性山上,多線程并發(fā)訪問voliatile修飾的變量時也不會產(chǎn)生阻塞。synchronized是原子性的英支,只有鎖定了變量的線程才能進入臨界區(qū)佩憾,從而保證臨界區(qū)的所有語句全部執(zhí)行。多線程并發(fā)訪問sychronized修飾的變量會產(chǎn)生阻塞干花。
機理:
當(dāng)線程對volatile變量讀時妄帘,會把工作內(nèi)存中值置為無效。當(dāng)線程對sychronized變量讀時池凄,會在該線程鎖定變量時把工作內(nèi)存中值置為無效抡驼。
當(dāng)線程對voliatile變量寫時,會把值刷新到主內(nèi)存中肿仑。當(dāng)線程對sychronized變量寫時婶恼,會在變量解鎖時把值刷新到主內(nèi)存中桑阶。
2.3. 注意
無論synchronized加在方法上還是對象上,其修飾的都是對象勾邦,而不是方法或者某個代碼塊代碼語句蚣录。
每個對象只有一個鎖與之相關(guān)聯(lián)。
實現(xiàn)同步需要很大的系統(tǒng)開銷來做控制眷篇,不要做無謂的鎖定萎河。
2.4. synchronized的作用域
synchronized的作用域只有兩種。實際上蕉饼,synchronized直接作用于內(nèi)存中的一個內(nèi)存塊虐杯,因此,可以通過鎖定內(nèi)存塊來鎖定一個實例變量或者鎖定一個靜態(tài)區(qū)域昧港。
- 某個對象實例內(nèi)
synchronized aMethod(){}可以防止多個線程同時訪問這個對象的synchronized方法擎椰,如果對象有多個synchronized方法,則只要一個線程訪問了任何一個synchronized方法创肥,其他線程不能同時訪問任何一個該對象的synchronized方法(synchronized作用于對象达舒,且每個對象只有一個鎖)。
顯然叹侄,不同對象的synchronized方法則不會互相影響(synchronized作用于對象)巩搏。
- 某個類的范圍
又或者說作用于靜態(tài)方法/靜態(tài)代碼塊。synchronized static aMethod(){}防止多個線程同時訪問這個類中的synchronized static方法趾代,它可以對類的所有實例對象起作用贯底。
2.5. synchronized應(yīng)用
2.5.1. synchronized方法
每個實例對應(yīng)一個lock,線程獲得該含有synchronized方法的實例的鎖才可以執(zhí)行,否則阻塞撒强。方法一旦執(zhí)行禽捆,則一直到方法返回才可以釋放鎖。此后被阻塞的線程才能獲得該鎖飘哨。對于一個實例胚想,其聲明為synchronized的方法顯然只有一個能處于執(zhí)行狀態(tài)。從而避免了類訪問變量的沖突杖玲。
synchronized同步的開銷很大,如果synchronized作用于一個比較大的方法上淘正,顯然是不合算的摆马。
2.5.2. synchronized代碼塊
synchronized代碼塊形式如下:
synchronized (synchronizedObject){
//Some thing
}
代碼塊內(nèi)部代碼必須在獲得synchronizedObject的鎖時才能執(zhí)行。需要重點說的是synchronized(this)鸿吆,這也是比較常用的代碼塊囤采。
synchronized的效果類似于在方法前修飾,只是修飾的范圍縮小成代碼塊惩淳。兩個線程同時訪問一個變量時蕉毯,如果一個線程在執(zhí)行synchronized的代碼乓搬,那么該實例被鎖定,另一個線程如果要訪問該實例被synchronized作用的范圍代虾,則會被阻塞进肯。
此外,如果不使用this作為鎖棉磨,而是只是想讓一段代碼同步江掩,可以臨時創(chuàng)建如下鎖:
private byte[] lock=new byte[0];
從操作碼上講,創(chuàng)建一個長度為0的數(shù)組對象是最經(jīng)濟的乘瓤,只需要3條操作碼环形。
2.5.3. synchronized靜態(tài)方法
synchronized修飾靜態(tài)方法時或者在普通方法中以類為對象如下形式:
class StaticSynchronized{
public void aMethod{
synchronized (StaticSynchronized.class){
//Some thing
}
}
}
為synchronized靜態(tài)方法。
注意的是衙傀,對于同一個類抬吟,其static和實例方法如果都用synchronized修飾,其作用的必然不是同一個對象(顯然)统抬。
2.5.4. synchronized對象
比較簡單粗暴的實現(xiàn)方式无蜂,直接把對象鎖定,思路也很清晰座云。Java負責(zé)跟蹤被加鎖的對象氧腰,該鎖定對象的線程每次給對象加鎖時對象的計數(shù)器+1,每次解鎖時計數(shù)器-1妆偏,如果對象的計數(shù)器為0刃鳄,那么解除該線程的鎖定。
3. 參考文章
如何使用 volatile, synchronized, final 進行線程間通信