1. 程式人生 > >synchronized關鍵字與volatile關鍵字

synchronized關鍵字與volatile關鍵字

一.synchronized關鍵字

在多執行緒程式設計中,常常看到synchronized關鍵字。從字面來理解,就是一個鎖,就是說被這個關鍵字修飾的部分相當於上了個鎖,拿到了這個鎖的執行緒才可以使用,其他執行緒無法使用,除非鎖被釋放。當然這只是淺顯的理解了synchronized,接下來,就讓我們來探索屬於synchronized的奧祕吧~

  • synchronized作用於何處?

  1. synchronized作用於例項方法
同步方法,鎖的是物件的例項
	public synchronized void update(){
		
	}

來看一段程式碼

public class SynchronizedTest implements Runnable {
	static int i = 0;//共享資料
	/*
	 * 自增操作
	 */
	public synchronized void increase() {
		i++;
	}

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

	public static void main(String[] args) throws InterruptedException {
		SynchronizedTest instance = new SynchronizedTest();
		Thread t1 = new Thread(instance);
		Thread t2 = new Thread(instance);
		Thread.currentThread().sleep(1000);
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(i);
	}
}

執行結果:

這個synchronized作用於increase這個例項方法上,實際上鎖的是呼叫該方法的例項物件instance。我們很清楚一個物件只能有一把鎖,因此當一個執行緒正在訪問一個物件的synchronized作用的例項方法時,其他執行緒的就不能訪問該物件的synchronized方法。這種同步方法也有可能會出現問題,比如當另一個執行緒重新new了一個例項物件,去訪問synchronized作用的方法,此時,是不影響的,所以會導致共享資源的安全性就無法保證了。

     2. synchronized作用於靜態方法

//加在靜態方法,鎖的是類
	public static synchronized void del(){
		
	}

     來看程式碼

public class SynchronizedTest implements Runnable {
	static int i=0;

    /**
     * 作用於靜態方法,鎖是當前class物件,也就是
     * SynchronizedTest類對應的class物件
     */
    public static synchronized void increase(){
        i++;
    }

    /**
     * 非靜態,訪問時鎖不一樣不會發生互斥
     */
    public synchronized void increase4Obj(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<10000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(new SynchronizedTest());
        Thread t2=new Thread(new SynchronizedTest());
        //啟動執行緒
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(i);
    }
}

執行結果依舊是20000.

當synchronized作用於靜態方法時,鎖的是當前類的物件

   3. synchronized作用於程式碼塊

//同步程式碼塊,鎖的是程式碼塊
	public void add(Object obj) {
		synchronized (obj) {
			
		}
	}
public class SynchronizedTest implements Runnable {
	static SynchronizedTest instance=new SynchronizedTest();
    static int i=0;
    @Override
    public void run() {
        synchronized(instance){
            for(int j=0;j<10000;j++){
                    i++;
              }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

synchronized作用於一個給定的例項物件instance,這個例項物件就是鎖物件,每次當執行緒進入synchronized包裹的程式碼塊時就會要求當前執行緒持有instance例項物件鎖,如果當前有其他執行緒正持有該物件鎖,那麼新到的執行緒就必須等待。

二.volatile關鍵字

首先來說幾個概念

原子性:意思就是操作不可被劃分,它要麼做,要麼不做。比如讀取和賦值操作

可見性:即一個共享變數被修改時,它的值會被立即更新到主存,這說明它是可見的。在Java中,普通的變數不具有可見性,只有被volatile關鍵字修飾的才具有可見性。或者可以通過synchronized和Lock加鎖的方式,保證在同一時刻只有一個執行緒去操作這個共享變數。

有序性:Java記憶體模型具有先天的有序性,稱其為happens-before原則,具體內容如下

  • 程式次序規則:一個執行緒內,按照程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作
  • 鎖定規則:一個unLock操作先行發生於後面對同一個鎖的lock操作
  • volatile變數規則:對一個變數的寫操作先行發生於後面對這個變數的讀操作
  • 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C
  • 執行緒啟動規則:Thread物件的start()方法先行發生於此執行緒的每個一個動作
  • 執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生
  • 執行緒終結規則:執行緒中所有的操作都先行發生於執行緒的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到執行緒已經終止執行
  • 物件終結規則:一個物件的初始化完成先行發生於他的finalize()方法的開始。

同時,Java中可以通過volatile關鍵字和加鎖的方式來保證有序性。

瞭解了上述幾個概念,接下來學習一下volatile關鍵字。

一旦一個變數被volatile修飾,就代表一個執行緒操作了這個變數,那麼其他變數是立即可以看到改變後的值的。

有幾個問題

1.volatile可以保證可見性嗎?

當然是可以的。Java中通過volatile來保證共享變數的可見性。

2.volatile可以保證原子性嗎?

不可以

來看程式碼

class Test{
	/**
	 * 執行結果總小於10000
	 * 自增操作不是原子操作!分為讀取當前值,對其+1,再寫入記憶體。假設某時刻變數a值為10,
	 執行緒1堆a進行讀操作後被阻塞,由於執行緒1對變數進行操作,導致執行緒2的快取
	 行無效,去主存讀取,執行緒1只進行讀操作,並未修改a的值,因此a變為11,此時執行緒1再進行
	 自增,對自己快取行內的10自增變為11,再將其寫入記憶體,本來我們理解會是12,但實際上a
	 的值為11 
	 */
	public volatile int a = 0;
	
	public void increase(){
		a++;
	}
}
public class Test2 {
	public static void main(String[] args) {
		final Test test = new Test();
		for(int i =0;i<10;i++){
			new Thread(){
				public void run(){
					for(int i = 0;i<1000;i++){
						test.increase();
					}
				}
			}.start();
		}
		
		while(Thread.activeCount()>1)  //保證前面的執行緒都執行完
            Thread.yield();
        System.out.println(test.a);
	}
}

這段程式碼的列印結果總是小於10000,與預期打印出10000不符,原因見程式碼註釋。

3.volatile可以保證有序性嗎?

在前面提到volatile關鍵字能禁止指令重排序,所以volatile能在一定程度上保證有序性。

  volatile關鍵字禁止指令重排序有兩層意思:

  1)當程式執行到volatile變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;保證先寫後讀。

  2)不能將在對volatile變數訪問的語句放在其後面執行,也不能把volatile變數後面的語句放到其前面執行。

三.synchronized與volatile比較

1.synchronized可以保證原子性和可見性,volatile只能保證可見性

2.volatile不會造成執行緒的阻塞,synchronized可能會造成執行緒的阻塞。由於volatile不需要加鎖,比synchronized更輕量級,不會阻塞執行緒

3.volatile是變數修飾符,僅能用於變數,而synchronized是一個方法或塊的修飾符。