同步是在被稱為內部鎖或者管鎖的內部實體上建立起來的。內部鎖在同步的兩個方面都扮演了重要的角色:加強對一個對象狀態互斥的進入,以及建立對可見性很重要的happens-before關系。
每個對象都有一個與其關聯的內部鎖。習慣地說,如果一個線程需要對某個對象的域進行互斥、持續的訪問,那么該線程需要在訪問之前,取得這個對象的內部鎖。線程持有鎖的時間,是指該線程獲得鎖,到它釋放鎖之間的時間。只要一個線程持有一個內部鎖,其他線程就無法獲取相同的鎖。當其他線程試圖獲取時,它們會被阻塞。
當一個線程釋放一個內部鎖時,一個happens-before關系就在釋放的動作和之后對相同鎖的獲取動作之間建立。
同步方法中的鎖
當一個線程調用一個同步方法時,它自動獲取了該對象的內部鎖,然后在方法返回時自動釋放。即使返回是異常退出,鎖仍然會釋放。
同步代碼塊中的鎖
另一個創建同步代碼的方式是使用同步代碼塊。不像是同步方法,同步代碼塊必須指定提供內部鎖的對象:
public void addName(String name) {
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}
在這個例子中,addName方法需要對lastName和nameCount進行變化同步,但同時也需要避免同步調用該對象的其他方法(在同步代碼塊中調用其他方法,會導致活鎖,這會在之后的章節講到)。沒有同步語句,那只會有一個分離的、非同步方法,只為了調用nameList.add。
同步語句使用更細致的同步顆粒,對提高并發也很有幫助。比如,有一個MsLLunch類,它有兩個變量,c1和c2,它們從來不會同時使用。所有對這兩個域進行修改的操作都必須是同步的,但c1和c2各自的更新操作卻沒有理由被對方打擾——如果這樣做,會降低并發,因為創造了不必要的阻塞。因此此處不用同步方法,或是與this相關的鎖,我們創建兩個獨立的對象,來關聯鎖。
public class MsLunch {
private long c1 = 0;
private long c2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1() {
synchronized(lock1) {
c1++;
}
}
public void inc2() {
synchronized(lock2) {
c2++;
}
}
}
使用這個用法需要非常小心。你需要要完成確定對域交錯訪問是真的安全。
可重入同步
一個線程無法獲取被另一個線程持有的鎖,但一個線程可以獲取它自己已經擁有的鎖。允許一個鎖來超過一次、反復獲取相同的鎖,就被稱為允許可重入同步。這描述了以下的情況:同步的代碼直接或間接地調用了一個方法,其中也包含著同步的代碼,并且這兩個代碼集使用相同的鎖。如果沒有可重入同步,那么被同步的代碼必須要有更多的預先檢查,來防止一個線程自己導致自己阻塞。