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元,即就是線程安全問題,下面我們分析爲什麼會出現這種狀況,可能是下面的這種情況(但是這種情況並不唯一,因爲餘額的變動並不是原子操作):
那麼出現線程安全的原因是什麼?
- 存在兩個或者兩個以上的線程對象,而且線程之間共享着一個資源。
- 有多個語句操作了共享資源。
爲了解決這個問題,java的多線程引入synchronized同步代碼塊和同步方法來解決這個問題,下面我們來看一下這兩種方法:
synchronized有兩種使用方式:
方式一:同步代碼塊
格式:
synchronized(鎖對象){
需要被同步的代碼…
}
注意:多線程操作的鎖對象必須是唯一共享的。否則無效。也就是說鎖對象是static的/常量,其實最簡單的就是使用一個常量作爲鎖對象
這種方式比較簡單,我們就不寫代碼的例子了。。。
方式二:同步函數
同步函數:同步函數就是使用synchronized修飾一個函數。
注意:
- 如果是一個非靜態的同步函數的鎖對象是this對象,如果是靜態的同步函數的鎖對象是當前函數所屬的類的字節碼文件(class對象)。因此,同步方法只能保證多個線程同時執行同一個對象的同步代碼段
- 同步函數的鎖對象是固定的,不能自己來指定的。
下面我們用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塊
對於同步方法來說,爲了防止自己再調用自己的時候(遞歸調用)時,自己把自己鎖死