與synchronized類似的,lock也能夠達到同步的效果。
1. 利用加鎖同步
鎖是一種實現資源排他使用的機制。
- 對於實例方法,要給調用該方法的對象加鎖。
- 對於靜態方法,要給這個類加鎖。
- 如果一個線程調用一個對象上的同步實例方法(靜態方法),首先給該對象(類)加鎖,然後執行該方法,最後解鎖。
- 在解鎖之前,另一個調用那個對象(類)中方法的線程將被阻塞,直到解鎖。
與 synchronized (someObject) 類似的,lock()方法,表示當前線程佔用lock對象,一旦佔用,其他線程就不能佔用了。
與 synchronized 不同的是,一旦synchronized 塊結束,就會自動釋放對someObject的佔用。 lock卻必須調用unlock方法進行手動釋放,爲了保證釋放的執行,往往會把unlock() 放在finally中進行。
Java 可以 顯式地加鎖,這給協調線程帶來了更多的控制功能。一個鎖是一個Lock 接口的實例,它定義了加鎖和釋放鎖的方法。鎖也可以使用 newCondition()
方法來創建任意個數的Condition 對象,用來進行線程通信。
ReentrantLock 是Lock 的一個具體實現,用於創建相互排斥的鎖。可以創建具有特定的公平策略
的鎖。
Lock lock = new ReentrantLock();
公平策略值爲真,則確保等待時間最長的線程首先獲得鎖
。
取值爲假的公平策略將鎖給任意一個在等待的線程。被多個線程訪問的使用公正鎖的程序,其整體性能可能比那些使用默認設置的程序差,但是在獲取鎖且避免資源缺乏時可以有更小的時間變化。
使用顯式鎖方法修改
private static class Account{
private static Lock lock = new ReentrantLock(); // creat a lock
private int balance = 0;
public int getBalance() {
return balance;
}
public void deposit(int amount) {
lock.lock();
try {
int newBalance = balance + amount;
Thread.sleep(5);
balance = newBalance;
}
catch(InterruptedException ex) {
}
finally {
lock.unlock(); // release the lock
}
}
}
在對lock() 的調用之後緊隨一個try - catch 塊並且在finally 子句中釋放這個鎖
是一個很好的編程習慣!
通常,使用synchronized 方法或語句比使用相互排斥的顯式鎖簡單些。然而,使用顯式鎖對同步具有狀態的線程更加直觀和靈活。
2. tryLock
synchronized 是不佔用到手不罷休的,會一直試圖佔用下去。
與 synchronized 的鑽牛角尖不一樣,Lock接口還提供了一個 trylock
方法。
trylock會在指定時間範圍內試圖佔用,佔成功了,執行。 如果時間到了,還佔用不成功,扭頭就走~
注意: 因爲使用trylock有可能成功,有可能失敗,所以後面unlock釋放鎖的時候,需要判斷是否佔用成功了,如果沒佔用成功也unlock,就會拋出異常
Thread t1 = new Thread() {
public void run() {
boolean locked = false;
try {
locked = lock.tryLock(1,TimeUnit.SECONDS);
if(locked){
Thread.sleep(5000);
}
else{
}
}
catch (InterruptedException e) {
e.printStackTrace();
}
finally {
if(locked)
lock.unlock();
}
}
};
t1.setName("t1");
t1.start();
3. 死鎖
- 線程1 首先佔有對象1,接着試圖佔有對象2
- 線程2 首先佔有對象2,接着試圖佔有對象1
- 線程1 等待線程2釋放對象2
- 與此同時,線程2等待線程1釋放對象1
使用一種稱爲 資源排序 的簡單技術可以輕易地避免死鎖的發生。
給每一個需要鎖的對象指定一個順序,確保每個線程都按這個順序來獲取鎖
4. 線程間交互
鎖上的條件可以用於協調線程之間的交互。
通過保證在臨界區上多個線程的相互排斥,線程同步完全可以避免競爭條件的發生,但
是有時候,還需要線程之間的相互協作。 可以使用條件實現線程間通信。
一個線程可以指定在某種條件下該做什麼。條件是通過調用Lock 對象的newCondition() 方法而創建的對象。一旦創建了條件,就可以 首先通過lock對象得到一個Condition對象,然後分別調用這個Condition對象的:await, signal,signalAll 方法來實現線程之間的相互通信。
await()
方法可以讓當前線程進入等待,直到條件發生。
signal()
方法喚醒一個等待的線程。
signalAll()
喚醒所有等待的線程。
條件由Lock 對象創建。爲了調用它的方法(await() 、signal()和signalAll()),必須首先擁有鎖。如果沒有獲取鎖就調用這些方法,會拋出 IllegalMonitorStateException
異常。
接下來舉例:給一個銀行賬戶存取款,當要取款時,如果賬戶餘額小於輸入的取款金額,則等待賬戶存錢,直至大於輸入金額後,進行取款操作。
package Thread;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
public class ThreadCooperation {
private static Account account = new Account();
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(new DepositTask()); // 提交提款任務
executorService.execute(new WithdrawTask()); // 提交存款任務
executorService.shutdown();
System.out.println("Thread 1\t\t Thread 2\t\t Balance");
}
public static class DepositTask implements Runnable{
@Override
public void run() {
try {
while(true) {
account.deposit((int)(Math.random()*10)+1);
Thread.sleep(1000);
}
}
catch(InterruptedException ex) {
ex.printStackTrace();
}
}
}
public static class WithdrawTask implements Runnable{
@Override
public void run() {
while(true) {
account.withdraw((int)(Math.random()*10)+1);
}
}
}
private static class Account{
private static Lock lock = new ReentrantLock(); // 創建一個鎖
private static Condition newDeposit = lock.newCondition(); // 鎖上的條件
private int balance = 0;
public int getBalance() {
return balance;
}
public void withdraw(int amount) {
lock.lock(); // 獲取鎖
try {
while(balance < amount) {
System.out.println("\t\t Wait for a deposit");
newDeposit.await(); // 等待newDeposit條件
}
balance -= amount;
System.out.println("\t\t\tWithdraw " + amount + "\t\t" + getBalance());
}
catch(InterruptedException ex) {
ex.printStackTrace();
}
finally {
lock.unlock(); // 釋放鎖
}
}
public void deposit(int amount) {
lock.lock();
try {
balance += amount;
System.out.println("deposit " + amount + "\t\t\t\t\t" + getBalance());
newDeposit.signalAll();
}
finally {
lock.unlock(); // release the lock
}
}
}
}
5. 線程狀態
任務在線程中執行。線程可以是以下5 種狀態之一: 新建、就緒、運行、阻塞、結束。
新創建一個線程時,它就進入新建狀態(New) 。調用線程的startO 方法啓動線程後,它進入就緒狀態(Ready) 。就緒線程是可運行的,但可能還沒有開始運行。操作系統必須爲它分配CPU 時間。
就緒線程開始運行時,它就進入運行狀態。如果給定的CPU 時間用完或調用線程的 yield()
方法,處於運行狀態的線程可能就進入就緒狀態。
有幾種原因可能使線程進人阻塞狀態(即非活動狀態) 。可能是它自己調用了join() 、sleep() 或 wait() 方法。它可能是在等待I/O 操作的完成。當使得其處於非激活狀態的動作不起作用時,阻塞線程可能被重新激活。例如,如果線程處於休眠狀態並且休眠時間已過期,線程就會被重新激活並進入就緒狀態。
最後,如果一個線程執行完它的run()方法,這個線程就被結束(finished) 。
isAlive()
方法是用來判斷線程狀態的方法。如果線程處於就緒、阻塞或運行狀態,則返回true; 如果線程處於新建並且沒有啓動的狀態,或者已經結束,則返回false
方法interrupt() 按下列方式中斷一個線程:當線程當前處於就緒或運行狀態時,給它設置一箇中斷標誌;當線程處於阻塞狀態時,它將被喚醒並進入就緒狀態,同時拋出異常java.lang.lnterruptedException 。
6. 總結Lock和synchronized的區別
-
Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現,Lock是代碼層面的實現。
-
Lock可以選擇性的獲取鎖,如果一段時間獲取不到,可以放棄。synchronized不行,會一根筋一直獲取下去。 藉助Lock的這個特性,就能夠規避死鎖,synchronized必須通過謹慎和良好的設計,才能減少死鎖的發生。
-
synchronized在發生異常和同步塊結束的時候,會自動釋放鎖。而Lock必須手動釋放, 所以如果忘記了釋放鎖,一樣會造成死鎖。