synchronized 關鍵字
synchronized關鍵字是用來控制線程同步的,就是在多線程的環境下,控制synchronized代碼段不被多個線程同時執行。是一種阻塞性的鎖,synchronized既可以加在一段代碼上,也可以加在方法上。
synchronized(this)及非static的synchronized方法,只能防止多個線程同時執行同一個對象
的同步代碼段。當synchronized鎖住一個對象后,別的線程如果也想拿到這個對象的鎖,就必須等待這個線程執行完成釋放鎖,才能再次給對象加鎖,這樣才達到線程同步的目的。即使兩個不同的代碼段,都要鎖同一個對象,那么這兩個代碼段也不能在多線程環境下同時運行。
所以我們在用synchronized關鍵字的時候,能縮小代碼段的范圍就盡量縮小,能在代碼段上加同步就不要再整個方法上加同步。這叫減小鎖的粒度,使代碼更大程度的并發。原因是基于以上的思想,鎖的代碼段太長了,別的線程是不是要等很久。
如果用synchronized加在靜態方法
上,就相當于用××××.class
鎖住整個方法內的代碼塊,此時是鎖住該類的Class對象,相當于一個全局,鎖。使用synchronized修飾的方法或者代碼塊可以看成是一個原子操作。
一個線程執行互斥代碼過程如下:
1. 獲得同步鎖;
2. 清空工作內存;
3. 從主內存拷貝對象副本到工作內存;
4. 執行代碼(計算或者輸出等);
5. 刷新主內存數據;
6. 釋放同步鎖。
所以,synchronized既保證了多線程的并發有序性,又保證了多線程的內存可見性。
volatile(非阻塞性的)
volatile的特性當我們聲明共享變量為volatile后,對這個變量的讀/寫將會很特別。理解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 {
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;
}
}
臨界區代碼的執行具有原子性。這意味著即使是64位的long型和double型變量,只要它是volatile變量,對該變量的讀寫就將具有原子性。
如果是多個volatile操作或類似于volatile++這種復合操作,這些操作整體上不具有原子性。簡而言之,volatile變量自身具有下列特性:
- 可見性: 對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。
- 對 volatile 變量的寫操作,不允許和它之前的讀寫操作打亂順序;對 volatile 變量的讀操作,不允許和它之后的讀寫亂序。
volatile不能保證原子性
,原子性不是volatile來保證的,如果操作本來就具有原子性,volatile就會保證這個原子性不會被打破,理解為加上同步。比如上面例子中的get和set函數。
以及下面這段線程不安全的singleton(單例模式)實現,盡管使用了volatile:
public class UnSafeSingleton {
private static volatile UnSafeSingleton
sInstance = null;
private UnSafeSingleton() {}
public static UnSafeSingleton getInstance(){
if (sInstance == null) {
sInstance = new UnSafeSingleton();
}
return sInstance;
}
}
關于原子性的理解:只對單個操作
就有原子性,比如i++這種就不是單個操作。
但是,輕易不要用volatile來替代synchronized來避免并發問題,因為很多原子操作自己可能并沒那么了解,除非你是并發專家。《java編程思想》的作者是這樣建議的。
單例模式的應用
先看一個"餓漢式"的模式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
這種方式比較常用,但容易產生垃圾對象。
優點:沒有加鎖,執行效率會提高。
缺點:類加載時就初始化,浪費內存。
它基于 Classloader 機制避免了多線程的同步問題,不過,instance 在類裝載時就實例化,雖然導致類裝載的原因有很多種,在單例模式中大多數都是調用 getInstance 方法, 但是也不能確定有其他的方式(或者其他的靜態方法,比如反射)導致類裝載,這時候初始化 instance顯然沒有達到 lazy loading 的效果。
一個雙重鎖定式的單例模式
public class Singleton {
private volatile static Singleton sSingleton;
private Singleton (){}
public static Singleton getSingleton() {
if (sSingleton == null) {
synchronized (Singleton.class) {
if (sSingleton == null) {
sSingleton = new Singleton();
}
}
}
return sSingleton;
}
}
討論下volatile關鍵字的必要性,如果沒有volatile關鍵字,問題可能會出在singleton = new Singleton();這句,用偽代碼表示
inst = allocat(); // 分配內存
sSingleton = inst; // 賦值
constructor(inst); // 真正執行構造函數
可能會由于虛擬機的優化等導致賦值操作先執行,而構造函數還沒完成,導致其他線程訪問得到singleton變量不為null,但初始化還未完成,導致程序崩潰。
下篇你見過這樣的單例模式嗎將見識到一種新的單例模式。