線程同步的作用
在多線程中,當兩個及以上線程併發訪問同個資源時,儘管有控制線程的方法,但由於線程調度具有不確定性,所以極容易導致錯誤,這時就需要線程同步機制來解決此問題。
實現線程同步——同步監視器
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加鎖的代碼中能執行另一段加鎖的代碼。