java併發編程的藝術(2)淺談volatile和synchronized

再多線程編程裏面,難免避免不了volatile和synchronized這兩個關鍵字。關於volatile這個關鍵字,最著名的就是“可見性”問題了,所謂的可見性問題是指:當有多個線程訪問同一個共享變量,並且對這個變量進行修改之後,另外的一個線程裏面可以讀取到這個最新修改的值。

關於volatile的定義和原理

Java語言規範第3版中對volatile的定義如下:Java編程語言允許線程訪問共享變量,爲了確保共享變量能被準確和一致地更新,線程應該確保通過排他鎖單獨獲得這個變量。Java語言提供了volatile,在某些情況下比鎖要更加方便。如果一個字段被聲明成volatile,Java線程內存模型確保所有線程看到這個變量的值是一致的。

那麼,在使用了volatile關鍵字定義之後,到底代碼的底層發生了什麼變化呢? 按照《java併發編程的藝術》這本書裏面的內容介紹是說,相應的彙編代碼被添加了一個叫做lock的前綴指令,但是光是書本這麼說還是太淺顯了。lz在網上找了各種文章,最後找到了一篇介紹如何通過class文件轉譯成爲彙編代碼的內容。
以下附上相應鏈接:
https://www.cnblogs.com/xrq730/p/7048693.html

通過自己對於代碼的彙編轉碼之後,可以看到命令窗口裏面出現以下相應的關鍵字內容:
這裏寫圖片描述

LZ發現,在使用了volatile關鍵字進行修飾的變量在轉換成彙編代碼的時候會多出來一個lock的指令內容。

說了這麼多,還是用一個案例來講解volatile關鍵字的用處吧
關於volatile的代碼案例如下所示:

package 併發編程02.volatile關鍵字案例;

//變量的可見性
public class VolatileVisible {
    boolean ready=true;
    private final static int SIZE=100;

    public static void main(String[] args) throws InterruptedException {
        VolatileVisible[] vv=new VolatileVisible[SIZE];
        for (int i=0;i<SIZE;i++) {
            (vv[i]=new VolatileVisible()).test();
        }
        System.out.println("---------");
    }

    public void test() throws InterruptedException {
        Thread t2=new Thread(){
            public void run(){
                System.out.println("t2:"+ready);
                while (ready){};
                System.out.println("this is end!");
            }
        };
        Thread t1=new Thread(){
            public void run(){
                ready=false;
            }
        };
        t2.start();
        Thread.yield();
        t1.start();
        t1.join();
        t2.join();
    }
}

這個案例中,可能會出現死循環的情況,這是因爲不同線程他們本地緩存裏面的ready並沒有被設置爲false,所以會一直進入循環狀態。那麼又該如何調整呢?加入一個volatile關鍵字即可改變。
在ready變量前邊加入一個volatile關鍵字即可。使用了volatile關鍵字修飾的變量,會對引用到相應變量的線程裏面的該變量信息進行及時同步。編譯器在運行時會注意到該變量是一個共享變量,因此會及時將其同步到各個線程的本地緩存當中。
在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。
這裏寫圖片描述

一旦某一個變量A被申明瞭volatile關鍵字之後,就會具有以下兩種特性:
1.保證改變量的可見性:當某個線程對A進行了相應的修改之後,其他線程裏面的A也會及時更新,每次使用該變量之前都會在從主內存中提取到自己的cpu緩存中。

2.禁止指令重排序優化。有volatile修飾的變量,賦值後多執行了一個“load addl $0x0, (%esp)”操作,這個操作相當於一個內存屏障(指令重排序時不能把後面的指令重排序到內存屏障之前的位置),只有一個CPU訪問內存時,並不需要內存屏障;(什麼是指令重排序:是指CPU採用了允許將多條指令不按程序規定的順序分開發送給各相應電路單元處理)。

關於synchronized

在多線程併發編程中synchronized一直是元老級角色,很多人都會稱呼它爲重量級鎖。但是,隨着Java SE 1.6對synchronized進行了各種優化之後,有些情況下它就並不那麼重了。本文詳細介紹Java SE 1.6中爲了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖,以及鎖的存儲結構和升級過程。

在synchronized裏面,包含有三種常見的鎖狀態:

對於普通的同步方法:
鎖是當前的對象
對於靜態函數的同步方法:
鎖是指引用當前類的class對象
對於同步方法塊的內容:
鎖是指Synchonized括號裏配置的對象

那麼接下來還是用一個案例來進行講解吧:

