1. 程式人生 > >執行緒同步 synchronized 同步程式碼塊 同步方法 同步鎖

執行緒同步 synchronized 同步程式碼塊 同步方法 同步鎖

一 同步程式碼塊

1.為了解決併發操作可能造成的異常,java的多執行緒支援引入了同步監視器來解決這個問題,使用同步監視器的通用方法就是同步程式碼塊。其語法如下:

synchronized(obj){

//同步程式碼塊

}

其中obj就是同步監視器,它的含義是:執行緒開始執行同步程式碼塊之前,必須先獲得對同步監視器的鎖定。任何時刻只能有一個執行緒可以獲得對同步監視器的鎖定,當同步程式碼塊執行完成後,該執行緒會釋放對該同步監視器的鎖定。雖然java程式允許使用任何物件作為同步監視器,但 是同步監視器的目的就是為了阻止兩個執行緒對同一個共享資源進行併發訪問,因此通常推薦使用可能被併發訪問的共享資源充當同步監視器。

2.小例子

Account.java


public class Account {
    private String accountNo;
    private double balance;
    public Account(String accountNo,double balance){
        this.accountNo=accountNo;
        this.balance=balance;
    }
 
    public double getBalance() {
        return balance;
    }
 
    public void setBalance(double balance) {
        this.balance = balance;
    }
 
    public String getAccountNo() {
        return accountNo;
    }
 
    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
 
        Account account = (Account) o;
 
        return accountNo.equals(account.accountNo);
 
    }
 
    @Override
    public int hashCode() {
        return accountNo.hashCode();
    }
}

DrawThread.java

public class DrawThread extends Thread {
    private Account account;
    private double drawAmount;
 
    public DrawThread(String name, Account account, double drawAmount) {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }
    public void run(){
        synchronized (account){
            if(account.getBalance()>=drawAmount){
                System.out.println(getName() + "取錢成功,吐出鈔票: " + drawAmount);
                try{
                    Thread.sleep(1);
                }catch(InterruptedException ex){
                    ex.getStackTrace();
                }
                account.setBalance(account.getBalance()-drawAmount);
                System.out.println("\t餘額為:"+account.getBalance());
            }else{
                System.out.println(getName()+"取錢失敗,餘額不足");
            }
        }
    }
}

DrawTest.java

public class DrawTest {
    public static void main(String[] args){
        Account acct=new Account("1234567",1000);
        new DrawThread("甲",acct,800).start();
        new DrawThread("乙",acct,800).start();
    }
}

甲取錢成功,吐出鈔票: 800.0

    餘額為:200.0

乙取錢失敗,餘額不足

3.如果將DrawThread的同步去掉:

public class DrawThread extends Thread {
    private Account account;
    private double drawAmount;
 
    public DrawThread(String name, Account account, double drawAmount) {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }
    public void run(){
//        synchronized (account){
            if(account.getBalance()>=drawAmount){
                System.out.println(getName() + "取錢成功,吐出鈔票: " + drawAmount);
                try{
                    Thread.sleep(1);
                }catch(InterruptedException ex){
                    ex.getStackTrace();
                }
                account.setBalance(account.getBalance()-drawAmount);
                System.out.println("\t餘額為:"+account.getBalance());
            }else{
                System.out.println(getName()+"取錢失敗,餘額不足");
            }
//        }
    }
}

會出現這些情況的結果:

乙取錢成功,吐出鈔票: 800.0

甲取錢成功,吐出鈔票: 800.0

    餘額為:200.0

    餘額為:-600.0

甲取錢成功,吐出鈔票: 800.0

乙取錢成功,吐出鈔票: 800.0

    餘額為:200.0

    餘額為:200.0

程式使用synchronized將run()方法裡的方法體修改成同步程式碼塊,同步監視器就是account物件,這樣的做法符合“加鎖-修改-釋放鎖”的邏輯,這樣就可以保證併發執行緒在任一時刻只有一個執行緒進入修改共享資源的程式碼區。多次執行,結果只有一個。

二 同步方法

1.同步方法就是使用synchronized關鍵字修飾某個方法,這個方法就是同步方法。這個同步方法(非static方法)無須顯式指定同步監視器,同步方法的同步監視器是this,也就是呼叫該方法的物件。通過同步方法可以非常方便的實現執行緒安全的類,執行緒安全的類有如下特徵:

該類的物件可以方便的被多個執行緒安全的訪問;

每個執行緒呼叫該物件的任意方法之後都能得到正確的結果;

每個執行緒呼叫該物件的任意方法之後,該物件狀態依然能保持合理狀態。

2.不可變類總是執行緒安全的,因為它的物件狀態不可改變可變類需要額外的方法來保證其執行緒安全,在Account類中我們只需要把balance的方法變成同步方法即可。

Account.java

public class Account {
    private String accountNo;
    private double balance;
    public Account(String accountNo,double balance){
        this.accountNo=accountNo;
        this.balance=balance;
    }
 
