線程安全 加鎖機制

線程安全就是多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其餘線程不能進行訪問直到該線程讀取完,其餘線程纔可以使用。不會出現數據不一致或者數據污染。
線程不安全就是不提供數據訪問保護,有可能出現多個線程前後更改數據形成所獲得的數據是髒數據。html

摘自 http://blog.csdn.net/vking_wang/article/details/9952063java

 

Java線程 加鎖機制:synchronized、Lock、Condition算法

一、synchronized

把代碼塊聲明爲 synchronized,有兩個重要後果,一般是指該代碼具備 原子性(atomicity)和 可見性(visibility)緩存

1.1 原子性

原子性意味着個時刻,只有一個線程可以執行一段代碼,這段代碼經過一個monitor object保護。從而防止多個線程在更新共享狀態時相互衝突。安全

1.2 可見性

可見性則更爲微妙,它要對付內存緩存和編譯器優化的各類反常行爲。它必須確保釋放鎖以前對共享數據作出的更改對於隨後得到該鎖的另外一個線程是可見的 。數據結構

做用:若是沒有同步機制提供的這種可見性保證,線程看到的共享變量多是修改前的值或不一致的值,這將引起許多嚴重問題。多線程

原理:當對象獲取鎖時,它首先使本身的高速緩存無效,這樣就能夠保證直接從主內存中裝入變量。 一樣,在對象釋放鎖以前,它會刷新其高速緩存,強制使已作的任何更改都出如今主內存中。 這樣,會保證在同一個鎖上同步的兩個線程看到在 synchronized 塊內修改的變量的相同值。併發

 

通常來講,線程以某種沒必要讓其餘線程當即能夠看到的方式(無論這些線程在寄存器中、在處理器特定的緩存中,仍是經過指令重排或者其餘編譯器優化),不受緩存變量值的約束,可是若是開發人員使用了同步,那麼運行庫將確保某一線程對變量所作的更新先於對現有synchronized 塊所進行的更新,當進入由同一監控器(lock)保護的另外一個synchronized 塊時,將馬上能夠看到這些對變量所作的更新。相似的規則也存在於volatile變量上。框架

——volatile只保證可見性,不保證原子性!dom

1.3 什麼時候要同步?

可見性同步的基本規則是在如下狀況中必須同步: 

  1. 讀取上一次多是由另外一個線程寫入的變量 
  2. 寫入下一次可能由另外一個線程讀取的變量

一致性同步:當修改多個相關值時,您想要其它線程原子地看到這組更改—— 要麼看到所有更改,要麼什麼也看不到。

這適用於相關數據項(如粒子的位置和速率)和元數據項(如鏈表中包含的數據值和列表自身中的數據項的鏈)。

 

在某些狀況中,您沒必要用同步來將數據從一個線程傳遞到另外一個,由於 JVM 已經隱含地爲您執行同步。這些狀況包括:

  1. 由靜態初始化器(在靜態字段上或 static{} 塊中的初始化器)
  2. 初始化數據時 
  3. 訪問 final 字段時 ——final對象呢?
  4. 在建立線程以前建立對象時 
  5. 線程能夠看見它將要處理的對象時

 

1.4 synchronize的限制

synchronized是不錯,但它並不完美。它有一些功能性的限制:

  1. 它沒法中斷一個正在等候得到鎖的線程;
  2. 也沒法經過投票獲得鎖,若是不想等下去,也就無法獲得鎖;
  3. 同步還要求鎖的釋放只能在與得到鎖所在的堆棧幀相同的堆棧幀中進行,多數狀況下,這沒問題(並且與異常處理交互得很好),可是,確實存在一些非塊結構的鎖定更合適的狀況。

 

二、ReentrantLock

Java.util.concurrent.lock 中的Lock 框架是鎖定的一個抽象,它容許把鎖定的實現做爲 Java 類,而不是做爲語言的特性來實現。這就爲Lock 的多種實現留下了空間,各類實現可能有不一樣的調度算法、性能特性或者鎖定語義。

