一、volatile簡介
Java語言規(guī)范第三版中對volatile的定義如下:
java編程語言允許線程訪問共享變量,為了確保共享變量能被準確和一致的更新,線程應該確保通過排他鎖單獨獲得這個變量。Java語言提供了volatile,在某些情況下比鎖更加方便。如果一個字段被聲明成volatile,java線程內存模型確保所有線程看到這個變量的值是一致的。
術語定義
| 術語 | 英文單詞 | 描述 |
|---|---|---|
| 共享變量 | 在多個線程之間能夠被共享的變量被稱為共享變量。共享變量包括所有的實例變量,靜態(tài)變量和數組元素。他們都被存放在堆內存中,Volatile只作用于共享變量。 | |
| 內存屏障 | Memory Barriers | 是一組處理器指令,用于實現對內存操作的順序限制。 |
| 緩沖行 | Cache line | 緩存中可以分配的最小存儲單位。處理器填寫緩存線時會加載整個緩存線,需要使用多個主內存讀周期。 |
| 原子操作 | Atomic operations | 不可中斷的一個或一系列操作。 |
| 緩存行填充 | cache line fill | 當處理器識別到從內存中讀取操作數是可緩存的,處理器讀取整個緩存行到適當的緩存(L1,L2,L3的或所有) |
| 緩存命中 | cache hit | 如果進行高速緩存行填充操作的內存位置仍然是下次處理器訪問的地址時,處理器從緩存中讀取操作數,而不是從內存。 |
| 寫命中 | write hit | 當處理器將操作數寫回到一個內存緩存的區(qū)域時,它首先會檢查這個緩存的內存地址是否在緩存行中,如果存在一個有效的緩存行,則處理器將這個操作數寫回到緩存,而不是寫回到內存,這個操作被稱為寫命中。 |
| 寫缺失 | write misses the cache | 一個有效的緩存行被寫入到不存在的內存區(qū)域。 |
實現原理
有volatile變量修飾的共享變量進行寫操作的時候會將當前處理器緩存行的數據寫回到系統內存中,這個寫回內存的操作會引起其它CPU里緩存的該內存地址的數據無效。具體實現細節(jié)參考這篇文章聊聊并發(fā)(一)深入分析Volatile的實現原理。
二、適用場景
volatile具有sychronized的可見性和有序性,但是不具備原子性。volatile的可見性確保了所有線程看到volatile聲明的共享變量值是最新的。另外在訪問volatile聲明的變量時線程不會加鎖,也就不會引起線程的阻塞,這就使得相對于sychronized來說它只是輕量級的同步機制。在某些情況下,如果讀操作遠遠大于寫操作時,使用volatile能夠在性能上優(yōu)于鎖。舉個volatile不具備原子性的例子:
2.1 volatile非原子性代碼示例(不要這樣做)
package com.game.lll.syn;
/**
* Volatile不具備原子性
* @author liulongling
*
*/
class VolatileExample3{
private volatile int count;
public void inc()
{
count++;
}
public static void main(String[] args) {
final VolatileExample3 test = new VolatileExample3();
for(int i=0;i<100000;i++){
new Thread(){
public void run() {
test.inc();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println("count:"+test.count);
}
}
控制臺輸出:
count:99994
Volatile不具備原子性:在代碼第六行雖然使用了volatile來修飾count,但是從結果可以看出count++的次數等于99994,而我們預期的結果是100000.所以volatile不足以使自增count++原子化,除非你能保證只有一個線程對變量執(zhí)行寫操作。在上一篇文章中:原子性(一) 我對出現這種結果做過原因分析。
2.2 模式一:狀態(tài)標志
package com.game.lll.syn;
class VolatileExample2{
private volatile boolean isShuttingDown = false; // 標志服務是否正在關閉
public boolean isShuttingDown() {
return isShuttingDown;
}
public void userLogin()
{
if(isShuttingDown)
{
// 正在關服,拒絕登陸
return;
}
if(!isShuttingDown)
{
shutdown();
}
System.out.println(Thread.currentThread()+"玩家登錄");
}
/**
* 關閉服務(斷開所有與玩家的連接并且不接受新玩家連接,保存數據,關閉程序)
*/
private void shutdown() {
// 停服處理
// 將在線玩家踢下線,并在關服過程中不接受新登陸需求
isShuttingDown = true;
System.out.println(Thread.currentThread()+"關閉服務器");
}
public static void main(String[] args) {
final VolatileExample2 test = new VolatileExample2();
for(int i=0;i<100000;i++){
new Thread(){
public void run() {
test.userLogin();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
}
}
控制臺輸出:
Thread[Thread-0,5,main]關閉服務器
Thread[Thread-0,5,main]玩家登錄
在游戲停服處理時,我們會聲明一個volatile修飾的bool變量。在上面例子已經證明volatile不具有原子性,那么在運行過程中可能會出現當線程一在執(zhí)行userLogin()過程中,線程二調用了shutdown()將isShuttingDown布爾變量設置為true。這時如果使用volatile ,線程二會馬上通知線程一isShuttingDown的狀態(tài)發(fā)生改變。
2.3 模式二:開銷較低的“讀-寫鎖” 策略
public synchronized void inc()
{
count++;
}
控制臺輸出:
count:100000
在讀操作多余寫操作時,我們可以使用synchronized來保證inc的操作是原子的,并使用volatile保證當前結果的可見性。這樣的策略不用每次在讀取count變量時只允許一個線程訪問它,使得在性能上支持多線程的volatile性能更好一些。
另外volatile還適應以下三個場景,詳細參考文章:http://blog.csdn.net/vking_wang/article/details/9982709#t5
只有滿足下面所有的標準后,你才能使用volatile變量:
- 寫入變量時并不依賴變量的當前值;或者能夠確保只有單一的線程改變變量的值。
- 變量不需要與其他的狀態(tài)變量共同參與不變約束
- 訪問變量時,沒有其它的原因需要加鎖
作者:小毛驢,一個Java游戲服務器開發(fā)者 原文地址:https://liulongling.github.io/