Java併發程式設計-synchronized指南

weixin_34041003發表於2016-03-10

       在多執行緒程式中,同步修飾符用來控制對臨界區程式碼的訪問。其中一種方式是用synchronized關鍵字來保證程式碼的執行緒安全性。在Java中,synchronized修飾的程式碼塊或方法不會被多個執行緒併發訪問。它強制要求執行緒在進入一個方法之前獲得一個鎖,在離開方法時釋放該鎖。它保證了在同一時刻只有一個執行緒能執行被其修飾的方法。

如果我們把一個方法或程式碼塊定義為同步的,就意味著在同一個物件中,只會有一個對同步方法的呼叫。如果在一個執行緒內部呼叫了一個同步方法,則其他執行緒會一直阻塞,直到第一個執行緒完成方法呼叫。

在進入一個物件的同步方法之前,需要申請對該物件上鎖,完成方法呼叫後釋放鎖供其他執行緒申請。同步方法遵循happens-before機制,它保證了物件狀態的改變在其他執行緒中都是可見的。

當標記一個程式碼塊為同步時,需要用一個物件作為引數。當一個執行執行緒執行到該程式碼塊時,要等到其他執行執行緒退出這個物件的同步程式碼區。然而,一個執行緒可以進入另一個物件的同步程式碼區。但是同一個物件的非同步方法可以不用申請鎖。

如果定義一個靜態方法為同步,則是在類上同步,而不是在物件上同步。也即如果一個靜態同步方法在執行時,整個類被鎖住,對該類中的其他靜態方法呼叫會阻塞。

1)當一個執行緒進入了一個例項的同步方法,則其他任何執行緒都不能進入該例項的任何一個同步方法。

2)當一個執行緒進入了一個類的靜態同步方法,則其他任何執行緒都不能進入該類的任何一個靜態同步方法。

注意:

  1. 同步的靜態方法和非靜態方法之間沒有關係。也即靜態同步方法和非靜態同步方法可以同時執行,除非非靜態同步方法顯式在該類上同步(例如,synchronized(MyClass.class){…})
  2. 類的建構函式不能定義成同步的。

監視器或內部鎖

鎖限制了對某個物件狀態的訪問,同時保證了happens-before關係。

每個物件都有一個鎖物件,一個執行緒在訪問物件之前必須申請鎖,完成以後釋放鎖。其他執行緒不能訪問物件,知道獲得該物件的鎖。這保證了一個執行緒改變了物件的狀態後,新的狀態對其他在同一個監視器上執行緒可見。

當執行緒釋放鎖時,會將cache中的內容更新到主記憶體,這也就使得該物件的狀態變化對其他執行緒是可見的——這就是happens-before關係。

synchronized和Volatile,包括Thread.start()和Thread.join()方法,都能保證happens-before關係。

同步語句和同步方法獲取的鎖相同,某個執行緒可以請求同一個鎖多次。

一個執行緒獲得了物件鎖後,不會影響其他執行緒訪問物件的欄位或呼叫物件的非同步方法。

同步語句首先嚐試獲取物件的鎖,獲取成功後立即開始執行同步程式碼塊,執行完後釋放鎖。

如果方法是物件成員或物件例項,執行緒將鎖住該例項。如果方法是靜態的,執行緒鎖住的是該類對應的Class物件。同步方法用SYNCHRONIZED標記,該標記被方法呼叫指令識別。

原子變數

來看語句 int c++,它包含多個操作,e.g. 從記憶體讀取c的值,將c的值加1,然後寫回記憶體。這個操作對單執行緒來說是正確的,但是在多執行緒環境卻可能出錯。它存在競態條件,在多執行緒環境中可能多個執行緒同時讀取c的值

原子訪問保證所有操作作為一個整體一次完成。一個原子操作要麼完全執行要麼完全不執行。

以下這些操作能認為是原子操作:

  1. 對引用型別和大部分基本資料型別(long和double型別除外)的讀和寫操作。
  2. 宣告為volatile型別變數的讀和寫操作(包括long和double變數)。

Java併發包java.util.concurrent.atomic定 義了對單個變數進行原子操作的類。所有類都有get和set方法,就像對volatile變數的讀寫一樣。這就意味著,一個寫操作happens- before其他任何對該變數的讀操作。原子方法compareAndSet同樣有這些特性,就像對整型變數做原子的算術運算一樣。

在Java 5.0的併發包中,定義了支援原子操作的類。Java虛擬機器編譯這些類時利用硬體提供的CAS(Compare and set)來實現。

  • AtomicInteger
  • AtomicLong
  • AtomicBoolean
  • AtomicReference

