package deadlock;
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() {
calLock_Obj1_First();
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
calLock_Obj2_First();
}
});
t1.start();
t2.start();
}
//先synchronized obj1,再synchronized obj2
private void calLock_Obj1_First() {
synchronized (obj1) {
sleep();
synchronized (obj2) {
sleep();
}
}
}
//先synchronized obj2,再synchronized obj1
private void calLock_Obj2_First() {
synchronized (obj2) {
sleep();
synchronized (obj1) {
sleep();
}
}
}
/**
* 爲了便於讓兩個線程分別鎖住其中一個對象,
* 一個線程鎖住obj1,然後一直等待obj2,
* 另一個線程鎖住obj2,然後一直等待obj1,
* 然後就是一直等待,死鎖產生
*/
private void sleep() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
帶着問題考慮java同步的問題:
同步
在多線程程序中,同步修飾符用來控制對臨界區代碼的訪問。其中一種方式是用synchronized關鍵字來保證代碼的線程安全性。在Java中,synchronized修飾的代碼塊或方法不會被多個線程併發訪問。它強制要求線程在進入一個方法之前獲得一個鎖,在離開方法時釋放該鎖。它保證了在同一時刻只有一個線程能執行被其修飾的方法。
如果我們把一個方法或代碼塊定義爲同步的,就意味着在同一個對象中,只會有一個對同步方法的調用。如果在一個線程內部調用了一個同步方法,則其他線程會一直阻塞,直到第一個線程完成方法調用。
在進入一個對象的同步方法之前,需要申請對該對象上鎖,完成方法調用後釋放鎖供其他線程申請。同步方法遵循happens-before機制,它保證了對象狀態的改變在其他線程中都是可見的。
當標記一個代碼塊爲同步時,需要用一個對象作爲參數。當一個運行線程執行到該代碼塊時,要等到其他運行線程退出這個對象的同步代碼區。然而,一個線程可以進入另一個對象的同步代碼區。但是同一個對象的非同步方法可以不用申請鎖。
如果定義一個靜態方法爲同步,則是在類上同步,而不是在對象上同步。也即如果一個靜態同步方法在執行時,整個類被鎖住,對該類中的其他靜態方法調用會阻塞。
1)當一個線程進入了一個實例的同步方法,則其他任何線程都不能進入該實例的任何一個同步方法。
2)當一個線程進入了一個類的靜態同步方法,則其他任何線程都不能進入該類的任何一個靜態同步方法。
注意:
-
同步的靜態方法和非靜態方法之間沒有關係。也即靜態同步方法和非靜態同步方法可以同時執行,除非非靜態同步方法顯式在該類上同步(例如,synchronized(MyClass.class){…})
-
類的構造函數不能定義成同步的。
監視器或內部鎖
鎖限制了對某個對象狀態的訪問,同時保證了happens-before關係。
每個對象都有一個鎖對象,一個線程在訪問對象之前必須申請鎖,完成以後釋放鎖。其他線程不能訪問對象,知道獲得該對象的鎖。這保證了一個線程改變了對象的狀態後,新的狀態對其他在同一個監視器上線程可見。
當線程釋放鎖時,會將cache中的內容更新到主內存,這也就使得該對象的狀態變化對其他線程是可見的——這就是happens-before關係。
synchronized和volatile,包括Thread.start()和Thread.join()方法,都能保證happens-before關係。
同步語句和同步方法獲取的鎖相同,某個線程可以請求同一個鎖多次。
一個線程獲得了對象鎖後,不會影響其他線程訪問對象的字段或調用對象的非同步方法。
同步語句首先嚐試獲取對象的鎖,獲取成功後立即開始執行同步代碼塊,執行完後釋放鎖。
如果方法是對象成員或對象實例,線程將鎖住該實例。如果方法是靜態的,線程鎖住的是該類對應的Class對象。同步方法用SYNCHRONIZED標記,該標記被方法調用指令識別。
原子變量
來看語句 int c++,它包含多個操作,e.g. 從內存讀取c的值,將c的值加1,然後寫回內存。這個操作對單線程來說是正確的,但是在多線程環境卻可能出錯。它存在競態條件,在多線程環境中可能多個線程同時讀取c的值
原子訪問保證所有操作作爲一個整體一次完成。一個原子操作要麼完全執行要麼完全不執行。
以下這些操作能認爲是原子操作:
-
對引用類型和大部分基本數據類型(long和double類型除外)的讀和寫操作。
-
聲明爲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能夠保證可見性,但是也帶來了競態條件。它不會鎖定等待完成某個操作。例如:
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()提示虛擬機進行垃圾回收,但是不能強迫其執行。