    //因為賬戶餘額不可以隨便更改,所以只為balance提供getter方法
    public double getBalance() {
        return balance;
    }
 
    public String getAccountNo() {
        return accountNo;
    }
 
    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
 
        Account account = (Account) o;
 
        return accountNo.equals(account.accountNo);
 
    }
 
    @Override
    public int hashCode() {
        return accountNo.hashCode();
    }
 
    //提供一個執行緒安全的draw()方法來完成取錢操作
    public synchronized void draw(double drawAmount){
        if(balance>=drawAmount){
            System.out.println(Thread.currentThread().getName()+"取錢成功!吐出鈔票:"+drawAmount);
            try{
                Thread.sleep(1);
            }catch (InterruptedException ex){
                ex.printStackTrace();
            }
            balance-=drawAmount;
            System.out.println("\t餘額為:"+balance);
        }else{
            System.out.println(Thread.currentThread().getName()+"取錢失敗,餘額不足");
        }
    }
}

DrawThread.java

public class DrawThread extends Thread {
    private Account account;
    private double drawAmount;
 
    public DrawThread(String name, Account account, double drawAmount) {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }
    public void run(){
        account.draw(drawAmount);
    }
}

DrawTest.java

public class DrawTest {
    public static void main(String[] args){
        Account acct=new Account("1234567",1000);
        new DrawThread("甲",acct,800).start();
        new DrawThread("乙",acct,800).start();
    }
}

注意,synchronized可以修飾方法,修飾程式碼塊,但是不能修飾構造器、成員變數等。在Account類中定義draw()方法,而不是直接在 run()方法中實現取錢邏輯,這種做法更符合面向物件規則。DDD設計方式,即Domain Driven Design(領域驅動設計),認為每個類都應該是完備的領域物件,Account代表使用者賬戶,就應該提供使用者賬戶的相關方法。通過draw()方法來執行取錢操作,而不是直接將setBalance()方法暴露出來任人操作。

但是,可變類的執行緒安全是以降低程式的執行效率為代價的,不要對執行緒安全類的所有方法都進行同步,只對那些會改變競爭資源(共享資源)的方法進行同步。同時,可變類有兩種執行環境:單執行緒環境和多執行緒環境, 則應該為可變類提供兩種版本,即執行緒安全版本和執行緒不安全版本。如JDK提供的StringBuilder在單執行緒環境下保證更好的效能,StringBuffer可以保證多執行緒安全。

三 釋放同步監視器的鎖定

1.任何執行緒進入同步程式碼塊,同步方法之前,必須先獲得對同步監視器的鎖定,那麼如何釋放對同步監視器的鎖定呢,執行緒會在一下幾種情況下釋放同步監視器:

當前執行緒的同步方法、同步程式碼塊執行結束,當前執行緒即釋放同步監視器;

當前執行緒在同步程式碼塊、同步方法中遇到break,return終止了該程式碼塊、方法的繼續執行;

當前執行緒在同步程式碼塊、同步方法中出現了未處理的Error或Exception,導致了該程式碼塊、方法的異常結束;

當前執行緒執行同步程式碼塊或同步方法時,程式執行了同步監視器物件的wait()方法,則當前執行緒暫停,並釋放同步監視器;

2.以下幾種情況,執行緒不會釋放同步監視器:

執行緒執行同步程式碼塊或同步方法時,程式呼叫Thread.sleep(),Thread.yield()方法來暫停當前執行緒的執行,當前執行緒不會釋放同步監視器;

執行緒執行同步程式碼塊時,其他執行緒呼叫了該執行緒的suspend()方法將該執行緒掛起,該執行緒不會釋放同步監視器,當然,程式應儘量避免使用suspend()和resume()方法來控制執行緒。

四 同步鎖:

1.Java5開始,Java提供了一種功能更加強大的執行緒同步機制——通過顯式定義同步鎖物件來實現同步,這裡的同步鎖由Lock物件充當。

Lock 物件提供了比synchronized方法和synchronized程式碼塊更廣泛的鎖定操作,Lock是控制多個執行緒對共享資源進行訪問的工具。通常, 鎖提供了對共享資源的獨佔訪問,每次只能有一個執行緒對Lock物件加鎖,執行緒開始訪問共享資源之前應該先獲得Lock物件。

某些鎖可能允許對共享資源併發訪問,如ReadWriteLock(讀寫鎖),Lock,ReadWriteLock是Java5提供的兩個根介面,併為 Lock提供了ReentrantLock實現類,為ReadWriteLock提供了ReentrantReadWriteLock實現類。在 Java8中提供了新型的StampLock類,在大多數場景下它可以替代傳統的ReentrantReadWriteLock。 ReentrantReadWriteLock為讀寫操作提供了三種鎖模式:Writing,ReadingOptimistic,Reading。

