【多線程】2.同步方法、變量的併發訪問

變量的線程安全

  1. 方法內聲明的變量是線程安全的,因爲每個線程各自有這個變量的一個副本,數據不共享;
  2. 成員變量(對象級變量)是非線程安全的,因爲可能存在多個線程爭相修改的情況,多線程爭搶即不安全;

synchronized使用的鎖對象

鎖定對象

  1. 可以鎖定Object對象
  2. 可以鎖定Class對象

鎖定Object對象的幾種方法

  1. 通過給對象的非靜態方法增加synchronized聲明,即可將this對象作爲監視器,鎖對象,同一個對象中的多個synchronized方法是同步執行的,因爲多個方法使用了synchronized,那麼他們都是基於this實例對象作爲鎖對象,所以他們是同步執行的;
  2. 通過synchronized(this)的做法,可以聲明同步代碼塊,不需要將整個方法都設置成同步執行,以便提高效率,這種做法也是基於this實例對象,即同一個對象中所有的同步代碼塊,非靜態同步方法,都是同步執行的,如果是多個對象,那麼他們的this不同,所以多個對象之間的多線程不受影響;

鎖定Class的方法

  1. 通過給類靜態方法增加synchronized聲明,這時候由於是靜態方法,所以這時候的鎖對象是類,即xx.class,與上面的鎖Object的this對象區分開,不相互影響;
  2. 通過synchronized(xx.class)的方式,也可以對類對象進行加鎖,那麼這個時候所有進入這個同步代碼塊的線程都是需要排隊執行過這一段(多個對象的併發線程,也是同步執行,因爲是將類對象Class作爲鎖對象)

synchronized的特性

不保證synchronized和非synchronized方法的同步執行

synchronized關鍵字只是標識進入該方法或者該代碼片段的線程會同步執行,不能保證在同步代碼塊內的所有變量不會被其他線程所修改;

一個典型的例子就是有變量A和變量B,有一個同步方法和一個非同步方法,都會修改這兩個變量,那同步方法可以保證方法內執行的邏輯是多個線程同步執行,但是不能保證變量A和變量B在同步方法修改前或者修改後不會被其他線程修改,因爲同步方法只是限定了一段代碼塊不會被多個線程異步執行,但是不會限制變量不能被多個線程修改;

鎖重入

當一個線程已經獲得某個鎖的時候,他再次申請獲得該鎖,那他是一定可以獲得的;舉例就是一個線程調用了一個對象同步方法,然後再調用這個對象的另外一個同步方法,這個時候也是可以獲得鎖的,這也是合理的做法,一個線程不可能被自己所阻塞了;

同步方法的一些其他特性

  1. 當在同步代碼塊中拋出異常,鎖對象會自動釋放;
  2. 同步不具備繼承性,父類的方法是同步方法,子類重載這個方法,子類的方法執行段不是同步的,通過super調用父類的方法是同步的;

同步代碼塊

與同步方法比較

同步代碼塊可以執行鎖定部分代碼片段,不一定要同步整個方法的代碼,這樣如果控制得好可以提升性能;這個操作就類似於try-catch儘量包括少的代碼片段一個道理;

同步代碼塊可以使用synchronized(this), synchronized(其他任意對象)作爲鎖對象,需求同一個鎖對象的多個線程會在同步代碼塊同步執行;

常量池給同步方法帶來的特性

由於java中將字符串、int部分字符緩存成常量池,可以在代碼中看到:

String a = "AA";
String b = "AA";

System.out.println(a == b);	// true

因爲a、b都指向了常量池的同一個字符串常量,應用在同步對象鎖的原理相同,此時 synchronized(a|b)的線程會同步執行,由此可以得知synchronized方法判斷對象鎖的方式是比較對象是否相等,即對象的內存地址;

多線程的死鎖

之前討論到了同步鎖可以是任意的對象,假設在同步代碼塊中(取得到了鎖對象A),之後再去要求鎖對象B,這時候就會可能引發多線程的死鎖條件(資源永遠不可能得到滿足),參考:

class ThreadA {
	synchronized(lockA) {
		doSomeThing();
		synchronized(lockB) {
			// 此處可能得不到滿足
		}
	}
}

class ThreadB {
	synchronized(lockB) {
		doSomeThing();
		synchronized(lockA) {
			// 此處可能得不到滿足
		}
	}
}

上面的僞代碼中,當線程A獲得了lockA,線程B獲得了lockB,之後他們再繼續申請lockB和lockA是不可能得到鎖對象,因爲各自線程都持有對方想要的鎖,而去申請對方持有的鎖,這就導致了永遠無法滿足這種情況,出現了多線程的死鎖;(多線程的死鎖可以通過jstack -l <pid>去發現 )

鎖對象的改變

