目錄
一、可見性簡介
1、可見性:一個線程對共享變量值的修改,能夠及時地被其他線程看到。
2、共享變量:如果一個變量在多個線程的工作內存中都存有副本,那麼這個變量就是共享變量。
3、java內存模型(JMM)
所有的變量都存在主內存中,每個線程都有自己的工作內存,裏面保存着該線程使用到的變量副本(就是主內存中該變量的一份拷貝)。
Java內存模型有兩條規定:
1、線程對共享變量的所有操作都必須在自己的工作內存中進行,不能直接對主內存中進行讀寫
2、不同線程之間無法訪問其他線程工作內存中的變量,線程間變量的值傳遞都需要通過主內存來完成。
二、synchronized實現可見性原理
synchronized可以實現互斥鎖,可以保證在同一個時刻都只有一個線程在執行鎖裏面的代碼。
synchronized能夠實現:原子性(同步)、可見性
(一)JMM關於synchronized的兩條規定
1、線程解鎖前,必須把共享變量的最新值刷新到主內存中
2、線程加鎖時,將情況工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值。
(二)線程執行互斥代碼的過程:
-
獲得互斥鎖
-
清空工作內存
-
從主內存拷貝變量的最新副本到線程工作內存
-
執行代碼
-
將更改後的最新的共享變量的值刷新到主內存
-
釋放互斥鎖
(重排序:代碼書寫的順序與實際執行的順序不同,指令重排序是編譯器或處理器爲了提高程序性而做的優化)
as-if-serial:無論如何重排序,程序執行的結果都應該和代碼順序執行的結果一致。
試着用synchronized關鍵字寫一個讀寫的demo:
package com.jqs.sync;
public class SynchronizedDemo {
//共享變量
private int result = 0;
private boolean ready = false;
private int num = 1;
//寫操作
public synchronized void write() { //synchronized在此可以保證共享變量的內存可見性,從而線程安全
ready = true;
num = 2;
}
//讀操作
public synchronized void read() { //synchronized在此可以保證共享變量的內存可見性,從而線程安全
if (ready)
result = num * 3;
System.out.println("result中的結果爲:" + result);
}
private class ReadWriteThread extends Thread {
private boolean flag;
public ReadWriteThread(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag)
write();
else
read();
}
}
public static void main(String args[]) {
SynchronizedDemo demo = new SynchronizedDemo();
demo.new ReadWriteThread(true).start();
try {
Thread.sleep(1000);
/*
* synchronized可以保證線程之間不會交叉執行,但是仍有可能因爲讀線程先執行run方法導致結果爲0。(即是寫線程不是一定就會在讀線程執行之前先執行完)
* 所以在寫線程啓動之後加入主線程的休眠語句,這樣寫線程就可以基本在讀線程執行之前執行完!!!正確結果應該是6
* */
} catch (InterruptedException e) {
e.printStackTrace();
}
demo.new ReadWriteThread(false).start();
}
}
注:當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將會被阻塞
三、volatile保證可見性
volatile關鍵字:能夠保證volatile變量的可見性,不能保證volatile變量複合操作的原子性
如何實現內存可見性:通過加入內存屏障和禁止重排序優化來實現
簡單的說就是,volatile變量在每次被線程訪問時,都強迫從主內存中讀取該變量的值,而當該變量發生變化時,又會強迫線程將最新的值刷新到主內存,這樣任何時刻,不同的線程總能看到該變量的最新值。
注意:volatile不能保證volatile變量複合操作的原子性
(原子性: 原子性就是指該操作是不可再分的。不論是多核還是單核,具有原子性的量,同一時刻只能有一個線程來對它進行操作。簡而言之,在整個操作過程中不會被線程調度器中斷的操作,都可認爲是原子性。比如 a = 1;):
private volatile int number=1;
number++;
//不是原子操作
//分爲三步:1、讀number值 2、number值加1 3、寫入number的值
試着寫一個demo
package com.jqs.sync;
public class VolatileDemo {
private volatile int number = 0;
public int getNumber() {
return this.number;
}
public void increase() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.number++;
}
public static void main(String args[]) {
final VolatileDemo demo = new VolatileDemo();
for (int i = 0; i < 500; i++) {
new Thread(new Runnable() {
@Override
public void run() {
demo.increase();
}
}).start();
}
/*
* 如果還有子線程還在執行,主線程就讓出cpu資源
* 直到所有的子線程都運行完了,主線程再往下執行
* */
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("number:" + demo.getNumber());
}
//由於number++不是原子操作,對應的三步操作有可能會有線程交叉執行,最終導致結果可能不爲500
}
存在這種情況的原因,就是線程A讀取到了num值,但是執行時被B佔用了cup,num被B加一併且寫入了主內存,但是此時A線程中的num值還沒有加一,之後A再加一將值寫入,就不對了。
想要解決的話,就把number++替換爲
synchronized (this){
this.number++;
}
或者用到lock,具體看代碼
Lock lock=new ReentrantLock();
lock.lock();
try {
this.number++;
}finally {
lock.unlock();
}
四、volatile適用場景
1、對變量的寫入操作不依賴其當前值
不滿足的:number++、count=count*5
滿足的:boolean變量、溫度記錄變量等
2、該變量沒有包含在具有其他變量的不變式中
如 low<up
五、synchronized和volatile比較
1、volatile不需要加鎖,比synchronized更輕量級,不會阻塞線程
2、從內存可見性的角度講,volatile讀相當於加鎖,volatile寫相當於解鎖
3、synchronized既能保證可見性,又可保證原子性,而volatile只能保證可見性,不能保證原子性。總結來說,就是synchronized可以保證線程安全,但是volatile沒辦法保證線程安全
六、總結
如果既想要線程安全又想性能好的話,synchronized和volatile要結合具體情況使用。
synchronized並不是萬能的,在加鎖時能夠鎖局部就不要鎖住整體,能夠鎖變量就不要鎖方法。在保證線程安全的前提下,加鎖的範圍越小越好。
參考:《Java併發編程實踐》
如有錯誤,歡迎指出,以上。