Java語言包含兩種內在的同步機制:同步塊(或方法)和 volatile 變量。這兩種機制的提出都是為了實現代碼線程的安全性。其中 Volatile 變量的同步性較差(但有時它更簡單並且開銷更低),而且其使用也更容易出錯。
Java中的同步塊用synchronized標記。同步塊在Java中是同步在某個對象上。所有同步在一個對象上的同步塊在同時只能被一個線程進入並執行操作。所有其他等待進入該同步塊的線程將被阻塞,直到執行該同步塊中的線程退出。
有四種不同的同步塊:
實例方法:一個實例一個線程。
靜態方法:一個類只能由一個線程同時執行。
實例方法中的同步塊
靜態方法中的同步塊
在多線程下最好是使用這種:
public class MyClass { public static void log2(String msg1, String msg2){ synchronized(MyClass.class){ log.writeln(msg1); log.writeln(msg2); } } }
上述同步塊都同步在不同對象上。實際需要那種同步塊視具體情況而定。
下面是一個同步的實例方法:
public synchronized void add(int value){ this.count += value; }
注意在方法聲明中同步(synchronized )關鍵字。這告訴Java該方法是同步的。
Java實例方法同步是同步在擁有該方法的對象上。這樣,每個實例其方法同步都同步在不同的對象上,即該方法所屬的實例。只有一個線程能夠在實例方法同步塊中運行。如果有多個實例存在,那麼一個線程一次可以在一個實例同步塊中執行操作。一個實例一個線程。
靜態方法同步和實例方法同步方法一樣,也使用synchronized 關鍵字。Java靜態方法同步如下示例:
public static synchronized void add(int value){ count += value; }
同樣,這裡synchronized 關鍵字告訴Java這個方法是同步的。
靜態方法的同步是指同步在該方法所在的類對象上。因為在Java虛擬機中一個類只能對應一個類對象,所以同時只允許一個線程執行同一個類中的靜態同步方法。
對於不同類中的靜態同步方法,一個線程可以執行每個類中的靜態同步方法而無需等待。不管類中的那個靜態同步方法被調用,一個類只能由一個線程同時執行。
實例方法中的同步塊
有時你不需要同步整個方法,而是同步方法中的一部分。Java可以對方法的一部分進行同步。
在非同步的Java方法中的同步塊的例子如下所示:
public void add(int value){ synchronized(this){ this.count += value; } }
示例使用Java同步塊構造器來標記一塊代碼是同步的。該代碼在執行時和同步方法一樣。
注意Java同步塊構造器用括號將對象括起來。在上例中,使用了“this”,即為調用add方法的實例本身。在同步構造器中用括號括起來的對象叫做監視器對象。上述代碼使用監視器對象同步,同步實例方法使用調用方法本身的實例作為監視器對象。
一次只有一個線程能夠在同步於同一個監視器對象的Java方法內執行。
下面兩個例子都同步他們所調用的實例對象上,因此他們在同步的執行效果上是等效的。
public class MyClass { public synchronized void log1(String msg1, String msg2){ log.writeln(msg1); log.writeln(msg2); } public void log2(String msg1, String msg2){ synchronized(this){ log.writeln(msg1); log.writeln(msg2); } } }
在上例中,每次只有一個線程能夠在兩個同步塊中任意一個方法內執行。
如果第二個同步塊不是同步在this實例對象上,那麼兩個方法可以被線程同時執行。
和上面類似,下面是兩個靜態方法同步的例子。這些方法同步在該方法所屬的類對象上。
public class MyClass { public static synchronized void log1(String msg1, String msg2){ log.writeln(msg1); log.writeln(msg2); } public static void log2(String msg1, String msg2){ synchronized(MyClass.class){ log.writeln(msg1); log.writeln(msg2); } } }
這兩個方法不允許同時被線程訪問。
如果第二個同步塊不是同步在MyClass.class這個對象上。那麼這兩個方法可以同時被線程訪問。
在下面例子中,啟動了兩個線程,都調用Counter類同一個實例的add方法。因為同步在該方法所屬的實例上,所以同時只能有一個線程訪問該方法。
public class Counter{ long count = 0; public synchronized void add(long value){ this.count += value; } } public class CounterThread extends Thread{ protected Counter counter = null; public CounterThread(Counter counter){ this.counter = counter; } public void run() { for(int i=0; i<10; i++){ counter.add(i); } } } public class Example { public static void main(String[] args){ Counter counter = new Counter(); Thread threadA = new CounterThread(counter); Thread threadB = new CounterThread(counter); threadA.start(); threadB.start(); } }
創建了兩個線程。他們的構造器引用同一個Counter實例。Counter.add方法是同步在實例上,是因為add方法是實例方法並且被標記上synchronized關鍵字。因此每次只允許一個線程調用該方法。另外一個線程必須要等到第一個線程退出add()方法時,才能繼續執行方法。
如果兩個線程引用了兩個不同的Counter實例,那麼他們可以同時調用add()方法。這些方法調用了不同的對象,因此這些方法也就同步在不同的對象上。這些方法調用將不會被阻塞。如下面這個例子所示:
public class Example { public static void main(String[] args){ Counter counterA = new Counter(); Counter counterB = new Counter(); Thread threadA = new CounterThread(counterA); Thread threadB = new CounterThread(counterB); threadA.start(); threadB.start(); } }
注意這兩個線程,threadA和threadB,不再引用同一個counter實例。CounterA和counterB的add方法同步在他們所屬的對象上。調用counterA的add方法將不會阻塞調用counterB的add方法。
線程為了提高效率,將某成員變量(如A)拷貝了一份(如B),線程中對A的訪問其實訪問的是B。只在某些動作時才進行A和B的同步。因此存在A和B不一致的情況。volatile就是用來避免這種情況的。volatile告訴jvm, 它所修飾的變量不保留拷貝,直接訪問主內存中的(也就是上面說的A) 。
理解volatile特性的一個好方法是:把對volatile變量的單個讀/寫,看成是使用同一個監視器鎖對這些單個讀/寫操作做了同步。下面我們通過具體的示例來說明。
class VolatileFeaturesExample { volatile long vl = 0L; //使用volatile聲明64位的long型變量 public void set(long l) { vl = l; //單個volatile變量的寫 } public void getAndIncrement () { vl++; //復合(多個)volatile變量的讀/寫 } public long get() { return vl; //單個volatile變量的讀 } }
假設有多個線程分別調用上面程序的三個方法,這個程序在語意上和下面程序等價:
class VolatileFeaturesExample { long vl = 0L; // 64位的long型普通變量 public synchronized void set(long l) { //對單個的普通 變量的寫用同一個監視器同步 vl = l; } public void getAndIncrement () { //普通方法調用 long temp = get(); //調用已同步的讀方法 temp += 1L; //普通寫操作 set(temp); //調用已同步的寫方法 } public synchronized long get() { //對單個的普通變量的讀用同一個監視器同步 return vl; } }
如上面示例程序所示,對一個volatile變量的單個讀/寫操作,與對一個普通變量的讀/寫操作使用同一個監視器鎖來同步,它們之間的執行效果相同。
監視器鎖的happens-before規則保證釋放監視器和獲取監視器的兩個線程之間的內存可見性,這意味著對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入。
監視器鎖的語義決定了臨界區代碼的執行具有原子性。這意味著即使是64位的long型和double型變量,只要它是volatile變量,對該變量的讀寫就將具有原子性。如果是多個volatile操作或類似於volatile++這種復合操作,這些操作整體上不具有原子性。
簡而言之,volatile變量自身具有下列特性:
可見性。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入。 原子性:對任意單個volatile變量的讀/寫具有原子性,但類似於volatile++這種復合操作不具有原子性。
參考文章:
Java 理論與實踐: 正確使用 Volatile 變量