線程同步基礎

多個執行線程共享一個資源的情景,是最多見的併發編程情景之一。在併發應用中經常遇到這樣的情景:多個線程讀或者寫相同的數據,或者訪問相同的文件或者數據庫鏈接。java

爲了防止這些共享資源可能出現的錯誤或數據不一致,咱們必須實現一些機制來防止這些錯誤的發生。數據庫

爲了解決這些問題,人們引入了臨界區概念。臨界區是一個可用以訪問共享資源的代碼塊,這個代碼塊在同一時間內只容許一個線程執行。編程

爲了幫助編程人員實行這個臨界區,java提供了同步機制。當一個線程試圖訪問一個臨界區時,它將使用一種同步機制來查看是否是已經有其餘線程進入臨界區。若是沒有其餘線程進入臨界區,它就能夠進入臨界區;若是已經有線程進入了臨界區,它就被同步機制掛起,直到進入的線程離開這個臨界區。若是在等待進入臨界區的線程不止一個,JVM會選擇其中一個,其他的將繼續等待。安全

java語言提供了兩種基本同步機制:數據結構

1.synchronized關鍵字機制多線程

2.Lock接口及其實現機制併發

1、synchronized關鍵字機制dom

1.同步方法
即有synchronized關鍵字修飾的方法。
因爲java的每一個對象都有一個內置鎖,當用此關鍵字修飾方法時,內置鎖會保護整個方法。在調用該方法前,須要得到內置鎖,不然就處於阻塞狀態。
代碼如:
public synchronized void save(){}
對於實例的同步方法,使用this即當前實例對象。
對於靜態的同步方法,使用當前類的字節碼對象。
注: synchronized關鍵字也能夠修飾靜態方法,此時若是調用該靜態方法,將會鎖住整個類。

2.同步代碼塊
即有synchronized關鍵字修飾的語句塊。
被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步
代碼如:
synchronized(object){}
Java中任意的對象均可以做爲一個監聽器(monitor),監聽器能夠被上鎖和解鎖,在線程同步中稱爲同步鎖,且同步鎖在同一時間只能被一個線程所持有。上面的obj對象就是一個同步鎖,分析一下上面代碼的執行過程:
1).一個線程執行到synchronized代碼塊,首先檢查obj,若是obj爲空,拋出NullPointerExpression異常;
2).若是obj不爲空,線程嘗試給監聽器上鎖,若是監聽器已經被鎖,則線程不能獲取到鎖,線程就被阻塞;
3).若是監聽器沒被鎖,則線程將監聽器上鎖,而且持有該鎖,而後執行代碼塊;
4).代碼塊正常執行結束或者非正常結束,監聽器都將自動解鎖;

線程同步鎖對多個線程必須是互斥的,即多個線程須要使用同一個同步鎖。代碼中obj對象被多個線程共享,可以實現同步。
注:同步是一種高開銷的操做,所以應該儘可能減小同步的內容。
一般沒有必要同步整個方法,使用synchronized代碼塊同步關鍵代碼便可。

二者的區別主要體如今同步鎖上面。對於實例的同步方法,由於只能使用this來做爲同步鎖,若是一個類中須要使用到多個鎖,爲了不鎖的衝突,必然須要使用不一樣的對象,這時候同步方法不能知足需求,只能使用同步代碼塊(同步代碼塊能夠傳入任意對象);或者多個類中須要使用到同一個鎖,這時候多個類的實例this顯然是不一樣的,也只能使用同步代碼塊,傳入同一個對象。ide

2、Lock接口及其實現機制高併發

Lock接口及實現類提供了更多的好處:

1.支持更靈活的同步代碼塊結構。使用synchronized關鍵字時,只能在同一個synchronized塊結構中獲取和釋放控制。Lock接口容許實現更復雜的臨界區結構,即控制的獲取和釋放不出如今同一個塊結構中。

2.相比synchronized關鍵字,Lock接口提供了更多的功能。其中一個新功能是tryLock()方法的實現。這個方法試圖獲取鎖,若是鎖已被其餘線程獲取,它將返回false,並繼續往下執行代碼。使用synchronized關鍵字時,若是線程A試圖執行一個同步代碼塊,而線程B已在執行這個同步代碼塊,則線程A就會被掛起直到線程B運行完這個同步代碼塊。使用鎖tryLock()方法,經過返回值將得知是否有其餘線程正在使用這個鎖保護的代碼塊。