上面提到過,可以將synchronized使用的鎖對象理解爲對象的內存地址,那麼當引用指向的對象發生改變,同樣的在synchronized鎖的對象也就相應發生改變,運行到synchronized語句後,解析引用爲具體對象地址,然後後續不會隨引用改變而改變;可以看一個例子

package test;

/**
* 鎖對象發生改變
*/
public class LockObjectChangeService {

    private String lock = "123";

    public void testMethod() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " begin " + System.currentTimeMillis());
            lock = "456";
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " end " + System.currentTimeMillis());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        LockObjectChangeService service = new LockObjectChangeService();

        ThreadA threadA = new ThreadA(service);
        threadA.setName("a");
        ThreadB threadB = new ThreadB(service);
        threadB.setName("b");

        threadA.start();
        Thread.sleep(50);	// 這裏爲了保證A線程能夠執行到更改lock的那一句
        threadB.start();

    }
}

class ThreadA extends Thread {

    private final LockObjectChangeService lockObjectChangeService;

    ThreadA(LockObjectChangeService lockObjectChangeService) {
        this.lockObjectChangeService = lockObjectChangeService;
    }

    @Override
    public void run() {
        lockObjectChangeService.testMethod();
    }
}

class ThreadB extends Thread {

    private final LockObjectChangeService lockObjectChangeService;

    ThreadB(LockObjectChangeService lockObjectChangeService) {
        this.lockObjectChangeService = lockObjectChangeService;
    }

    @Override
    public void run() {
        lockObjectChangeService.testMethod();
    }
}

通過這個例子可以發現,執行synchronized方法的時候,先解析引用地址,然後將地址作爲鎖對象(其實如果通過jvm的實現來看,他是在對象的對象頭markword部分寫入當前線程信息,即獲得該對象的對象鎖的線程信息,那可以理解,其實就是在那個對象上面打了一個自己線程的記號)

volatile關鍵字的使用

作用

volatile的作用就是使變量在多個線程之間可見

保證內存可見性

一般jvm爲了提升多線程的執行效率,會有一個公用數據棧,之後各個線程之間會從公用數據棧複製一份作爲自己的私有數據棧,每次修改完成私有數據棧之後,再同步回公用數據棧,再通知其他線程從公用數據棧同步回私有數據棧;

多線程的工作內存

舉例說明:如果有AB兩個線程同時拿到變量i,進行遞增操作。A線程將變量i放到自己的工作內存中,然後做+1操作,然而此時,線程A還沒有將修改後的值刷回到主內存中,而此時線程B也從主內存中拿到修改前的變量i,也進行了一遍+1的操作。最後A和B線程將各自的結果分別刷回到主內存中,看到的結果就是變量i只進行了一遍+1的操作,而實際上A和B進行了兩次累加的操作,於是就出現了錯誤。究其原因,是因爲線程B讀取到了變量i的髒數據的緣故;

此時如果對變量i加上volatile關鍵字修飾的話,它可以保證當A線程對變量i值做了變動之後,會立即刷回到主內存中,而其它線程讀取到該變量的值也作廢,強迫重新從主內存中讀取該變量的值,這樣在任何時刻,AB線程總是會看到變量i的同一個值。

不保證原子性

關鍵字雖然保證了實例變量在多個線程之間的可見性,但是他不具備同步性,那麼他也就不具備原子性。

舉例說明,我們常用的i++,他是分成兩個部分,第一步計算 i+1的結果,第二步將結果賦值回i,因爲不是原子操作,可能導致線程A,線程B同時進行了第一步,然後同時執行第二步,這就導致了i的結果等於2(初始化i=1

假設要保證i++的原子性,需要配合synchronized關鍵字來加同步鎖。

synchronzied代碼塊也具有volatile的作用

synchronized關鍵詞可以保證代碼塊的同步執行,也會將當前線程的工作內存與主內存進行同步,刷新回主內容,讓其他線程感知。

比較synchronized和volatile關鍵字

  1. 關鍵字volatile是線程同步的輕量級實現,volatile的性能優於synchronized,其中volatile只能修飾變量,synchronized可以修飾方法、代碼塊;隨着JDK新版本的發佈,synchronized的執行效率大幅度提升(主要體現在新版synchronized使用了偏向鎖、自旋鎖、重量鎖三個等級的鎖升級),synchronized關鍵字的使用比例大大增加;
  2. 多線程訪問volatile不會發生阻塞,多線程訪問synchronized會發生阻塞;
  3. volatile可以保證數據可見性,但是不能保證數據原子性;synchronized可以保證數據的原子性,也能間接地保證數據的可見性(synchronized會將工作內存與主內存進行同步)
  4. synchronizedvolatile解決的問題不一樣,synchronized解決的問題是多個線程併發訪問資源的同步性,volatile解決的是變量在多個線程的可見性;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章