volatile
volatile是一種輕量同步機制。請看例子
MyThread25類
public class MyThread25 extends Thread{
private boolean isRunning = true;
public boolean isRunning()
{
return isRunning;
}
public void setRunning(boolean isRunning)
{
this.isRunning = isRunning;
}
public void run()
{
System.out.println("進入run了");
while (isRunning == true){}
System.out.println("執行緒被停止了");
}
public static void main(String[] args) throws InterruptedException {
MyThread25 mt = new MyThread25();
mt.start();
Thread.sleep(1000);
mt.setRunning(false);
System.out.println("已設定為false");
}
}
輸出結果如下
進入run了
已設定為false
為什麼程式始終不結束?說明mt.setRunning(false);沒有起作用。
這裡我們說下Java記憶體模型(JMM)
java虛擬機器有自己的記憶體模型(Java Memory Model,JMM),JMM可以遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓java程式在各種平臺下都能達到一致的記憶體訪問效果。
JMM定義了執行緒和主記憶體之間的抽象關係:共享變數儲存在主記憶體(Main Memory)中,每個執行緒都有一個私有的本地記憶體(Local Memory),本地記憶體儲存了被該執行緒使用到的主記憶體的副本,執行緒對變數的所有操作都必須在本地記憶體中進行,而不能直接讀寫主記憶體中的變數。這三者之間的互動關係如下
出現上述執行結果的原因是,主記憶體isRunning = true, mt.setRunning(false)設定主記憶體isRunning = false,本地記憶體中isRunning仍然是true,執行緒用的是本地記憶體,所以進入了死迴圈。
在isRunning前加上volatile
private volatile boolean isRunning = true;
輸出結果如下
進入run了
已設定為false
執行緒被停止了
volatile不能保證原子類執行緒安全
先看例子
MyThread26_0類,用volatile修飾num
public class MyThread26_0 extends Thread {
public static volatile int num = 0;
//使用CountDownLatch來等待計算執行緒執行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
@Override
public void run() {
for(int j=0;j<1000;j++){
num++;//自加操作
}
countDownLatch.countDown();
}
public static void main(String[] args) throws InterruptedException {
MyThread26_0[] mt = new MyThread26_0[30];
//開啟30個執行緒進行累加操作
for(int i=0;i<mt.length;i++){
mt[i] = new MyThread26_0();
}
for(int i=0;i<mt.length;i++){
mt[i].start();
}
//等待計算執行緒執行完
countDownLatch.await();
System.out.println(num);
}
}
輸出結果如下
25886
理論上,應該輸出30000。原子操作表示一段操作是不可分割的,因為num++不是原子操作,這樣會出現執行緒對過期的num進行自增,此時其他執行緒已經對num進行了自增。
num++分三步:讀取、加一、賦值。
結論:
volatile只會對單個的的變數讀寫具有原子性,像num++這種複合操作volatile是無法保證其原子性的
解決方法:
用原子類AtomicInteger的incrementAndGet方法自增
public class MyThread26_1 extends Thread {
//使用原子操作類
public static AtomicInteger num = new AtomicInteger(0);
//使用CountDownLatch來等待計算執行緒執行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
@Override
public void run() {
for(int j=0;j<1000;j++){
num.incrementAndGet();//原子性的num++,通過迴圈CAS方式
}
countDownLatch.countDown();
}
public static void main(String []args) throws InterruptedException {
MyThread26_1[] mt = new MyThread26_1[30];
//開啟30個執行緒進行累加操作
for(int i=0;i<mt.length;i++){
mt[i] = new MyThread26_1();
}
for(int i=0;i<mt.length;i++){
mt[i].start();
}
//等待計算執行緒執行完
countDownLatch.await();
System.out.println(num);
}
}
輸出結果如下
30000
原子類方法組合使用執行緒不安全
例子如下
ThreadDomain27類
public class ThreadDomain27 {
public static AtomicInteger aiRef = new AtomicInteger();
public void addNum()
{
System.out.println(Thread.currentThread().getName() + "加了100之後的結果:" + aiRef.addAndGet(100));
aiRef.getAndAdd(1);
}
}
MyThread27類
public class MyThread27 extends Thread{
private ThreadDomain27 td;
public MyThread27(ThreadDomain27 td)
{
this.td = td;
}
public void run()
{
td.addNum();
}
public static void main(String[] args)
{
try
{
ThreadDomain27 td = new ThreadDomain27();
MyThread27[] mt = new MyThread27[5];
for (int i = 0; i < mt.length; i++)
{
mt[i] = new MyThread27(td);
}
for (int i = 0; i < mt.length; i++)
{
mt[i].start();
}
Thread.sleep(1000);
System.out.println(ThreadDomain27.aiRef.get());
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
輸出結果如下
Thread-2加了100之後的結果:100
Thread-3加了100之後的結果:200
Thread-0加了100之後的結果:302
Thread-1加了100之後的結果:403
Thread-4加了100之後的結果:504
505
理想的輸出結果是100,201,302...,因為addAndGet方法和getAndAdd方法構成的addNum不是原子操作。
解決該問題只需要在addNum加上synchronized關鍵字。
輸出結果如下
Thread-1加了100之後的結果:100
Thread-0加了100之後的結果:201
Thread-2加了100之後的結果:302
Thread-3加了100之後的結果:403
Thread-4加了100之後的結果:504
505
結論:
volatile解決的是變數在多個執行緒之間的可見性,但是無法保證原子性。
synchronized不僅保障了原子性外,也保障了可見性。
volatile和synchronized比較
先看例項,使用volatile是什麼效果
CountDownLatch保證10個執行緒都能執行完成,當然你也可以在System.out.println(test.inc);之前使用Thread.sleep(xxx)
public class MyThread28 {
//使用CountDownLatch來等待計算執行緒執行完
static CountDownLatch countDownLatch = new CountDownLatch(10);
public volatile int inc = 0;
public void increase() {
inc++;
}
public static synchronized void main(String[] args) throws InterruptedException {
final MyThread28 test = new MyThread28();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
countDownLatch.countDown();
}
}.start();
}
countDownLatch.await();
System.out.println(test.inc);
}
}
執行結果如下
9677
每次執行結果都不一致。剛才我已經解釋過,這裡我再解釋一遍。
使用volatile修飾int型變數i,多個執行緒同時進行i++操作。比如有兩個執行緒A和B對volatile修飾的i進行i++操作,i的初始值是0,A執行緒執行i++時從本地記憶體剛讀取了i的值0(i++不是原子操作),就切換到B執行緒了,B執行緒從本地記憶體中讀取i的值也為0,然後就切換到A執行緒繼續執行i++操作,完成後i就為1了,接著切換到B執行緒,因為之前已經讀取過了,所以繼續執行i++操作,最後的結果i就為1了。同理可以解釋為什麼每次執行結果都是小於10000的數字。
解決方法:
使用synchronized關鍵字
public class MyThread28 {
//使用CountDownLatch來等待計算執行緒執行完
static CountDownLatch countDownLatch = new CountDownLatch(10);
public int inc = 0;
public synchronized void increase() {
inc++;
}
public static synchronized void main(String[] args) throws InterruptedException {
final MyThread28 test = new MyThread28();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
countDownLatch.countDown();
}
}.start();
}
countDownLatch.await();
System.out.println(test.inc);
}
}
輸出結果如下
10000
synchronized不管是否是原子操作,它能保證同一時刻只有一個執行緒獲取鎖執行同步程式碼,會阻塞其他執行緒。
結論:
volatile只能用在變數,synchronized可以在變數、方法上使用。
volatile不會造成執行緒阻塞,synchronized會造成執行緒阻塞。
volatile效率比synchronized高。