3.Lock接口容許分離讀和寫操做,容許多個讀線程和只有一個寫線程。

4.相比synchronized關鍵字,Lock接口具備更好的性能。

 

鎖的公平性
ReentrantLock和ReentrantReadWriteLock類的構造器都含有一個布爾參數fair,它容許你控制着兩個類的行爲。默認fair值爲false,它稱爲非公平模式。
在非公平模式下,當有不少線程在等待鎖時,鎖將選擇它們中的一個來訪問臨界區,這個選擇是沒有任何約束的。
若是fair值爲true,則稱爲公平模式。
在公平模式下,當有不少線程在等待鎖時,鎖將選擇它們中的一個來訪問臨界區,並且選擇的是等待時間最長的。
這兩種模式只適用於lock()和unlock()方法。而Lock接口的tryLock()方法沒有將線程置於休眠,fair屬性並不影響這個方法。

 

經常使用的線程類:

1)、ReentrantLock類:是一個可重入的互斥鎖,重入鎖是一種遞歸無阻塞的同步機制。ReentrantLock由最近成功獲取鎖,尚未釋放的線程所擁有,當鎖被另外一個線程擁有時,調用lock的線程能夠成功獲取鎖。若是鎖已經被當前線程擁有,當前線程會當即返回。
a、防止重複執行(忽略重複觸發)
ReentrantLock lock = new ReentrantLock();
public void getObject(){
    //若是已經被lock,則當即返回false不會等待,達到忽略操做的效果
    if (lock.tryLock()) {  
        try {  
            //操做  
        } finally {  
            lock.unlock();  
        }  
    }
}    

b、同步執行,相似synchronized
ReentrantLock lock = new ReentrantLock(); //參數默認false,不公平鎖  
//ReentrantLock lock = new ReentrantLock(true); //公平鎖  
public void getObject(){
    //若是被其它資源鎖定,會在此等待鎖釋放,達到暫停的效果  
    lock.lock();
    try {  
        //操做  
    } finally {  
        lock.unlock();  
    }
}
c、嘗試等待執行
ReentrantLock lock = new ReentrantLock(true); //公平鎖  
public void getObject(){
    try {  
        if (lock.tryLock(5, TimeUnit.SECONDS)) {      
            //若是已經被lock,嘗試等待5s,看是否能夠得到鎖,若是5s後仍然沒法得到鎖則返回false繼續執行  
            try {  
                //操做  
            } finally {  
                lock.unlock();  
            }  
        }  
    } catch (InterruptedException e) {  
        e.printStackTrace(); //當前線程被中斷時(interrupt),會拋InterruptedException                   
    }
}

d、可中斷鎖的同步執行
ReentrantLock lock = new ReentrantLock(true); //公平鎖  
public void getObject(){
    lock.lockInterruptibly();  
    try {  
        //操做  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    } finally {  
        lock.unlock();  
    }
}

ReadWriteLock類:讀寫鎖,維護了一對相關的鎖,一個用於只讀操做,一個用於寫入操做。只要沒有writer,讀取鎖能夠由多個reader線程同時保持。寫入鎖是獨佔的。
ReentrantReadWriteLock類:可重入讀寫鎖,會使用兩把鎖來解決問題,一個讀鎖,一個寫鎖。
線程進入讀鎖的前提條件:
1).沒有其餘線程的寫鎖,
2).沒有寫請求或者有寫請求,但調用線程和持有鎖的線程是同一個
線程進入寫鎖的前提條件:
1).沒有其餘線程的讀鎖
2).沒有其餘線程的寫鎖
ReentrantReadWriteLock和ReentrantLock的區別,它和後者都是單獨的實現,彼此之間沒有繼承或實現的關係:
(a).重入方面其內部的WriteLock能夠獲取ReadLock,可是反過來ReadLock想要得到WriteLock則永遠都不要想。
(b).WriteLock能夠降級爲ReadLock,順序是:先得到WriteLock再得到ReadLock,而後釋放WriteLock,這時候線程將保持Readlock的持有。反過來ReadLock想要升級爲WriteLock則不可能。
(c).ReadLock能夠被多個線程持有而且在做用時排斥任何的WriteLock,而WriteLock則是徹底的互斥。這一特性最爲重要,由於對於高讀取頻率而相對較低寫入的數據結構,使用此類鎖同步機制則能夠提升併發量。
(d).不論是ReadLock仍是WriteLock都支持Interrupt,語義與ReentrantLock一致。
(e).WriteLock支持Condition而且與ReentrantLock語義一致,而ReadLock則不能使用Condition,不然拋出UnsupportedOperationException異常。
讀寫鎖的例子:
import java.util.Random;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
    public static void main(String[] args) {
        Queue3 q3 = new Queue3();
        for(int i=0;i<3;i++){
            new Thread(){
                public void run(){
                    while(true){
                        q3.get();                        
                    }
                }
            }.start();
        }
        for(int i=0;i<3;i++){        
            new Thread(){
                public void run(){
                    while(true){
                        q3.put(new Random().nextInt(10000));
                    }
                }            
            }.start();    
        }
    }
}
 