2.在實現執行緒安全的控制中,比較常用的是ReentrantLock(可重入鎖)。主要的程式碼格式如下:


class X{
    //定義鎖物件
    private final ReentrantLock lock=new ReentrantLock();
    //定義需要保證執行緒安全的方法
    public void m(){
        //加鎖
        lock.lock();
        try{
            //...method body
        }
        //使用finally塊來保證釋放鎖
        finally{
            lock.unlock();
        }
    }
}

將Account.java修改為:


public class Account {
    private final ReentrantLock lock=new ReentrantLock();
    private String accountNo;
    private double balance;
    public Account(String accountNo,double balance){
        this.accountNo=accountNo;
        this.balance=balance;
    }
 
    //因為賬戶餘額不可以隨便更改,所以只為balance提供getter方法
    public double getBalance() {
        return balance;
    }
 
    public String getAccountNo() {
        return accountNo;
    }
 
    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
 
        Account account = (Account) o;
 
        return accountNo.equals(account.accountNo);
 
    }
 
    @Override
    public int hashCode() {
        return accountNo.hashCode();
    }
 
    //提供一個執行緒安全的draw()方法來完成取錢操作
    public void draw(double drawAmount){
        //加鎖
        lock.lock();
        try {
            if (balance >= drawAmount) {
                System.out.println(Thread.currentThread().getName() + "取錢成功!吐出鈔票:" + drawAmount);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                balance -= drawAmount;
                System.out.println("\t餘額為:" + balance);
            } else {
                System.out.println(Thread.currentThread().getName() + "取錢失敗,餘額不足");
            }
        }finally {
            lock.unlock();
        }
    }
}

使用Lock與使用同步程式碼有點相似,只是使用Lock時可以顯式使用Lock物件作為同步鎖,而使用同步方法時系統隱式使用當前物件作為同步監視器。使用 Lock時每個Lock物件對應一個Account物件,一樣可以保證對於同一個Account物件,同一個時刻只能有一個執行緒進入臨界區。Lock提供 了同步方法和同步程式碼塊所沒有的其他功能,包括使用非塊結構的tryLock()方法,以及試圖獲取可中斷鎖的lockInterruptibly()方法,還有獲取超時失效鎖的tryLock(long,TimeUnit)方法。

ReentrantLock可重入鎖的意思是,一個執行緒可以對已被加鎖的ReentrantLock鎖再次加鎖,ReentrantLock物件會維持一個計數器來追蹤lock()方法的巢狀呼叫,執行緒在每次呼叫lock()加鎖後,必須顯式呼叫unlock()來釋放鎖,所以一段被鎖保護的程式碼可以呼叫另一個被相同鎖保護的方法。

五 死鎖

當兩個執行緒相互等待對方釋放同步監視器時就會發生死鎖,Java虛擬機器沒有檢測,也沒有采取措施來處理死鎖情況,所以多執行緒程式設計時應該採取措施避免死鎖出現。一旦出現死鎖,程式既不會發生任何異常,也不會給出任何提示,只是所有執行緒都處於阻塞狀態,無法繼續。

如DeadLock.java


class A{
    public synchronized void foo(B b){
        System.out.println("當前執行緒名為:"+Thread.currentThread().getName()+"進入了A例項的foo()方法");
        try{
            Thread.sleep(200);
        }catch(InterruptedException ex){
            ex.printStackTrace();
        }
        System.out.println("當前執行緒名為:"+Thread.currentThread().getName()+"試圖呼叫B例項的last()方法");
        b.last();
    }
    public synchronized void last(){
        System.out.println("進入了A類的last()方法內部");
    }
}
class B{
    public synchronized void bar(A a){
        System.out.println("當前執行緒名為:"+Thread.currentThread().getName()+"進入了B例項的bar()方法");
        try{
            Thread.sleep(200);
        }catch(InterruptedException ex){
            ex.printStackTrace();
        }
        System.out.println("當前執行緒名為:"+Thread.currentThread().getName()+"試圖呼叫A例項的last()方法");
        a.last();
    }
    public synchronized void last(){
        System.out.println("進入了B類的last()方法內部");
    }
}
public class DeadLock implements Runnable{
    A a =new A();
    B b=new B();
    public void init(){
        Thread.currentThread().setName("主執行緒");
        a.foo(b);
        System.out.println("進入了主執行緒之後...");
    }
    public void run(){
        Thread.currentThread().setName("副執行緒");
        b.bar(a);
        System.out.println("進入了副執行緒之後...");
    }
    public static void main(String[] args){
        DeadLock d1=new DeadLock();
        new Thread(d1).start();
        d1.init();
    }
}

結果:

當前執行緒名為:主執行緒進入了A例項的foo()方法

當前執行緒名為:副執行緒進入了B例項的bar()方法

當前執行緒名為:主執行緒試圖呼叫B例項的last()方法

當前執行緒名為:副執行緒試圖呼叫A例項的last()方法