1. 程式人生 > >多執行緒2-synchronized、lock

多執行緒2-synchronized、lock

1、什麼時候會出現執行緒安全問題?

  在多執行緒程式設計中,可能出現多個執行緒同時訪問同一個資源,可以是:變數、物件、檔案、資料庫表等。此時就存在一個問題:

  每個執行緒執行過程是不可控的,可能導致最終結果與實際期望結果不一致或者直接導致程式出錯。

  如我們在第一篇部落格中出現的count--的問題。這是一個典型的非執行緒安全問題。這一被多個執行緒訪問的資源count變數被稱為:臨界資源(共享資源)。但當多個執行緒執行一個方法,方法內部的區域性變數並不是臨界資源,因為方法在棧上執行,java棧是執行緒私有的,因此不會產生執行緒安全問題


 2、如何解決執行緒安全問題?

  基本上所有的併發模式解決執行緒安全問題,都採用序列化臨界資源的方案,即同一時刻,只能有一個執行緒訪問臨界資源,也稱作同步互斥訪問。在訪問臨界資源的程式碼前加鎖,當訪問完臨界資源後釋放鎖,讓其他執行緒繼續訪問。java中提供了兩種方式來實現同步互斥訪問:synchronized和lock


 3、synchronized關鍵字詳解

  synchronized的三種應用方式包括:

  a:修飾例項方法,作用於當前例項加鎖,進入同步程式碼前要獲得當前例項的鎖

  b:修飾靜態方法,作用於當前類物件加鎖,進入同步程式碼前要獲得當前類物件的鎖

  c:修飾程式碼塊,指定加鎖物件,對給定物件加鎖,進入同步程式碼庫前要獲得給定物件的鎖

  程式碼演示:例項物件鎖就是synchronized修飾例項物件中的例項方法,例項方法不包括靜態方法

public class ThreadDemo2 {
    public static void main(String[] args) throws InterruptedException {
        Test1 test1 = new Test1();
        Thread t1 = new Thread(test1);
        Thread t2 = new Thread(test1);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}
class Test1 implements Runnable{ //臨界資源 static int i=0; /** * synchronized 修飾例項方法 */ public synchronized void increase(){ i++; } @Override public void run() { for(int j=0;j<100000;j++){ increase(); System.out.println(i); } } }

  當synchronized修飾increase()方法後,i值的操作便是執行緒安全的。輸出結果是200000,如果不加synchronized,結果可能小於這個值。當一個執行緒正在訪問一個物件的synchronized例項方法,其他執行緒就不能訪問該物件其他的synchronized方法。一個物件只有一把鎖。當一個執行緒獲取該物件的鎖後,其他執行緒無法獲取該物件的鎖。但可以訪問該物件的非synchronized方法。有一種特殊的情況,當兩個執行緒訪問的例項物件不同,則鎖是不同的,當這兩個執行緒操作資料並非共享的,執行緒安全是有保障的,但當操作資料是共享的,那麼執行緒安全無法保證,演示 如下程式碼:

public class ThreadDemo2 {
    public static void main(String[] args) throws InterruptedException {
        Test1 test1 = new Test1();
        Thread t1 = new Thread(test1);
        Test1 test2 = new Test1();
        Thread t2 = new Thread(test2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}
class Test1 implements Runnable{
    //臨界資源
    static int i=0;

    /**
     * synchronized 修飾例項方法 鎖物件是例項物件
     */
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<100000;j++){
            increase();
            System.out.println(i);
        }
    }
}

  此demo執行結果也會出現值小於20000,我們建立了兩個Test1的例項,啟動兩個不同的執行緒對共享變數i進行操作,雖然我們隊increase方法添加了同步鎖,但卻new了兩個不同例項,此時就存在兩個例項物件鎖,因此t1和t2都會進入各自的物件鎖,因此無法保證執行緒安全。解決這種錯誤地方式就是將synchronized作用於靜態的increase方法,這樣的話,物件鎖就是當前類物件,無論建立多少個例項物件,類物件只有一個,物件鎖是唯一的。

public class ThreadDemo2 {
    public static void main(String[] args) throws InterruptedException {
        Test1 test1 = new Test1();
        Thread t1 = new Thread(test1);
        Test1 test2 = new Test1();
        Thread t2 = new Thread(test2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}
class Test1 implements Runnable{
    //臨界資源
    static int i=0;

    /**
     * synchronized 修飾靜態方法,鎖物件是類的class物件
     */
    public static synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<100000;j++){
            increase();
            System.out.println(i);
        }
    }
}