ReentrantLock 類實現了Lock ,它擁有與synchronized 相同的併發性和內存語義,可是添加了相似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用狀況下更佳的性能。(換句話說,當許多線程都想訪問共享資源時,JVM 能夠花更少的時候來調度線程,把更多時間用在執行線程上。)

class Outputter1 {    
    private Lock lock = new ReentrantLock();// 鎖對象    
  
    public void output(String name) {           
        lock.lock();      // 獲得鎖    
  
        try {    
            for(int i = 0; i < name.length(); i++) {    
                System.out.print(name.charAt(i));    
            }    
        } finally {    
            lock.unlock();// 釋放鎖    
        }    
    }    
}    

區別:

須要注意的是,用sychronized修飾的方法或者語句塊在代碼執行完以後鎖自動釋放,而是用Lock須要咱們手動釋放鎖,因此爲了保證鎖最終被釋放(發生異常狀況),要把互斥區放在try內,釋放鎖放在finally內!!

 

 

三、讀寫鎖ReadWriteLock

上例中展現的是和synchronized相同的功能,那Lock的優點在哪裏?

例如一個類對其內部共享數據data提供了get()和set()方法,若是用synchronized,則代碼以下:

class syncData {        
    private int data;// 共享數據        
    public synchronized void set(int data) {    
        System.out.println(Thread.currentThread().getName() + "準備寫入數據");    
        try {    
            Thread.sleep(20);    
        } catch (InterruptedException e) {    
            e.printStackTrace();    
        }    
        this.data = data;    
        System.out.println(Thread.currentThread().getName() + "寫入" + this.data);    
    }       
    public synchronized  void get() {    
        System.out.println(Thread.currentThread().getName() + "準備讀取數據");    
        try {    
            Thread.sleep(20);    
        } catch (InterruptedException e) {    
            e.printStackTrace();    
        }    
        System.out.println(Thread.currentThread().getName() + "讀取" + this.data);    
    }    
}    

而後寫個測試類來用多個線程分別讀寫這個共享數據:

 

public static void main(String[] args) {    
//        final Data data = new Data();    
          final syncData data = new syncData();    
//        final RwLockData data = new RwLockData();    
          
        //寫入  
        for (int i = 0; i < 3; i++) {    
            Thread t = new Thread(new Runnable() {    
              @Override  
              public void run() {    
                    for (int j = 0; j < 5; j++) {    
                        data.set(new Random().nextInt(30));    
                    }    
                }    
            });  
            t.setName("Thread-W" + i);  
            t.start();  
        }    
        //讀取  
        for (int i = 0; i < 3; i++) {    
            Thread t = new Thread(new Runnable() {    
              @Override  
              public void run() {    
                    for (int j = 0; j < 5; j++) {    
                        data.get();    
                    }    
                }    
            });    
            t.setName("Thread-R" + i);  
            t.start();  
        }    
    }    

運行結果:

Thread-W0準備寫入數據  
Thread-W0寫入0  
Thread-W0準備寫入數據  
Thread-W0寫入1  
Thread-R1準備讀取數據  
Thread-R1讀取1  
Thread-R1準備讀取數據  
Thread-R1讀取1  
Thread-R1準備讀取數據  
Thread-R1讀取1  
Thread-R1準備讀取數據  
Thread-R1讀取1  
Thread-R1準備讀取數據  
Thread-R1讀取1  
Thread-R2準備讀取數據  
Thread-R2讀取1  
Thread-R2準備讀取數據  
Thread-R2讀取1  
Thread-R2準備讀取數據  
Thread-R2讀取1  
Thread-R2準備讀取數據  
Thread-R2讀取1  
Thread-R2準備讀取數據  
Thread-R2讀取1  
Thread-R0準備讀取數據 //R0和R2能夠同時讀取,不該該互斥!  
Thread-R0讀取1  
Thread-R0準備讀取數據  
Thread-R0讀取1  
Thread-R0準備讀取數據  
Thread-R0讀取1  
Thread-R0準備讀取數據  
Thread-R0讀取1  
Thread-R0準備讀取數據  
Thread-R0讀取1  
Thread-W1準備寫入數據  
Thread-W1寫入18  
Thread-W1準備寫入數據  
Thread-W1寫入16  
Thread-W1準備寫入數據  
Thread-W1寫入19  
Thread-W1準備寫入數據  
Thread-W1寫入21  
Thread-W1準備寫入數據  
Thread-W1寫入4  
Thread-W2準備寫入數據  
Thread-W2寫入10  
Thread-W2準備寫入數據  
Thread-W2寫入4  
Thread-W2準備寫入數據  
Thread-W2寫入1  
Thread-W2準備寫入數據  
Thread-W2寫入14  
Thread-W2準備寫入數據  
Thread-W2寫入2  
Thread-W0準備寫入數據  
Thread-W0寫入4  
Thread-W0準備寫入數據  
Thread-W0寫入20  
Thread-W0準備寫入數據  
Thread-W0寫入29  