class Queue3{
    private Object data = null;//共享數據,只能有一個線程能寫該數據,但能夠有多個線程同時讀該數據。
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    public void get(){
        rwl.readLock().lock();//上讀鎖,其餘線程只能讀不能寫
        System.out.println(Thread.currentThread().getName() + " be ready to read data!");
        try {
            Thread.sleep((long)(Math.random()*1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "have read data :" + data);        
        rwl.readLock().unlock(); //釋放讀鎖,最好放在finnaly裏面
    }
   
    public void put(Object data){
        rwl.writeLock().lock();//上寫鎖,不容許其餘線程讀也不容許寫
        System.out.println(Thread.currentThread().getName() + " be ready to write data!");                    
        try {
            Thread.sleep((long)(Math.random()*1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.data = data;        
        System.out.println(Thread.currentThread().getName() + " have write data: " + data);                    
        rwl.writeLock().unlock();//釋放寫鎖    
    }
}

在鎖中使用多條件

一個鎖可能關聯一個或多個條件,這些條件經過Condition接口聲明。目的是容許線程獲取鎖而且查看等待的某一個條件是否知足,若是不知足就掛起直到某個線程喚醒它們。Condition接口提供了掛起線程和喚起線程的機制。

Condition是在java1.5中才出現的,它用來替代傳統的Object的wait()、notify()實現線程間的協做,相比使用Object的wait()、notify(),使用Condition1的await()、signal()這種方式實現線程間協做更加安全和高效。Condition是個接口,基本的方法就是await()和signal()方法;Condition依賴於Lock接口,生成一個Condition的基本代碼是lock.newCondition()調用Condition的await()和signal()方法,都必須在lock保護以內,就是說必須在lock.lock()和lock.unlock之間纔可使用Conditon中的await()對應Object的wait();Condition中的signal()對應Object的notify();Condition中的signalAll()對應Object的notifyAll()。代碼示例:import java.util.concurrent.locks.Condition;  import java.util.concurrent.locks.ReentrantLock;      public class Main {      public static void main(String[] args) {          final ReentrantLock reentrantLock = new ReentrantLock();          final Condition condition = reentrantLock.newCondition();            new Thread(new Runnable() {              @Override              public void run() {                  reentrantLock.lock();                  System.out.println(Thread.currentThread().getName() + "拿到鎖了");                  System.out.println(Thread.currentThread().getName() + "等待信號");                  try {                      condition.await();                  } catch (InterruptedException e) {                      e.printStackTrace();                  }                    System.out.println(Thread.currentThread().getName() + "拿到信號");                    reentrantLock.unlock();              }          }, "線程1").start();            new Thread(new Runnable() {              @Override              public void run() {                  reentrantLock.lock();                  System.out.println(Thread.currentThread().getName() + "拿到鎖了");                    try {                      Thread.sleep(3000);                  } catch (InterruptedException e) {                      e.printStackTrace();                  }                    System.out.println(Thread.currentThread().getName() + "發出信號");                  condition.signalAll();                    reentrantLock.unlock();              }          }, "線程2").start();      }  } 

相關文章
相關標籤/搜索