volatile變數

volatile只能用來修飾變數。用volatile修飾的變數可能被非同步地修改,所以編譯器會對它們特殊處理。

volatile修飾符保證讀取某個欄位的任何執行緒都能看到該變數最近被寫入的值。

使用volatile修飾的變數降低了記憶體一致性的風險,因為任何對volatile變數的寫操作都能被其他執行緒可見。另外,當一個執行緒訪問volatile變數時,不止能看到對該變數最近的修改,還能修改該變數的程式碼所帶來的其他影響。

在多執行緒環境中,物件在不同執行緒中都儲存有副本。但是volatile變數卻沒有,它們在堆中只有一個例項。這樣對volatile變數的修改就能立即對其他執行緒可見。另外,本地執行緒快取沒有完成後重新整理的工作。

volatile能夠保證可見性,但是也帶來了競態條件。它不會鎖定等待完成某個操作。例如:

1 volatile int i=0;

兩 個執行緒同時執行 i +=5 時,會得到5-10之間的某個值(譯者注:原文為i +=5 invoking by two simultaneously thread give result 5 or 10 but it guarantee to see immediate changes 感覺有問題)

使用場景:用一個volatile布林變數作為一個執行緒終止的標誌。

靜態和volatile變數之間的差別

宣告一個靜態變數,意味著該類的多個例項將共享該變數,靜態變數與類關聯而不是與物件關聯。執行緒可能會有靜態變數的本地快取值。

當兩個執行緒同時更新靜態(非volatile)變數的值時,可能有一個執行緒的快取中是一個過期的值。雖然多執行緒能夠訪問的是同一個靜態變數,每個執行緒還是可能會儲存自己的快取副本。

一個volatile變數則在記憶體中只保留一個副本,該副本在多個執行緒中共享。

volatile變數和同步之間的差別

線上程記憶體和主記憶體之間,volatile只是同步了一個變數的值,synchronized則同步了(synchronized塊中)所有變數的值,並且會鎖住和釋放一個監視器。所以,synchronized比volatile會有更多的開銷。

volatile變數不允許有一個本地副本與主記憶體中的值不同。一個宣告為volatile的變數必須保證所有執行緒中的副本同步,不管哪個執行緒修改了變數的值,另外其他執行緒都能立即看到該值。

鎖物件

鎖物件的作用像synchronized程式碼使用的隱式鎖一樣。像隱式鎖一樣,同時只能有一個執行緒持有鎖。鎖還支援wait/notify機制,通過他們之間的condition物件。

鎖物件相對於隱式鎖最大的優點是,他們能從嘗試獲得鎖的狀態返回。如果鎖當前不可用或者在一個超時時間之前,tryLock()方法能夠返回。在獲得鎖之前,如果其他執行緒傳送了一箇中斷,lockInterruptibly()方法能返回。

Java記憶體回收

在 Java中,建立的物件存放在堆中。Java堆被稱為記憶體回收堆。記憶體收集不能強制執行。當記憶體收集器執行時,它釋放掉那些不可達物件佔用的記憶體。垃圾收集執行緒作為一個優先順序較低的守護執行緒執行。你能通過System.gc()提示虛擬機器進行垃圾回收,但是不能強迫其執行。

如何寫一個死鎖程式

在多執行緒環境中,死鎖意味著兩個或多個執行緒一直阻塞,等待其他執行緒釋放鎖。下面是死鎖的一個示例:

public class DeadlockSample {
private final Object obj1 = new Object();
private final Object obj2 = new Object();

public static void main(String[] args) {
DeadlockSample test = new DeadlockSample();
test.testDeadlock();
}

private void testDeadlock() {
Thread t1 = new Thread(new Runnable() {
public void run() {
calLock12();
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
calLock21();
}
});
t1.start();
t2.start();

}
private void calLock12() {
synchronized (obj1) {
sleep();
synchronized (obj2) {
sleep();
}
}
}
private void calLock21() {
synchronized (obj2) {
sleep();
synchronized (obj1) {
sleep();
}
}
}
private void sleep() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
  • 垃圾收集器不會回收強引用。
  • 在記憶體不足時才會回收軟引用,所以用它實現快取可以避免記憶體不足。
  • 垃圾收集器將會在下一次垃圾收集時回收弱引用。弱引用能被用來實現特殊的map。java.util.WeakHashMap中的key就是弱引用。
  • 虛引用會被立即回收。能被用來跟蹤物件被垃圾回收的活動。

 

相關文章