在多線程裏面總會提及到一個線程安全的問題,那麼讓我們來理解一下什麼是線程安全問題吧!首先,讓我們來了解一下什麼是線程安全吧:
所謂的線程安全就是指:多個線程同時對於一個全局變量進行寫的操作!!!
那麼如何防止這類危害發生呢?我們需要加一個叫做線程鎖的東西:
具體讓我們來看下代碼案例:(模擬搶火車票的場景)

package com.sise.lab02;

class ThreadTrainl implements Runnable{
    private int trainCount=100;
    public Object obj=new Object();

    //局部變量不會受到線程安全問題的影響
    @Override
    public void run() {
        while (trainCount>0){
            try{
                Thread.sleep(50);
            }catch (Exception e){
            }
            sale();
        }
    }


    private  void sale() {
        //同步代碼塊 ---走到這個位置的時候,可能會有多個線程進行訪問,誰先拿到鎖誰就先進行購票
            synchronized (obj) {
                if (trainCount > 0) {
                    System.out.println(Thread.currentThread().getName() + ":開始售出第" + (100 - trainCount + 1) + "" + "張票");
                }
                trainCount--;
            }
        }
    }

//模擬火車站搶票
public class ThreadMain {
    public static void main(String[] args) {
        //爲了模擬有兩個窗口在搶票
        ThreadTrainl threadTrainl=new ThreadTrainl();
        Thread t1= new Thread(threadTrainl,"窗口1");
        Thread t2= new Thread(threadTrainl,"窗口2");
        t1.start();
        t2.start();
    }
}

原本如果不加鎖的話,容易出現的問題就是,兩個線程同時訪問到sale方法,對票數造成
數據的影響,導致線程的不安全問題

同步的前提是:
1.必須要有兩個或者以上的線程纔可以:
2.必須是多個線程使用同一個鎖
3.必須要爭同步中只能由一個線程在運行

線程鎖synchronized的原理就是:
第一個訪問到這把鎖的線程先使用,然後其他線程處於等待狀態,當鎖釋放了以後,其他線程就可以上前去訪問了,這個時候,就會出現了相應的資源搶佔情況畢竟搶鎖,是一件相當消耗資源的事情
優點:
可以防止線程安全問題
缺點:
佔用資源

說到了鎖,順便可以提一下,還有一種鎖 Reetrantlock是jdk5裏面自帶的,需要手動釋放鎖纔可以。

上邊那我們提到的是同步鎖,另外一種方式是同步函數在方法前邊加一個synchronized即可:
private synchronized void sale()
那麼這個時候我們需要來進行深入研究了,同步函數究竟是用的什麼鎖呢?
答案是 this鎖。
那麼該如何進行證明呢?請看下邊這個案例:

package com.sise.lab02;

class ThreadTrain2 implements Runnable{
    private int trainCount=100;
    public Object obj=new Object();

    public boolean flag=true;
    //局部變量不會受到線程安全問題的影響
    @Override
    public void run() {
        if(flag){
            while (trainCount>0) {
                try {
                    Thread.sleep(50);
                } catch (Exception e) {
                }
                //使用了同步代碼塊,裏面用的是this鎖
                synchronized (obj) {
                    if (trainCount > 0) {
                        System.out.println(Thread.currentThread().getName() + ":開始售出第" + (100 - trainCount + 1) + "" + "張票");
                    }
                    trainCount--;
                }
            }
        }else{
            while (trainCount>0) {
                try {
                    sale();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    private synchronized void sale() throws InterruptedException {
        //同步代碼塊 ---走到這個位置的時候,可能會有多個線程進行訪問,誰先拿到鎖誰就先進行購票
            if (trainCount > 0) {
                Thread.sleep(40);
                System.out.println(Thread.currentThread().getName() + ":開始售出第" + (100 - trainCount + 1) + "" + "張票");
            }
            trainCount--;
        }
    }


//模擬火車站搶票
public class ThreadMain02 {
    public static void main(String[] args) throws InterruptedException {
        //爲了模擬有兩個窗口在搶票
        ThreadTrain2 threadTrain2=new ThreadTrain2();
        Thread t1= new Thread(threadTrain2,"窗口1");
        Thread t2= new Thread(threadTrain2,"窗口2");
        t1.start();
        Thread.sleep(40);
        threadTrain2.flag=false;
        t2.start();
    }
}

因爲兩個函數裏面使用的鎖一個是this,一個是obj,所以訪問的時候不能避免線程安全問題,因此會出現衝突的結果:售出第101張票。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章