多線程之volatile與synchronized(二)

JMM中主要是圍繞并發(fā)過程中如何處理原子性,可見性和有序性三個特性來建立的。最終可以保證線程安全性,volatile和synchronized兩個關(guān)鍵字又是我們最常碰到與最容易提到的關(guān)鍵字,這次放在一起來講。

與文無關(guān)

線程安全性:當(dāng)多個線程訪問某個類的時候,不管運行環(huán)境采用何種調(diào)度方式或這些線程如何交替執(zhí)行,并且在主調(diào)代碼中不需要額外的同步或協(xié)同,這個類都能表現(xiàn)出正確的行為,那么就稱這個類是線程安全的。

原子性、可見性與有序性

首先來看一下這幾個特性代表的具體含義。

  • 原子性(Atomicity):原子性是指,一個操作是不可中斷的。即使是多個線程一起執(zhí)行的時候,一個操作一旦開始,就不會被其他線程干擾。

    JDK的包中提供了專門的原子包java.util.concurrent.atomic,synchronized關(guān)鍵字還有Lock來讓程序在并發(fā)環(huán)境下具有原子性的特點。

  • 可見性(Visibility):可見性是指當(dāng)一個線程修改了共享變量的值,其它線程能立即得知這個修改。

    volatile,synchronized和final關(guān)鍵字能實現(xiàn)可見性。使用final關(guān)鍵字需要注意對象逃逸

  • 有序性:如果再本線程內(nèi)觀察,所有操作都是有序的,如果再一個線程中觀察另外一個線程,那么所有操作都是無序的。前半句是指“線程內(nèi)表現(xiàn)為串行”,后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象”

    volatile和synchronized關(guān)鍵字可以線程之間操作的有序性。

Volatile

一個變量定義為volatile之后,它將具有兩種特性:

  1. 保證次變了對所有線程的可見性,一條線程修改了這個值,新值對其它線程是可以立即得知的。
  2. 禁止指令重排優(yōu)化。

volatile變量在寫操作時候,會在寫操作后加上store屏障指令,將本地內(nèi)存刷新到主內(nèi)存。
volatile變量讀操作的時候,會在讀操作之前加入一條load屏障指令,從主內(nèi)存中讀取共享變量。

關(guān)于JMM的8大操作指令,可以查看我的上篇文章,java內(nèi)存模型。

volatile變量為什么在并發(fā)下不安全?

volatile變量在各個線程的工作內(nèi)存中也可以存在不一致的情況,但由于每次使用之前都要刷新,執(zhí)行引擎看不到不一致的情況,因此可以認(rèn)為不存在一致性問題,但是Java里面的運算并非原子操作。

假如說一個寫入值操作不需要依賴依賴這個值的原先值,那么在進(jìn)行寫入的時候我們就不需要進(jìn)行讀取操作。
寫入操作對原本的值的時候沒有要求,那么所有線程都可以寫入新的值,雖然讀取到的值是相同的,每個線程的操作也是正確的,但是最終結(jié)果卻是錯誤的。


JMM

感興趣的可以運行如下代碼:

public class VolatileTest {
    public static volatile int count = 0;
    public static final int THREAD_COUNT = 20;

    public static void add(){
        count++;
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            });
            threads[i].start();
        }
        
        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i].join();
        }        
        
        System.out.println(count);
    }
    
}
// 如果并發(fā)正確的話:應(yīng)該是20000,但是每次運行結(jié)果都不到20000
Volatile適合做什么?

適合做標(biāo)量,當(dāng)一個線程對某個變量進(jìn)行讀寫操作,而其它線程僅僅進(jìn)行讀操作的時候,是可以保證volatile的正確性的。如下:

volatile bool stopped;
public void stop(){
    stopped = true
}

while(!stoppped){
    // 執(zhí)行操作
}

Synchronized

Synchronized保證了原子性,可見性與有序性,它的工作時對同步的代碼塊加鎖,使得每次只有一個線程進(jìn)入代碼塊,從而保證線程安全。synchronized反應(yīng)到字節(jié)碼層面就是monitorenter與monitorexit.

注意*:雖然synchonized關(guān)鍵字看起來是萬能的,能保證線程安全性,但是越萬能的控制往往越伴隨著越大的性能影響。

Synchonzied用法
  1. 實例方法上,被修飾的方法稱為同步方法,其作用的范圍是整個方法,作用的對象是調(diào)用這個方法的對象;
  2. 靜態(tài)方法上,其作用的范圍是整個靜態(tài)方法,作用的對象是這個類的所有對象;
  3. 實例方法代碼塊.
  4. 靜態(tài)方法代碼塊。
 //實例方法
 public synchronized void add(int value){
      this.count += value;
 }
 //靜態(tài)方法
 public static synchronized void add(int value){
      count += value;
 }
 
 //實例方法代碼塊 
 public void add(int value){
   synchronized(this){
       this.count += value;   
    }
 }
 
 //靜態(tài)方法代碼塊
 public class MyClass {

    public static synchronized void log1(String msg1, String msg2){
       log.writeln(msg1);
       log.writeln(msg2);
    }

  
    public static void log2(String msg1, String msg2){
       synchronized(MyClass.class){
          log.writeln(msg1);
          log.writeln(msg2);  
       }
    }
  }
  
Synchonzied案例
public class SynchronziedTest implements Runnable{
    static int i = 0;
    static int j = 0;
    static SynchronziedTest instance=  new SynchronziedTest();

    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            increase();
        }
    }

    public synchronized void increase(){
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        // 注意新建的線程指向的同一個實例,
        // 如果指向不同的實例,那么兩個線程關(guān)注的鎖就不是同一把鎖,就會導(dǎo)致線程不安全
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        
        //錯誤的用法
//        Thread t3 = new Thread(new SynchronziedTest());
//        Thread t4 = new Thread(new SynchronziedTest());
        
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
//結(jié)果為:200000

注意創(chuàng)建線程的時候指向同一個實例,才會鎖住相同的對象。

最后

這次我們講了線程安全性的基本原則,然后解釋了volatile和synchronized關(guān)鍵字,多線程中不得不掌握的關(guān)鍵字。

參考

  • 《實戰(zhàn)Java高并發(fā)設(shè)計》
  • 《深入理解JVM虛擬機》
  • 《Java并發(fā)編程與高并發(fā)解決方案》
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容