Java多線程 -- 線程同步

Java多線程 – 線程同步

一、線程安全問題

  在多線程編程中,極容易出現一個問題,即線程安全。舉個栗子~,如果此時火車站有十張餘票,四個售票口,此時四個售票口相當於四個線程,他們一同運轉,賣票,那麼就會出現一個很經典的安全問題。

示例代碼:

class demo{
    public static void main(String[] args) throws InterruptedException {
        Train train = new Train();
        Train train1 = new Train();
        Train train2 = new Train();
        Train train3 = new Train();
        train.start();
        train1.start();
        train2.start();
        train3.start();

    }
}

class Train extends Thread{
    private static int count = 10;
    @Override
    public void run(){
        while (true){
            try{
                Thread.sleep(500);
                boolean flag = sale();
                if(!flag)
                    break;
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }

    public boolean sale(){
        if(count > 0){
            --count;
            System.out.println( Thread.currentThread().getName() + "售出一張,剩餘:"+count);
            return true;
        }else{
            return false;
        }
    }
}

大家可以運行一下以上的程序,會發現有的時候售出的票不只是10張,這正是線程調度的不確定性,

二、同步代碼塊

  之所以出現以上的情況,是因爲run()方法的方法體不具有同步安全性,當多個線程同步修改同一變量的時候容易出現以上情況,爲了解決這個尷尬的問題,Java的多線程支持引入了同步監視器來解決這個問題,使用同步監視器的通用方法就是同步代碼塊。

synchronized(obj){
	//此處代碼就是同步代碼塊
}

  注意:任何時刻都只能擁有一個線程獲得對同步監視器的鎖定,當同步代碼塊執行完成之後,該線程就會釋放對同步監視器的鎖定。

class demo{
    public static void main(String[] args) throws InterruptedException {
        Train train = new Train();
        Train train1 = new Train();
        Train train2 = new Train();
        Train train3 = new Train();
        train.start();
        train1.start();
        train2.start();
        train3.start();

    }
}

class Train extends Thread{
    private static int count = 10;
    @Override
    public void run(){
		//獲取類鎖
        synchronized(Train.class){
            while (true){
                try{
                    Thread.sleep(500);
                    boolean flag = sale();
                    if(!flag)
                        break;
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }

    public boolean sale(){
        if(count > 0){
            --count;
            System.out.println( Thread.currentThread().getName() + "售出一張,剩餘:"+count);
            return true;
        }else{
            return false;
        }
    }
}

三、同步方法

  與同步代碼塊相對應,Java的多線程安全支持,還提供了同步方法,同步方法就是使用synchronized修飾的實例方法(非static方法),則該方法稱爲同步方法,對於synchronized修飾的實例方法,無需顯式指定同步監視器,同步方法的同步監視器是this,也就是調用該方法的對象。
  通過同步方法可以非常方便地實現線程安全的類。
  線程安全的類有以下的特徵:
   1. 該類的對象可以被多個線程安全的訪問。
   2. 每個線程調用該對象的任意方法之後都將得到正確結果。
   3. 每個線程調用該對象的任意方法之後,該對象狀態依然保持合理狀態。

注意:synchronized可以修飾方法和代碼塊,但不能修飾構造器和成員變量等

四、釋放同步監視器的鎖定

  任何線程進入同步代碼塊或同步方法前,都必須先獲得對同步監視器的鎖定,那麼何時會釋放對同步監視器的鎖定呢?程序無法顯示釋放對同步監視器的鎖定,而是在以下這些情況釋放對同步監視器的鎖定:
  1. 當前線程的同步方法、同步代碼執行結束,當前線程即釋放同步監視器。
  2. 當前線程在同步代碼塊、同步方法中遇到了breakreturn終止了該代碼塊、該方法的繼續執行,當前線程將會釋放同步監視器。
  3. 當前線程在同步代碼塊、同步方法之中出現了未處理的errorException,導致了該代碼塊,該方法異常結束的時候,當前線程將會釋放同步監視器。
  4. 當前線程執行同步代碼塊或同步方法時,程序執行了同步監視器對象的wait()方法,則當前線程暫停,並且釋放同步監視器。

  以下情況,線程不會釋放同步監視器:
  1. 線程執行同步代碼塊或者同步方法的時候,程序調用Thread.sleep()Thread.yield()方法來暫停當前線程的執行,但是當前線程並不會釋放同步監視器。
  2. 線程執行同步代碼塊的時候,其他的線程調用該線程的suspend()方法將其掛起,該線程不會釋放同步監視器,當然,需要注意的是:程序中儘量少使用suspend()resume()方法來控制線程。

五、同步鎖

  從Java 5開始,Java提供了一種功能更強大的線程同步機制 – 通過顯示定義同步鎖對象來實現同步。
  Lock提供了比synchronized方法和代碼塊更廣泛的鎖定操作,Lock允許實現更加靈活的結構,可以具有差別很大的屬性,並且支持多個相關的Condition對象。
  Lock是控制多個線程對共享資源進行訪問的工具。通常,所提供了對共享資源的獨佔訪問,每次都只能有一個線程對Lock對象枷鎖,線程開始訪問共享資源之前,應該先獲得Lock對象。
  某些所可能允許對共享資源併發操作,例如說:ReadWriteLock(讀寫鎖),LockReadWriteLock是Java 5提供的兩個根接口,且爲Lock提供了ReentrantLock(可重入鎖)實現類,爲ReadWreiteLock提供了ReentrantReadWriteLock實現類。
  Java 8新增了新型的StampedLock類,在大多數場景中他可以替代傳統的ReentrantreadWriteLockReentrantreadWriteLock爲讀寫操作提供了三種鎖模式:WritingReadingOptimisiticReading
  在實現線程安全的控制之中,較爲常用的是ReentrantLock(可重入鎖)。使用該Lock對象可以顯式地添加所和釋放鎖。

import java.util.concurrent.locks.ReentrantLock;

class demo{
    //定義鎖對象
    private final ReentrantLock lock = new ReentrantLock();
    //定義需要保證線程安全的方法
    public void m(){
        //加鎖
        lock.lock();
        try{
            //需要保證線程安全的代碼
        }
        //使用finally塊來釋放鎖
        finally {
            //解鎖
            lock.unlock();
        }
    }
}

  使用ReentrantLock對象進行同步,加鎖和釋放鎖均出現在不同的額作用範圍之內,通常建議使用finally塊來確保在必要的時候釋放鎖,通常使用ReentrantLock對象。

六、避免死鎖

  當兩個線程相互等待對方釋放同步監視器就會發生死鎖,其中Java虛擬機中沒有監測,也沒有采取措施處理死鎖情況,所以多線程編程中應當精良避免死鎖出現,一旦出現死鎖,整個程序既不會發生任何異常,也不會有任何提示,只是所有線程處於阻塞狀態,無法繼續。

  死鎖不應該在程序中出現,編寫時應該儘量避免死鎖。
  1. 避免多次鎖定,儘量避免同一個線程對多個同步監視器進行鎖定
  2. 具有相同的加鎖順序,如果多個線程對多個同步監視器進行鎖定,則應該報這個它們以相同的順序請求加鎖。
  3. 採用定時所,程序調用Lock對象的truLock()方法加鎖時,可以指定timeunit參數,當超過指定時間就會自動釋放對Lock的鎖定。
  4. 死鎖檢測,這是依靠算法進行實現的死鎖預防措施,主要針對那些不可能實現按序加鎖,也不可能使用定時鎖的場景。

//會產生死鎖的代碼:

class A{
    public synchronized void foo(B b){
        System.out.println("線程名:" + Thread.currentThread().getName() + "進入A實例的foo方法");
        try{
            Thread.sleep(200);
        }
        catch (InterruptedException e){
            e.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 foo(A a){
        System.out.println("線程名:" + Thread.currentThread().getName() + "進入A實例的foo方法");
        try{
            Thread.sleep(200);
        }
        catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("線程名:" + Thread.currentThread().getName() + "企圖調用B實例的Last方法");
        a.last();
    }

    public synchronized void last(){
        System.out.println("進入B類的last方法內部");
    }
}

public class demo implements Runnable{
    A a = new A();
    B b = new B();
    public void init(){
        Thread.currentThread().setName("主線程");
        a.foo(b);
        System.out.println("進入主線程之後");
    }

    @Override
    public void run(){
        Thread.currentThread().setName("副線程");
        b.foo(a);
        System.out.println("進入副線程");
    }

    public static void main(String[] args) {
        demo d = new demo();
        new Thread(d).start();
        d.init();
    }

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