如今一切都看起來很好!各個線程互不干擾!等等。。讀取線程和寫入線程互不干擾是正常的,可是兩個讀取線程是否須要互不干擾??

對!讀取線程不該該互斥!

咱們能夠用讀寫鎖ReadWriteLock實現:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class Data {        
    private int data;// 共享數據    
    private ReadWriteLock rwl = new ReentrantReadWriteLock();       
    public void set(int data) {    
        rwl.writeLock().lock();// 取到寫鎖    
        try {    
            System.out.println(Thread.currentThread().getName() + "準備寫入數據");    
            try {    
                Thread.sleep(20);    
            } catch (InterruptedException e) {    
                e.printStackTrace();    
            }    
            this.data = data;    
            System.out.println(Thread.currentThread().getName() + "寫入" + this.data);    
        } finally {    
            rwl.writeLock().unlock();// 釋放寫鎖    
        }    
    }       
  
    public void get() {    
        rwl.readLock().lock();// 取到讀鎖    
        try {    
            System.out.println(Thread.currentThread().getName() + "準備讀取數據");    
            try {    
                Thread.sleep(20);    
            } catch (InterruptedException e) {    
                e.printStackTrace();    
            }    
            System.out.println(Thread.currentThread().getName() + "讀取" + this.data);    
        } finally {    
            rwl.readLock().unlock();// 釋放讀鎖    
        }    
    }    
}    

測試結果:

Thread-W1準備寫入數據  
Thread-W1寫入9  
Thread-W1準備寫入數據  
Thread-W1寫入24  
Thread-W1準備寫入數據  
Thread-W1寫入12  
Thread-W0準備寫入數據  
Thread-W0寫入22  
Thread-W0準備寫入數據  
Thread-W0寫入15  
Thread-W0準備寫入數據  
Thread-W0寫入6  
Thread-W0準備寫入數據  
Thread-W0寫入13  
Thread-W0準備寫入數據  
Thread-W0寫入0  
Thread-W2準備寫入數據  
Thread-W2寫入23  
Thread-W2準備寫入數據  
Thread-W2寫入24  
Thread-W2準備寫入數據  
Thread-W2寫入24  
Thread-W2準備寫入數據  
Thread-W2寫入17  
Thread-W2準備寫入數據  
Thread-W2寫入11  
Thread-R2準備讀取數據  
Thread-R1準備讀取數據  
Thread-R0準備讀取數據  
Thread-R0讀取11  
Thread-R1讀取11  
Thread-R2讀取11  
Thread-W1準備寫入數據  
Thread-W1寫入18  
Thread-W1準備寫入數據  
Thread-W1寫入1  
Thread-R0準備讀取數據  
Thread-R2準備讀取數據  
Thread-R1準備讀取數據  
Thread-R2讀取1  
Thread-R2準備讀取數據  
Thread-R1讀取1  
Thread-R0讀取1  
Thread-R1準備讀取數據  
Thread-R0準備讀取數據  
Thread-R0讀取1  
Thread-R2讀取1  
Thread-R2準備讀取數據  
Thread-R1讀取1  
Thread-R0準備讀取數據  
Thread-R1準備讀取數據  
Thread-R0讀取1  
Thread-R2讀取1  
Thread-R1讀取1  
Thread-R0準備讀取數據  
Thread-R1準備讀取數據  
Thread-R2準備讀取數據  
Thread-R1讀取1  
Thread-R2讀取1  
Thread-R0讀取1  

 

