java進階:15.3 多線程 - 鎖、ReentrantLock

與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 首先佔有對象1,接着試圖佔有對象2
  2. 線程2 首先佔有對象2,接着試圖佔有對象1
  3. 線程1 等待線程2釋放對象2
  4. 與此同時,線程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的區別

  1. Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現,Lock是代碼層面的實現。

  2. Lock可以選擇性的獲取鎖,如果一段時間獲取不到,可以放棄。synchronized不行,會一根筋一直獲取下去。 藉助Lock的這個特性,就能夠規避死鎖,synchronized必須通過謹慎和良好的設計,才能減少死鎖的發生。

  3. synchronized在發生異常和同步塊結束的時候,會自動釋放鎖。而Lock必須手動釋放, 所以如果忘記了釋放鎖,一樣會造成死鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章