  synchronized作用於靜態方法時,鎖是當前類的class物件鎖。靜態成員是類成員。通過class物件鎖可以控制靜態成員的併發操作。需要注意的是:如果一個執行緒呼叫一個例項物件的非static synchronized方法,而另一個執行緒呼叫這個例項物件所屬類的靜態synchronized方法,是允許的,不會發生互斥現象。因為訪問靜態synchronized方法佔用的鎖是當前類的class物件,而非訪問靜態synchronized方法佔用的當前例項物件鎖。鎖物件不同,但我們需要意識到這種情況下可能發生執行緒安全問題,因為操作了共享資源。

  一些情況下,我們編寫的方法體過大,同時存在一些比較耗時的操作。而需要同步的程式碼只有一小部分,我們可以通過synchronized程式碼塊來對需要同步的程式碼進行包裹:

public class ThreadDemo2 implements Runnable{
    static ThreadDemo2 test1 = new ThreadDemo2();
    //臨界資源
    static int i=0;
    @Override
    public void run() {
        //省略其他耗時操作....
        //使用同步程式碼塊對變數i進行同步操作,鎖物件為test1
        synchronized(test1){
            for(int j=0;j<1000000;j++){
                i++;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(test1);
        Thread t2 = new Thread(test1);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

  當前情況下,將synchronized作用於一個給定的例項物件test1,即當前例項物件就是鎖物件,除了test1作為物件外,我們還可以使用this物件(synchronized(this)代表當前例項)或當前類的class物件作為鎖(synchronized(ThreadDemo2.class))。


 synchronized的一些特性:

  在java中synchronized是基於原子性的內部鎖機制,是可重入的,在一個執行緒呼叫synchronized方法的同時在其方法體內部呼叫該物件另一個synchronized方法,也就是說一個執行緒得到一個物件鎖後再次請求該物件鎖,是允許的,這就是synchronized的可重入性

  執行緒中斷與synchronized:對於synchronized來說,如果一個執行緒在等待鎖,結果只有兩種。要麼它獲得鎖繼續執行,要麼儲存等待,即使呼叫中斷執行緒的方法,也不會有效。

  等待喚醒機制與synchronized:這裡主要指notify/notify/wait方法。使用這三個方法必須在synchronized程式碼塊或synchronized方法中,否則就會丟擲IllegalMonitorStateException異常。因為呼叫這幾個方法必須拿到當前物件的monitor物件。monitor存在於引用指標中,而synchronized關鍵字可以獲取monitor。與sleep不同的是wait方法呼叫完後,執行緒將被暫停,但wait方法會釋放掉當前持有的鎖。直到執行緒呼叫notify/notifyAll方法後才繼續執行。sleep方法只讓執行緒休眠並不釋放鎖。同時notify/notifyAll方法呼叫後,不會馬上釋放鎖,而是在相應的synchronized程式碼塊或synchronized方法執行結束後才自動釋放鎖。


 4、Lock詳解

  在上面synchronized的詳解中,我們可以瞭解到當一個程式碼塊被synchronized修飾,一個執行緒獲取了對應的鎖並執行該程式碼塊。其他執行緒只能一直等待,等待獲取鎖的執行緒釋放鎖。這裡獲取鎖的執行緒是釋放鎖的可能有兩種:

  1)獲取鎖的執行緒執行完該程式碼塊,執行緒釋放鎖

  2)執行緒執行發生異常,jvm會讓執行緒自動釋放鎖

  如果這個獲取鎖的執行緒由於等待IO或其他原因被阻塞且沒有釋放鎖,其他執行緒便只能等待。這種情況下synchronized就有了一些缺陷。通過Lock我們可以彌補這些缺陷。

  Lock的使用:

  在lock介面中,有四個方法來獲取鎖。lock()、tryLock()、tryLock(long time,TimeUnit unit) 和lockInterruptibly()。使用unLock()來釋放鎖。由於lock不會主動釋放鎖,發生異常時,不會自動釋放鎖。一般使用Lock必須在try{}catch{}塊中進行。並將釋放鎖的操作放在finally中,保證鎖一定被釋放,防止死鎖的發生。

  Lock():獲取鎖,如果鎖已被其他執行緒獲取,則進行等待

Lock lock = ...;
lock.lock();
try{
    //處理任務
}catch(Exception ex){

}finally{
  //釋放鎖  
  lock.unlock();      
}

  tryLock():嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗則返回false。該方法無論如何都會立即返回,拿不到鎖時不會一直等待。

  tryLock(long time,TimeUnit unit):與tryLock()方法類似,不同的是該方法拿不到鎖時會等待一定時間,在時間期限內還拿不到鎖就返回false。

Lock lock = ...;
if(lock.tryLock()){
    try{
        //處理任務
    }catch(Exception ex){
    }finally{
        //釋放鎖
       lock.unlock();            
    }  
}else{
    //如果不能獲取鎖,執行其他任務
}

  lockInterruptibly():獲取鎖時,如果執行緒正在等待獲取鎖,那該執行緒能響應中斷,即中斷等待狀態。當兩個執行緒同時通過lock.lockInterruptibly()想獲取某個鎖時,若A執行緒獲取了鎖,而B執行緒只能等待。那麼對B執行緒呼叫threadB.interrupt()方法能夠中斷B執行緒的等待過程。該方法的宣告中丟擲了異常,使用時必須放在try塊中或在呼叫方法外宣告丟擲InterruptedException。

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

  注意:持有鎖的執行緒,是不會被Interrupt()方法中斷的,它只能中斷阻塞過程中的執行緒。當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,才可以響應中斷。


 5、Lock介面的實現類ReentranLock

  ReentrantLock是唯一實現了Lock介面的類,並提供了更多的方法。

  Lock的正確使用

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    //此處宣告lock為全域性變數
    private Lock lock = new ReentrantLock();
    public static void main(String[] args) {
        final Test test = new Test();
        new Thread() {
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
        new Thread() {
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }
    
    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println("當前是執行緒:"+thread.getName()+"獲得了鎖");
            for (int i = 0; i < 5; i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            
        } finally {
            System.out.println("執行緒:"+thread.getName()+"釋放了鎖");
            lock.unlock();
        }
    }
}

此處注意,特意在宣告Lock的時候註釋了是全域性變數。因為當lock在方法裡建立成區域性變數的時候。每個執行緒執行到lock.lock()獲取到的是不同的鎖。不會發生衝突。一般使用時將Lock宣告為全域性變數即可。

在這段程式碼裡的insert()方法使用tryLock()方法,可以知道執行緒有沒有獲取到鎖並輸出結果。

public void insert(Thread thread) {
        if(lock.tryLock()) {
            try {
                System.out.println("當前是執行緒:"+thread.getName()+"獲得了鎖");
                for (int i = 0; i < 5; i++) {
                    arrayList.add(i);
                }
            } catch (Exception e) {
                
            } finally {
                System.out.println("執行緒:"+thread.getName()+"釋放了鎖");
                lock.unlock();
            }
        }else {
            System.out.println("執行緒:"+thread.getName()+"獲取鎖失敗");
        }
    }

6、ReadWriteLock

  ReadWriteLock定義了兩個方法,一個用來獲取讀鎖,一個用來獲取寫鎖。將檔案的讀寫操作分開,分成兩個鎖來分配給執行緒。使得多個執行緒可以同時進行讀操作。

  實現類:ReentrantReadWriteLock。主要兩個方法readLock()和writeLock()用來獲取讀鎖和寫鎖。

  一個例項:多個執行緒同時進行讀操作。使用synchronized

public class Test {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    public static void main(String[] args) {
        final Test test = new Test();
        new Thread() {
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();;
        new Thread() {
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();;
    }
    
    public synchronized void get(Thread thread) {
        long start = System.currentTimeMillis();
        while(System.currentTimeMillis() - start <= 1) {
            System.out.println("執行緒:"+thread.getName()+"正在進行讀操作");
        }
        System.out.println("執行緒:"+thread.getName()+"讀操作完畢");
    }
}

這樣的輸出結果可以發現,一個時間段內只有一個執行緒在執行讀操作。一個執行緒執行完讀操作,另一個執行緒才有機會執行。

改為讀寫鎖,實現多個執行緒同時讀操作

public  void get(Thread thread) {
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println("執行緒:"+thread.getName()+"正在進行讀操作");
            }
            System.out.println("執行緒:"+thread.getName()+"讀操作完畢");
        } catch (Exception e) {
            // TODO: handle exception
        } finally {
            rwl.readLock().unlock();
        }
    }

這段程式碼的輸出結果可以看出,同時兩個執行緒都在執行讀操作。這樣的話效率大大提升。不過要注意,如果一個執行緒佔用了讀鎖,此時其他執行緒要申請寫鎖,那申請寫鎖的執行緒會一直等待釋放讀鎖。如果一個執行緒佔用了寫鎖,此時其他執行緒要申請寫鎖或讀鎖,則申請的執行緒會一直等待釋放寫鎖。從效能上看,競爭資源不激烈,lock跟synchronized效能差不多,當競爭資源激烈時,lock的效能要遠遠優於synchronized。


Synchronized和lock區別:
  1、Synchronized是java語言內建的特性,而lock是一個介面
  2、Synchronized不需要使用者手動釋放鎖,當synchronized方法或程式碼塊執行完後,自動釋放鎖,而lock需要使用者手動釋放鎖,如果沒有手動釋放,可能產生死鎖
  3、Synchronized修飾時,等待的執行緒會一直等待不能響應中斷,lock可以讓等待鎖的執行緒響應中斷。
  4、Lock可以知道有沒有成功獲取鎖(tryLock方法),而synchronized不可以
  5、Lock可以提高多個執行緒進行讀操作的效率

 7、鎖的概念:

  可重入鎖:從互斥鎖的設計上來說,當一個執行緒試圖操作一個由其他執行緒持有的物件鎖的臨界資源時,將會處於阻塞狀態,但當一個執行緒再次請求自己持有物件鎖的臨界資源時,這種情況屬於重入鎖,請求將會成功。該分配機制是基於執行緒而非基於方法的呼叫。

class MyClass{
    synchronized void method1(){
        method2();
    }
    synchronized void method2(){
    } 

  synchronized是可重入鎖。當一個執行緒執行method1時,已經獲取到了物件的鎖。呼叫method2就無需重新申請鎖。不具備重入性時,執行緒持有該物件的鎖,又去申請該物件的鎖。將會使執行緒一直等待永遠獲取不到鎖。synchronized和Lock都具備可重入性。

  可中斷鎖:可以響應中斷的鎖。即可以使在等待中的執行緒自己中斷或者在別的執行緒中中斷它。Lock是可響應中斷的,synchronized不是。

  公平鎖:儘量以請求鎖的順序來獲取鎖。多個執行緒同時等待一個鎖,當此鎖被釋放時,等待最久的執行緒優先獲得該鎖。

  非公平鎖:無法保證鎖的獲取是按照請求鎖的順序進行的。這樣可能導致某個或一些執行緒永遠獲取不到鎖,synchronized就是非公平鎖,它無法保證等待的執行緒獲取鎖的順序。對於ReentranLock和ReentrantReadWriteLock,它預設是公平鎖,也可設定為非公平鎖

  讀寫鎖:該鎖將一個資源的訪問分成了兩個鎖,讀鎖和寫鎖。保證了多個執行緒之間的讀操作不發生衝突。ReadWriteLock是讀寫鎖,它是一個介面。ReentrantReadWriteLock實現了這個介面。可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。