SE高階(5):多線程—②線程同步、死鎖、volatile關鍵字

線程同步的作用

在多線程中,當兩個及以上線程併發訪問同個資源時,儘管有控制線程的方法,但由於線程調度具有不確定性,所以極容易導致錯誤,這時就需要線程同步機制來解決此問題。


實現線程同步——同步監視器

Java中,任何對象都能作爲同步監視器。同步監視器是爲了確保一個線程在對對象操作時,別的線程無權訪問,即任何時刻只能有一個線程對同步監視器鎖定。所以一般把併發訪問的共享資源作爲同步監視器。
在Java中,使用關鍵字synchronized或者Lock類來對同步監視器鎖定。

synchronized的用法

  • 同步代碼塊:synchronized(obj){},傳入的obj對象就是同步監視器,在執行同步代碼塊中的代碼之前,必須先獲得對同步監視器的鎖定。
  • synchronized修飾方法:同步方法的監視器是this,所以調用該方法的對象就是同步監視器,則無需顯式指定。

Lock的用法

  • 有ReentrantLock(可重入鎖)、ReentrantReadWriteLock(可重入讀寫鎖)、以及Java8新增的StampedLock類。
  • Lock對象相當於同步監視器對象,併發訪問時,每次只能允許一個線程對Lock對象加鎖,執行完之後必須手動釋放鎖,釋放鎖方法一定要放在finally語句中。
  • 另外ReentrantReadWriteLock(提供了三種讀寫操作模式,但不常用。

線程同步實例

先給上一段多線程實現的取錢代碼,沒有采用線程同步,看看會出現什麼情況。

public class Account {
	private int balance;//餘額
	public Account(int balance) {
		this.balance = balance;
		System.out.println("賬戶當前餘額:" + balance);
	}	
	//取錢方法
	public void quqian(int money) {
		if(money <= balance) {
			System.out.println(Thread.currentThread().getName() + ",取出" + money);
			//睡眠線程2s,用於測試另一個線程
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {e.printStackTrace();}							
			//修改餘額的代碼
			this.balance -= money;
			System.out.println(Thread.currentThread().getName()+",餘額:" + this.balance);
		}else {
			System.out.println(Thread.currentThread().getName() + "取錢失敗!");
		}		
	}
}	
//執行存錢操作的Runnable線程
class QqPerson implements Runnable{
	private int money;
	private Account ac;
	public QqPerson(Account ac,int money) {
		this.ac = ac;
		this.money = money;
	}
	@Override
	public void run() {
		ac.quqian(money);		
	}
}
public class Test {
	public static void main(String[] args) {
		Account ac = new Account(1000);
		//傳入Account對象、取錢金額
		QqPerson q1 = new QqPerson(ac,600);
		QqPerson q2 = new QqPerson(ac,600);
		//線程類傳入Runnable對象
		Thread t1 = new Thread(q1,"取錢1號");
		Thread t2 = new Thread(q2,"取錢2號");
		//啓動線程
		t1.start();
		t2.start();
	}
}
  • 實例解析:在執行修改餘額代碼之前,調用sleep(2000)強制讓線程睡眠2s,這樣能更好地體現多個線程訪問共享資源時,由於線程調度的不確定性導致的線程安全問題。事實上,如果不加sleep(),就會出現一會正確一會錯誤的情況。遇到這種問題,就需要使用線程同步來解決。

使用同步代碼塊實例

//執行存錢操作的線程
class QqPerson implements Runnable{
	private int money;
	private Account ac;
	public QqPerson(Account ac,int money) {
		this.ac = ac;
		this.money = money;
	}
	@Override
	public void run() {
		//加入同步代碼塊
		synchronized(ac){
			ac.quqian(money);
		}
	}
}
  • 實例解析:其餘的代碼都是相同的,只是把併發訪問共享資源的執行代碼放入同步代碼塊中,在代碼中,依然使用sleep()來睡眠線程,但是會發現別的線程無法執行,這是因爲同步鎖還未被釋放,執行完後釋放鎖,別的線程才能獲取同步鎖。

使用同步方法實例

	//取錢方法
	public synchronized void quqian(int money) {
		if(money <= balance) {
			System.out.println(Thread.currentThread().getName() + ",取出" + money);
			//睡眠線程1s,用於測試另一個線程
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {e.printStackTrace();}							
			//修改餘額的代碼
			this.balance -= money;
			System.out.println(Thread.currentThread().getName()+",餘額:" + this.balance);
		}else {
			System.out.println(Thread.currentThread().getName() + "取錢失敗!");
		}		
	}
  • 實例解析:因爲併發訪問共享資源的執行代碼是由一個方法來完成,所以可以用synchronized修飾該方法。加鎖原理和同步代碼塊一樣,只是無需顯式加入同步鎖對象。
  • 加鎖流程:第一個線程執行取錢方法,先獲得同步鎖對象,然後執行方法內部代碼,執行到sleep()會睡眠當前線程,但因爲睡眠線程獲得了鎖對象,所以別的線程此時無權訪問,睡眠時間到了之後,又開始繼續執行,直到代碼執行結束,然後釋放鎖。下一個線程開始執行,又繼續前面的步驟。

使用Lock加鎖實例

public class Account {
	private int balance;//餘額
	ReentrantLock lock = new ReentrantLock();
	StampedLock slock = new StampedLock();
	
	public Account(int balance) {
		this.balance = balance;
		System.out.println("賬戶當前餘額:" + balance);
	}
	//取錢方法
	public  void quqian(int money) {
		try {
			lock.lock();//加鎖
			if(money <= balance) {
				System.out.println(Thread.currentThread().getName() + ",取出" + money);												
				//修改餘額的代碼
				this.balance -= money;
				System.out.println(Thread.currentThread().getName()+",餘額:" + this.balance);
			}else {
				System.out.println(Thread.currentThread().getName() + "取錢失敗!");
			}
		}finally {
			lock.unlock();//手動釋放鎖
		}
	}
}	
  • 實例解析:上述代碼使用了ReentrantLock(可重入鎖)來實現線程同步。Lock對象代表了併發訪問的同步監視器的領域,以Lock對象作爲顯式的同步鎖對象。使用Lock加鎖,一定要手動釋放鎖。

線程同步注意點:

  • 線程持有同步鎖對象期間,使用sleep()不會釋放鎖。但如果使用wait()會導致釋放同步鎖。
  • sleep()的調用者是當前線程,而wait()的調用者是對象,所以wait()會導致鎖被釋放。
  • 同步代碼塊需要顯式傳入同步鎖對象,同步方法無需同步鎖對象,因爲隱式傳入。
  • Lock對象相當於隱式傳入的同步鎖對象,所以使用時要保證該同步鎖對象是訪問共享資源的調用對象。
  • Lock可以嵌套加鎖,即在Lock加鎖的代碼中能執行另一段加鎖的代碼。

線程死鎖

線程同步時,兩個及以上線程相互持有同步鎖對象,這就導致無法對同步監視器對象進行鎖定,造成所有線程都進入阻塞狀態,這就是死鎖。出現死鎖不會出現異常,不會有任何提示。如果通過eclipse的控制檯來查看,看起來程序是結束了,但紅正方形的出現說明了程序是運行的,只是一直在等待鎖的釋放。

關鍵字volatile——用於修飾變量

volatile:易變、不穩定的。

volatile使用場景:

如果只是讀寫一兩個實例變量就使用線程同步,則開銷會很大,volatile關鍵字能爲實例變量的同步訪問提供了一種免鎖機制。用於在多線程中同步公共變量,保證用戶在操作一個變量,但不保證多線程的原子性。

原子性解釋:

假設對共享變量除了賦值之外並不完成其他操作,那麼可以將這些共享變量聲明爲volatile。

變量使用同步的條件:

如果向一個變量寫入值,而這個變量接下來可能會被另一個線程讀取,或者從一個變量讀值,而這個變量可能是之前被另一個線程寫入的,此時必須使用同步。




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