java實現線程安全

線程安全實現的方法

通過實現不變性、可見性、原子性、線程封閉、委託來實現線程安全。

synchronized鎖

synchronized是Java的一個關鍵字,它能夠將代碼塊(方法)鎖起來,保證了線程的原子性和可見性。

  • 它使用起來是非常簡單的,只要在代碼塊(方法)添加關鍵字synchronized,即可以實現同步的功能~

    public synchronized void test() {

        // doSomething
    }

synchronized是一種互斥鎖

  • 一次只能允許一個線程進入被鎖住的代碼塊

synchronized是一種內置鎖/監視器鎖

  • Java中每個對象都有一個內置鎖(監視器,也可以理解成鎖標記),而synchronized就是使用對象的內置鎖(監視器)來將代碼塊(方法)鎖定的!

synchronized一般用來修飾三種東西:

  • 修飾普通方法,synchronized獲取對象鎖。
  • 修飾代碼塊,synchronized可以獲取其他對象的鎖
  • 修飾靜態方法,synchronized獲取類鎖

Lock鎖

lock是一個接口,其中lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的。unLock()方法是用來釋放鎖的而ReentrantLock是唯一實現了Lock接口的類

  • Lock方式來獲取鎖支持中斷、超時不獲取、是非阻塞的
  • 提高了語義化,哪裏加鎖,哪裏解鎖都得寫出來
  • Lock顯式鎖可以給我們帶來很好的靈活性,但同時我們必須手動釋放鎖
  • 支持Condition條件對象
  • 允許多個讀線程同時訪問共享資源
public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意這個地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了鎖");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"釋放了鎖");
            lock.unlock();
        }
    }
}

 

volatile

volatile是Java提供的一種輕量級的同步機制,在併發編程中,它也扮演着比較重要的角色。同synchronized相比(synchronized通常稱爲重量級鎖),volatile更輕量級,相比使用synchronized所帶來的龐大開銷,倘若能恰當的合理的使用volatile,自然是美事一樁。

 

爲了能比較清晰徹底的理解volatile,我們一步一步來分析。首先來看看如下代碼


public class TestVolatile {
    boolean status = false;

    /**
     * 狀態切換爲true
     */
    public void changeStatus(){
        status = true;
    }

    /**
     * 若狀態爲true,則running。
     */
    public void run(){
        if(status){
            System.out.println("running....");
        }
    }
}

上面這個例子,在多線程環境裏,假設線程A執行changeStatus()方法後,線程B運行run()方法,可以保證輸出"running....."嗎?

  答案是NO! 

  這個結論會讓人有些疑惑,可以理解。因爲倘若在單線程模型裏,先運行changeStatus方法,再執行run方法,自然是可以正確輸出"running...."的;但是在多線程模型中,是沒法做這種保證的。因爲對於共享變量status來說,線程A的修改,對於線程B來講,是"不可見"的。也就是說,線程B此時可能無法觀測到status已被修改爲true。那麼什麼是可見性呢?

  所謂可見性,是指當一條線程修改了共享變量的值,新值對於其他線程來說是可以立即得知的。很顯然,上述的例子中是沒有辦法做到內存可見性的。

  爲什麼出現這種情況呢,我們需要先了解一下JMM(java內存模型)

  java虛擬機有自己的內存模型(Java Memory Model,JMM),JMM可以屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓java程序在各種平臺下都能達到一致的內存訪問效果。

  JMM決定一個線程對共享變量的寫入何時對另一個線程可見,JMM定義了線程和主內存之間的抽象關係:共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory),本地內存保存了被該線程使用到的主內存的副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量。

大概瞭解了JMM的簡單定義後,問題就很容易理解了,對於普通的共享變量來講,比如我們上文中的status,線程A將其修改爲true這個動作發生在線程A的本地內存中,此時還未同步到主內存中去;而線程B緩存了status的初始值false,此時可能沒有觀測到status的值被修改了,所以就導致了上述的問題。那麼這種共享變量在多線程模型中的不可見性如何解決呢?比較粗暴的方式自然就是加鎖,但是此處使用synchronized或者Lock這些方式太重量級了,有點炮打蚊子的意思。比較合理的方式其實就是volatile

  volatile具備兩種特性,第一就是保證共享變量對所有線程的可見性。將一個共享變量聲明爲volatile後,會有以下效應:

    1.當寫一個volatile變量時,JMM會把該線程對應的本地內存中的變量強制刷新到主內存中去;

    2.這個寫會操作會導致其他線程中的緩存無效。

上面的例子只需將status聲明爲volatile,即可保證在線程A將其修改爲true時,線程B可以立刻得知

 volatile boolean status = false;

 

  • volatile還可以防止重排序(重排序指的就是:程序執行的時候,CPU、編譯器可能會對執行順序做一些調整,導致執行的順序並不是從上往下的。從而出現了一些意想不到的效果)。而如果聲明瞭volatile,那麼CPU、編譯器就會知道這個變量是共享的,不會被緩存在寄存器或者其他不可見的地方。
public class TestVolatile {
    int a = 1;
    boolean status = false;

    /**
     * 狀態切換爲true
     */
    public void changeStatus(){
        a = 2;//1
        status = true;//2
    }

    /**
     * 若狀態爲true,則running。
     */
    public void run(){
        if(status){//3
            int b = a+1;//4
            System.out.println(b);
        }
    }
}

 假設線程A執行changeStatus後,線程B執行run,我們能保證在4處,b一定等於3麼?

  答案依然是無法保證!也有可能b仍然爲2。上面我們提到過,爲了提供程序並行度,編譯器和處理器可能會對指令進行重排序,而上例中的1和2由於不存在數據依賴關係,則有可能會被重排序,先執行status=true再執行a=2。而此時線程B會順利到達4處,而線程A中a=2這個操作還未被執行,所以b=a+1的結果也有可能依然等於2。

  使用volatile關鍵字修飾共享變量便可以禁止這種重排序。若用volatile修飾共享變量,在編譯時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序

volatile禁止指令重排序也有一些規則,簡單列舉一下:

  1.當第二個操作是voaltile寫時,無論第一個操作是什麼,都不能進行重排序

  2.當地一個操作是volatile讀時,不管第二個操作是什麼,都不能進行重排序

  3.當第一個操作是volatile寫時,第二個操作是volatile讀時,不能進行重排序

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