synchronized實現線程安全的原理分析

synchronized

例子:我們模擬兩個線程取錢的操作,代碼如下:

class Account {
	 String accountNo;//賬戶名
	 double balance;//賬戶餘額
	public Account(String accountNo, double balance) {
	this.accountNo = accountNo;
		this.balance = balance;
	}

	public void draw(double drawAmount) {
	//如果賬戶餘額>=所取得錢數則取錢成功,否則失敗
		if (balance >= drawAmount) {
			balance -= drawAmount;
			System.out.println(Thread.currentThread().getName() + "取錢成功, 餘額爲:" + balance);
		} else {
			System.out.println(Thread.currentThread().getName() + "取錢失敗,餘額不足");
		}
	}
}

class DrawThread extends Thread{
	Account account;
	public DrawThread(Account account) {
		this.account=account;
	}
	@Override
	public void run() {
		while (account.balance>=100) {
			account.draw(100);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
	}
}
public class SynchronizeTest {
public static void main(String[] args) {
	Account account=new Account("dandan", 1000);
	DrawThread drawThread1= new DrawThread(account);
	DrawThread drawThread2= new DrawThread(account);
	drawThread1.start();
	drawThread2.start();
 }
}

運行結果:
在這裏插入圖片描述
然而運行結果,卻出現兩個800元,即就是線程安全問題,下面我們分析爲什麼會出現這種狀況,可能是下面的這種情況(但是這種情況並不唯一,因爲餘額的變動並不是原子操作):
在這裏插入圖片描述
那麼出現線程安全的原因是什麼?

  1. 存在兩個或者兩個以上的線程對象,而且線程之間共享着一個資源。
  2. 有多個語句操作了共享資源。
    爲了解決這個問題,java的多線程引入synchronized同步代碼塊和同步方法來解決這個問題,下面我們來看一下這兩種方法:

synchronized有兩種使用方式:
方式一:同步代碼塊
格式:
synchronized(鎖對象){
需要被同步的代碼…
}
注意:多線程操作的鎖對象必須是唯一共享的。否則無效。也就是說鎖對象是static的/常量,其實最簡單的就是使用一個常量作爲鎖對象
這種方式比較簡單,我們就不寫代碼的例子了。。。
方式二:同步函數
同步函數:同步函數就是使用synchronized修飾一個函數。
注意:

  1. 如果是一個非靜態的同步函數的鎖對象是this對象,如果是靜態的同步函數的鎖對象是當前函數所屬的類的字節碼文件(class對象)。因此,同步方法只能保證多個線程同時執行同一個對象的同步代碼段
  2. 同步函數的鎖對象是固定的,不能自己來指定的。
    下面我們用Synchronized修飾這個取錢的方法,代碼如下:
class Account {
	 String accountNo;
	 double balance;

	public Account(String accountNo, double balance) {
		// TODO Auto-generated constructor stub
		this.accountNo = accountNo;
		this.balance = balance;
	}

	public synchronized void draw(double drawAmount) {
		if (balance >= drawAmount) {
			balance -= drawAmount;
			System.out.println(Thread.currentThread().getName() + "取錢成功, 餘額爲:" + balance);
		} else {
			System.out.println(Thread.currentThread().getName() + "取錢失敗,餘額不足");
		}
	}
}

class DrawThread extends Thread{
	Account account;
	public DrawThread(Account account) {
		// TODO Auto-generated constructor stub
		this.account=account;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while (account.balance>=100) {
			account.draw(100);
		}
		
	}
}
public class SynchronizeTest {
public static void main(String[] args) {
	Account account=new Account("dandan", 50000);
	DrawThread drawThread1= new DrawThread(account);
	DrawThread drawThread2= new DrawThread(account);
	drawThread1.start();
	drawThread2.start();
 }
}

執行結果:
在這裏插入圖片描述
那麼synchronized的原理是什麼?
原來synchronized關鍵字經過編譯之後, 會在同步塊的前後分別形成monitorenter和monitorexit這兩個字節碼指令,這兩個字節碼都需要一個reference類型的參數(即我們之前說的鎖對象)來指明要鎖定和解鎖的對象。
在執行monitorenter指令時, 首先要嘗試獲取對象的鎖。 如果這個對象沒被鎖定, 或者當前線程已經擁有了那個對象的鎖, 把鎖的計數器加1, 相應的, 在執行monitorexit指令時會將鎖計數器減1,當計數器爲0時, 鎖就被釋放。如果獲取對象鎖失敗, 那當前線程就要阻塞等待, 直到對象鎖被另外一個線程釋放爲止。
爲什麼要用計數器來實現鎖對象?
其實爲了保證不會出現自己把自己鎖死的問題:
對於同步代碼塊來說,即在synchronized塊中,再定義一個synchronized塊
對於同步方法來說,爲了防止自己再調用自己的時候(遞歸調用)時,自己把自己鎖死

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