與互斥鎖定相比,讀-寫鎖定容許對共享數據進行更高級別的併發訪問。雖然一次只有一個線程(writer 線程)能夠修改共享數據,但在許多狀況下,任何數量的線程能夠同時讀取共享數據(reader 線程)

 

從理論上講,與互斥鎖定相比,使用讀-寫鎖定所容許的併發性加強將帶來更大的性能提升。

在實踐中,只有在多處理器上而且只在訪問模式適用於共享數據時,才能徹底實現併發性加強。——例如,某個最初用數據填充而且以後不常常對其進行修改的 collection,由於常常對其進行搜索(好比搜索某種目錄),因此這樣的 collection 是使用讀-寫鎖定的理想候選者。

 

四、線程間通訊Condition

Condition能夠替代傳統的線程間通訊,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll()。

——爲何方法名不直接叫wait()/notify()/nofityAll()?由於Object的這幾個方法是final的,不可重寫!

 

傳統線程的通訊方式,Condition均可以實現。

注意,Condition是被綁定到Lock上的,要建立一個Lock的Condition必須用newCondition()方法。

 

Condition的強大之處在於它能夠爲多個線程間創建不一樣的Condition

看JDK文檔中的一個例子:假定有一個綁定的緩衝區,它支持 put 和 take 方法。若是試圖在空的緩衝區上執行take 操做,則在某一個項變得可用以前,線程將一直阻塞;若是試圖在滿的緩衝區上執行 put 操做,則在有空間變得可用以前,線程將一直阻塞。咱們喜歡在單獨的等待 set 中保存put 線程和take 線程,這樣就能夠在緩衝區中的項或空間變得可用時利用最佳規劃,一次只通知一個線程。可使用兩個Condition 實例來作到這一點。

——其實就是java.util.concurrent.ArrayBlockingQueue的功能

class BoundedBuffer {  
  final Lock lock = new ReentrantLock();          //鎖對象  
  final Condition notFull  = lock.newCondition(); //寫線程鎖  
  final Condition notEmpty = lock.newCondition(); //讀線程鎖  
  
  final Object[] items = new Object[100];//緩存隊列  
  int putptr;  //寫索引  
  int takeptr; //讀索引  
  int count;   //隊列中數據數目  
  
  //
  public void put(Object x) throws InterruptedException {  
    lock.lock(); //鎖定  
    try {  
      // 若是隊列滿,則阻塞<寫線程>  
      while (count == items.length) {  
        notFull.await();   
      }  
      // 寫入隊列,並更新寫索引  
      items[putptr] = x;   
      if (++putptr == items.length) putptr = 0;   
      ++count;  
  
      // 喚醒<讀線程>  
      notEmpty.signal();   
    } finally {   
      lock.unlock();//解除鎖定   
    }   
  }  
  
  //
  public Object take() throws InterruptedException {   
    lock.lock(); //鎖定   
    try {  
      // 若是隊列空,則阻塞<讀線程>  
      while (count == 0) {  
         notEmpty.await();  
      }  
  
      //讀取隊列,並更新讀索引  
      Object x = items[takeptr];   
      if (++takeptr == items.length) takeptr = 0;  
      --count;  
  
      // 喚醒<寫線程>  
      notFull.signal();   
      return x;   
    } finally {   
      lock.unlock();//解除鎖定   
    }   
  }   

優勢:假設緩存隊列中已經存滿,那麼阻塞的確定是寫線程,喚醒的確定是讀線程,相反,阻塞的確定是讀線程,喚醒的確定是寫線程。那麼假設只有一個Condition會有什麼效果呢?緩存隊列中已經存滿,這個Lock不知道喚醒的是讀線程仍是寫線程了,若是喚醒的是讀線程,皆大歡喜,若是喚醒的是寫線程,那麼線程剛被喚醒,又被阻塞了,這時又去喚醒,這樣就浪費了不少時間。

相關文章
相關標籤/搜索