1. volatile
-
定義:Java編程語(yǔ)言允許線程訪問(wèn)共享變量涝桅,為了確保共享變量能被準(zhǔn)確和一致的更新,線程應(yīng)該確保通過(guò)排他鎖單獨(dú)獲得這個(gè)變量烙样。
-
實(shí)現(xiàn)原理:
Java代碼如下:
instance = new Singleton();//instance是volatile變量
轉(zhuǎn)換成匯編代碼:
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0,(%esp);
有volatile變量修飾的共享變量進(jìn)行寫(xiě)操作的時(shí)候會(huì)多第二行匯編代碼冯遂,lock前綴的指令在多核處理器下會(huì)引發(fā)了兩件事情。
- 將當(dāng)前處理器緩存行的數(shù)據(jù)會(huì)寫(xiě)回到系統(tǒng)內(nèi)存谒获。
- 這個(gè)寫(xiě)回內(nèi)存的操作會(huì)引起在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無(wú)效蛤肌。
處理器為了提高處理速度,不直接和內(nèi)存進(jìn)行通訊批狱,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進(jìn)行操作裸准,但操作完之后不知道何時(shí)會(huì)寫(xiě)到內(nèi)存,如果對(duì)聲明了Volatile變量進(jìn)行寫(xiě)操作赔硫,JVM就會(huì)向處理器發(fā)送一條Lock前綴的指令炒俱,將這個(gè)變量所在緩存行的數(shù)據(jù)寫(xiě)回到系統(tǒng)內(nèi)存。但是就算寫(xiě)回到內(nèi)存爪膊,如果其他處理器緩存的值還是舊的权悟,再執(zhí)行計(jì)算操作就會(huì)有問(wèn)題,所以在多處理器下推盛,為了保證各個(gè)處理器的緩存是一致的峦阁,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議,每個(gè)處理器通過(guò)嗅探在總線上傳播的數(shù)據(jù)來(lái)檢查自己緩存的值是不是過(guò)期了耘成,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改榔昔,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無(wú)效狀態(tài),當(dāng)處理器要對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候凿跳,會(huì)強(qiáng)制重新從系統(tǒng)內(nèi)存里把數(shù)據(jù)讀到處理器緩存里件豌。
-
volatile實(shí)現(xiàn)原則
- Lock前綴指令會(huì)引起處理器緩存回寫(xiě)到內(nèi)存。
- 一個(gè)處理器的緩存回寫(xiě)到內(nèi)存會(huì)導(dǎo)致其他處理器的緩存無(wú)效控嗜。
-
volatile使用條件
volatile變量具有 synchronized 的可見(jiàn)性特性茧彤,但是不具備原子性。這就是說(shuō)線程能夠自動(dòng)發(fā)現(xiàn) volatile 變量的最新值疆栏。只能在有限的一些情形下使用 volatile 曾掂。要使 volatile 變量提供理想的線程安全,必須同時(shí)滿足下面兩個(gè)條件:
- 對(duì)變量的寫(xiě)操作不依賴(lài)于當(dāng)前值壁顶。
- 該變量沒(méi)有包含在具有其他變量的不變式中珠洗。
反例:volatile變量不能用于約束條件中,下面是一個(gè)非線程安全的數(shù)值范圍類(lèi)若专。它包含了一個(gè)不變式 —— 下界總是小于或等于上界许蓖。
public class NumberRange {
private volatile int lower;
private volatile int upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
將 lower 和 upper 字段定義為 volatile 類(lèi)型不能夠充分實(shí)現(xiàn)類(lèi)的線程安全;而仍然需要使用同步——使 setLower()和 setUpper() 操作原子化。否則膊爪,如果湊巧兩個(gè)線程在同一時(shí)間使用不一致的值執(zhí)行 setLower 和 setUpper 的話自阱,則會(huì)使范圍處于不一致的狀態(tài)。例如米酬,如果初始狀態(tài)是(0, 5)沛豌,同一時(shí)間內(nèi),線程 A 調(diào)用setLower(4) 并且線程 B 調(diào)用setUpper(3)赃额,顯然這兩個(gè)操作交叉存入的值是不符合條件的加派,那么兩個(gè)線程都會(huì)通過(guò)用于保護(hù)不變式的檢查,使得最后的范圍值是(4, 3) —— 一個(gè)無(wú)效值
-
volatile的適用場(chǎng)景
狀態(tài)標(biāo)志:也許實(shí)現(xiàn) volatile 變量的規(guī)范使用僅僅是使用一個(gè)布爾狀態(tài)標(biāo)志跳芳,用于指示發(fā)生了一個(gè)重要的一次性事件芍锦,例如完成初始化或請(qǐng)求停機(jī)。
volatile boolean isShutdown;
...
public void shutdown() {
isShutdown = true;
}
public void doWork() {
while (!isShutdown) {
// do stuff
}
}
線程1執(zhí)行doWork()的過(guò)程中筛严,可能有另外的線程2調(diào)用了shutdown醉旦,所以boolean變量必須是volatile。
這種類(lèi)型的狀態(tài)標(biāo)記的一個(gè)公共特性是:通常只有一種狀態(tài)轉(zhuǎn)換桨啃;shutdownRequested 標(biāo)志從false 轉(zhuǎn)換為true,然后程序停止檬输。
開(kāi)銷(xiāo)較低的“讀-寫(xiě)鎖”策略:如果讀操作遠(yuǎn)遠(yuǎn)超過(guò)寫(xiě)操作照瘾,您可以結(jié)合使用內(nèi)部鎖和 volatile 變量來(lái)減少公共代碼路徑的開(kāi)銷(xiāo)。
如下顯示的線程安全的計(jì)數(shù)器丧慈,使用 synchronized 確保增量操作是原子的析命,并使用 volatile 保證當(dāng)前結(jié)果的可見(jiàn)性。如果更新不頻繁的話逃默,該方法可實(shí)現(xiàn)更好的性能鹃愤,因?yàn)樽x路徑的開(kāi)銷(xiāo)僅僅涉及 volatile 讀操作,這通常要優(yōu)于一個(gè)無(wú)競(jìng)爭(zhēng)的鎖獲取的開(kāi)銷(xiāo)完域。
public class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock held
@GuardedBy("this") private volatile int value;
//讀操作软吐,沒(méi)有synchronized,提高性能
public int getValue() {
return value;
}
//寫(xiě)操作吟税,必須synchronized凹耙。因?yàn)閤++不是原子操作
public synchronized int increment() {
return value++;
}
}
使用鎖進(jìn)行所有變化的操作,使用 volatile 進(jìn)行只讀操作肠仪。
其中肖抱,鎖一次只允許一個(gè)線程訪問(wèn)值,volatile 允許多個(gè)線程執(zhí)行讀操作异旧。
2. synchronized
-
介紹
在多線程并發(fā)編程中synchronized一直是元老級(jí)角色意述,很多人都會(huì)稱(chēng)呼它為重量級(jí)鎖,但是隨著Java SE1.6對(duì)Synchronized進(jìn)行了各種優(yōu)化之后,有些情況下它并不那么重了荤崇。Java SE1.6中為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗而引入的偏向鎖和輕量級(jí)鎖拌屏,以及鎖的存儲(chǔ)結(jié)構(gòu)和升級(jí)過(guò)程。
關(guān)鍵字synchronized可以修飾方法或者以同步塊的形式來(lái)使用天试,他主要確保多個(gè)線程在同一個(gè)時(shí)刻槐壳,只能有一個(gè)線程處于方法或者同步塊中,他保證了線程對(duì)變量訪問(wèn)的可見(jiàn)性和排他性喜每。
-
synchronized特點(diǎn)
把代碼塊聲明為 synchronized务唐,有兩個(gè)重要后果,通常是指該代碼具有原子性(atomicity)和 可見(jiàn)性(visibility)带兜。
- 原子性意味著個(gè)時(shí)刻枫笛,只有一個(gè)線程能夠執(zhí)行一段代碼,這段代碼通過(guò)一個(gè)monitor object保護(hù)刚照。從而防止多個(gè)線程在更新共享狀態(tài)時(shí)相互沖突刑巧。 所謂原子性操作是指不會(huì)被線程調(diào)度機(jī)子打斷的操作,這種操作一旦開(kāi)始无畔,就一直到幸運(yùn)星結(jié)束啊楚,中間不會(huì)有任何切換(切換線程)。
- 可見(jiàn)性則更為微妙浑彰,它必須確保釋放鎖之前對(duì)共享數(shù)據(jù)做出的更改對(duì)于隨后獲得該鎖的另一個(gè)線程是可見(jiàn)的恭理。 —— 如果沒(méi)有同步機(jī)制提供的這種可見(jiàn)性保證,線程看到的共享變量可能是修改前的值或不一致的值郭变,這將引發(fā)許多嚴(yán)重問(wèn)題颜价。
-
synchronized實(shí)現(xiàn)同步的基礎(chǔ)
Java中的每一個(gè)對(duì)象都可以作為鎖。具體表現(xiàn)為一下3中形式:
- 對(duì)于普通的同步方法诉濒,鎖是當(dāng)先實(shí)例對(duì)象周伦。
- 對(duì)于靜態(tài)同步方法,鎖是當(dāng)前類(lèi)的Class對(duì)象未荒。
- 對(duì)于同步方法塊专挪,鎖是Synchronized括號(hào)里配置的對(duì)象。
示例代碼:
public class SynchronizedTest2 {
// synchronized關(guān)鍵字修飾靜態(tài)的方法 同步方法
public synchronized static void printNum1(){
for(int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
// synchronized關(guān)鍵字使用類(lèi)鎖 同步代碼塊
public static void printNum2(){
synchronized(SynchronizedTest2.class) {
for(int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
// synchronized關(guān)鍵字修飾 同步方法 這里使用的是對(duì)象鎖
public synchronized void printNum3(){
for(int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
final SynchronizedTest2 test = new SynchronizedTest2();
Thread t1 = new Thread(new Runnable() {
public void run() {
SynchronizedTest2.printNum1();
}
},"A");
Thread t2 = new Thread(new Runnable() {
public void run() {
SynchronizedTest2.printNum2();
}
},"B");
Thread t3 = new Thread(new Runnable() {
public void run() {
test.printNum3();
}
},"C");
t1.start();
t2.start();
t3.start();
}
}
以上代碼中 靜態(tài)方法printNum1() 和 printNum2() 使用的都是類(lèi)鎖茄猫,方法printNum3()使用的是當(dāng)前對(duì)象的鎖狈蚤。
我們開(kāi)了三個(gè)線程A,B,C分別運(yùn)行printNum1(),printNum2()划纽,printNum3()方法 從結(jié)果中我們可以看到 線程A和線程B是始終同步的脆侮,線程C和線程A,B之間沒(méi)有同步關(guān)系
-
實(shí)現(xiàn)原理
示例代碼: Synchronized.java
public class Synchronized {
public static void main(String[] args) {
synchronized (Synchronized.class){//對(duì)Synchronized class對(duì)象進(jìn)行加鎖
}
m();//靜態(tài)同步方法,對(duì)Synchronized class對(duì)象進(jìn)行加鎖
}
public synchronized static void m() {
}
}
在Synchronized.class同級(jí)目錄執(zhí)行javap -v Synchronized.class勇劣,以下是相關(guān)部分的輸出:
public static void main(java.lang.String[]);
// 方法修飾符靖避,表示:public staticflags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc #1 // class com/murdock/books/multithread/book/Synchronized
2: dup
3: monitorenter // monitorenter:監(jiān)視器進(jìn)入潭枣,獲取鎖
4: monitorexit // monitorexit:監(jiān)視器退出,釋放鎖
5: invokestatic #16 // Method m:()V
8: return
public static synchronized void m();
// 方法修飾符幻捏,表示: public static synchronized
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
- 上面class信息中盆犁,對(duì)于同步塊的實(shí)現(xiàn)使用了monitorenter和monitorexit指令,而同步方法則是依靠方法修飾符上的ACC_SYNCHRONIZED來(lái)完成的篡九。無(wú)論采用哪種方式谐岁,其本質(zhì)是對(duì)一個(gè)對(duì)象的監(jiān)視器(monitor)進(jìn)行獲取,而這個(gè)獲取過(guò)程是排他的榛臼,也就是同一時(shí)刻只能有一個(gè)線程獲取到由synchronized所保護(hù)對(duì)象的監(jiān)視器伊佃。
- 任意一個(gè)對(duì)象都擁有自己的監(jiān)視器,當(dāng)這個(gè)對(duì)象由同步塊或者這個(gè)對(duì)象的同步方法調(diào)用時(shí)沛善,執(zhí)行方法的線程必須先獲取到該對(duì)象的監(jiān)視器才能進(jìn)入同步塊或者同步方法航揉,而沒(méi)有獲取到監(jiān)視器(執(zhí)行該方法)的線程將會(huì)被阻塞在同步塊和同步方法的入口處,進(jìn)入BLOCKED狀態(tài)金刁。
下圖描述了對(duì)象帅涂、對(duì)象的監(jiān)視器、同步隊(duì)列和執(zhí)行線程之間的關(guān)系尤蛮。
從圖中可以看到媳友,任意線程對(duì)Object(Object由synchronized保護(hù))的訪問(wèn),首先要獲得Object的監(jiān)視器产捞。如果獲取失敗庆锦,線程進(jìn)入同步隊(duì)列,線程狀態(tài)變?yōu)锽LOCKED轧葛。當(dāng)訪問(wèn)Object的前驅(qū)(獲得了鎖的線程)釋放了鎖,則該釋放操作喚醒阻塞在同步隊(duì)列中的線程艇搀,使其重新嘗試對(duì)監(jiān)視器的獲取尿扯。
-
線程等待/同步機(jī)制
等待/通知機(jī)制,是指一個(gè)線程A調(diào)用了對(duì)象O的wait()方法進(jìn)入等待狀態(tài)焰雕,而另一個(gè)線程B調(diào)用了對(duì)象O的notify()或者notifyAll()方法衷笋,線程A收到通知后從對(duì)象O的wait()方法返回,進(jìn)而執(zhí)行后續(xù)操作矩屁。上述兩個(gè)線程通過(guò)對(duì)象O來(lái)完成交互辟宗,而對(duì)象上的wait()和notify/notifyAll()的關(guān)系就如同開(kāi)關(guān)信號(hào)一樣,用來(lái)完成等待方和通知方之間的交互工作吝秕。
下面我們直接使用對(duì)象鎖的相關(guān)條件實(shí)現(xiàn)一個(gè)生產(chǎn)者和消費(fèi)者案例:
public class SynchronizedTest3 {
private Object object = new Object();
private List<Integer> list = new ArrayList<Integer>();
private boolean flag = true;
// 這里我們使用object對(duì)象的鎖泊脐,以及該鎖的條件對(duì)象
// 生產(chǎn)者線程一次生產(chǎn)一個(gè)數(shù)據(jù)5
public void produce() throws InterruptedException {
synchronized(object) {
while(flag){
if (list.size() > 0){
object.wait();
} else {
list.add(5);
System.out.println("生產(chǎn)者生產(chǎn)數(shù)據(jù)");
object.notifyAll();
}
}
}
}
// 消費(fèi)者線程每次消費(fèi)一個(gè)數(shù)據(jù)
public void consume() throws InterruptedException {
synchronized(object){
while(flag){
if (list.size() <= 0) {
object.wait();
} else {
System.out.println(list.remove(0));
System.out.println("消費(fèi)者消費(fèi)數(shù)據(jù)");
object.notifyAll();
}
}
}
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
final SynchronizedTest3 test = new SynchronizedTest3();
Thread t1 = new Thread(new Runnable() {
public void run() {
try {
test.produce();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
try {
test.consume();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
t1.start();
t2.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
test.setFlag(false);
}
}
- 生產(chǎn)者線程檢查容器list中是否有數(shù)據(jù),如果有數(shù)據(jù)則調(diào)用object.wait()使得生產(chǎn)者線程進(jìn)入該條件的等待集中烁峭,如果容器中沒(méi)有數(shù)據(jù)容客,則生產(chǎn)者線程生產(chǎn)數(shù)據(jù)放入list中秕铛,讓后調(diào)用object.notifyAll()方法從該條件等待集中所有線程的阻塞狀態(tài)。
- 消費(fèi)者線程檢查容器中是否有數(shù)據(jù)缩挑,如果有數(shù)據(jù)則消費(fèi)數(shù)據(jù)然后調(diào)用notifyAll()方法但两,是的處于該條件等待集中的生產(chǎn)者線程解除阻塞狀態(tài)。如果沒(méi)有數(shù)據(jù)調(diào)用object.wait()方法是的消費(fèi)者線程進(jìn)入該條件的等待集中供置。
下圖描述了上述示例的全過(guò)程谨湘。