Java并發編程之鎖機制

一、JAVA鎖實現

鎖是用來控制多個線程訪問共享資源的方式,JDK提供三種方式的鎖實現:(1)Synchronized 關鍵字(2)Lock(3)原子操作類(無鎖)

1.Synchronized

synchronized是基于JVM內置鎖實現,基于進入與退出Monitor對象實現方法同步和代碼塊同步,監視器鎖的實現依賴底層操作系統的Mutex lock(互斥鎖)實現

代碼塊同步是使用monitorenter 和monitorexit指令實現的。

private static Object lock = new Object();
public void test(){ //
    synchronized(lock){//編譯后,插入monitorenter指令到同步代碼塊開始位置
            
    }//編譯后,插入monitorexit指令到同步代碼塊結束位置
}

任何對象都有一個monitor與之關聯,獲取對象的鎖即是獲取對象所對應的monitor的所有權,synchronized用的鎖是存在JAVA對象頭里的

JAVA對象頭包括 MarkWord,類型指針,數據長度三部分;MarkWord存儲對象的鎖,hashcode等信息

JDK1.6 對synchronized 進行優化,引入了偏向鎖和輕量級鎖,減少獲取鎖和釋放鎖帶來的性能消耗。

鎖級別從低到高依次是:無鎖狀態- 偏向鎖-輕量級鎖-重量級鎖

  • 無鎖:表示沒有對資源進行鎖定,所有的線程都能訪問并修改同一個資源,但同時只有一個線程能修改成功,其它線程循環重試

  • 偏向鎖:偏向鎖是指一段同步代碼一直被一個線程所訪問,那么該線程會自動獲取鎖,降低獲取鎖的代價(第一次進入同步代碼,用CAS把線程 ID 設置到對象的 Mark Word 頭)

  • 輕量級鎖:指當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。

  • 重量級鎖:升級為重量級鎖時,鎖標志的狀態值變為“10”,此時Mark Word中存儲的是指向重量級鎖的指針,此時等待鎖的線程都會進入阻塞狀態

例子:

public class SynchronizedTest {

    private static Object lock = new Object();
    public synchronized static void staticMethod(){  //實際上是對該類對象加鎖,俗稱“類鎖”
            ...
    }
    public synchronized void method(){ //實際上是對調用方法的對象加鎖,俗稱“對象鎖”
            ...
    }
    public void method1(){ 
        synchronized (SynchronizedTest.class){ //對SynchronizedTest.class對象加鎖
                ...
        }
    }
    public void method2(){
        synchronized (lock){ //對lock對象加鎖
                ...
        }
    }
    public static void main(String[] args) {
    }
}

同一個對象在兩個線程中分別訪問該對象的兩個同步方法 會互斥

不同對象在兩個線程中調用同一個同步方法 不會互斥

用類直接在兩個線程中調用兩個不同的同步靜態方法 會互斥

一個對象在兩個線程中分別調用一個靜態同步方法和一個非靜態同步方法 不會互斥

2. Lock

JDK1.5 引入Lock接口(以及相關實現類)(java.util.concurrent.locks包中)實現鎖功能,需要顯示的獲取和釋放鎖。Lock擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種同步特性。

class X {     
  private final ReentrantLock lock = new ReentrantLock();     
  // ...        
  public void m() {       
    lock.lock();  // block until condition holds   不要寫到try里,防止獲取鎖(自定義鎖的實現)發生異常,導致鎖無故釋放    
    try {         
      // ... method body       
    } finally {   //確保最終能釋放鎖      
      lock.unlock();      
    }     
  }   
}

讀寫鎖:

public class ReadWriteTest{
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Object data = null;
    public void read(){
        readWriteLock.readLock().lock();
        try{
            System.out.println(Thread.currentThread().getName() + " ready to read data" );
            Thread.sleep(new Random().nextInt(1000));
            System.out.println(Thread.currentThread().getName() + ":" + data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
    public void write(Object data){
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " ready to write" + data);
            Thread.sleep(new Random().nextInt(2000));
            this.data = data;
            System.out.println(Thread.currentThread().getName() + ":" + this.data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }
    public static void main(String[] args) {
        ReadWriteTest readWriteTest = new ReadWriteTest();
        for(int i = 0; i < 5; i++){
            new Thread(() -> {
                int j= 10;
                while (j > 0) {
                    readWriteTest.read();j--;
                }
            }, "Reader"+i).start();
        }
        for(int i = 0; i < 2; i++){
            new Thread(() -> {
                int j= 10;
                while (j > 0) {
                    readWriteTest.write(new Random().nextInt(10000));
                    j--;
                }
            }, "Writer"+i).start();
        }
    }
}
  • 互斥關系:寫鎖與寫鎖是互斥的,寫鎖與讀鎖也是互斥的,只有讀鎖和讀鎖共享的
  • 可重入性:如果當前線程已持有寫鎖,可再次持有寫鎖或者讀鎖
  • 不允許鎖升級:如果當前線程持有讀鎖,不能直接申請寫鎖

實現原理

利用隊列同步器AbstractQueuedSynchronizer(AQS)實現,AQS當中的同步等待隊列也稱CLH隊列,CLH隊列是Craig、Landin、Hagersten三人發明的一種基于雙向鏈表數據結構的隊列,是FIFO先入先出線程等待隊列,Java中的CLH隊列是原CLH隊列的一個變種,線程由原自旋機制改為阻塞機制。

  • 狀態:volatile int state(代表共享資源狀態), 設置成volatile類型,以保證其修改的可見性
  • 同步隊列:等待對象的集合,以雙向鏈表的形式實現
  • CAS:同步隊列的操作采用CAS
lock.png
AQS.png
lock-share.png
lock-share-release.png

Node 屬于AQS的內部類,成員:除了前置和后置節點還有節點對應的線程(thread),節點的狀態(waitStatus),nextWaiter(主要用于條件變量)

自定義同步類

自定義同步器在實現時只需要實現共享資源state的獲取與釋放方式即可,至于具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在底層實現好了。自定義同步器實現時主要實現以下幾種方法:

  • isHeldExclusively():該線程是否正在獨占資源。只有用到condition才需要去實現它。
  • tryAcquire(int):獨占方式。嘗試獲取資源,成功則返回true,失敗則返回false。
  • tryRelease(int):獨占方式。嘗試釋放資源,成功則返回true,失敗則返回false。
  • tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩余可用資源;正數表示成功,且有剩余資源。
  • tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放后允許喚醒后續等待結點返回true,否則返回false。

同步類在實現時一般都將自定義同步器(sync)定義為內部類,供自己使用;而同步類自己(Mutex)則實現某個接口,對外服務。當然,接口的實現要直接依賴sync,它們在語義上也存在某種對應關系!而sync只用實現資源state的獲取-釋放方式tryAcquire-tryRelelase,至于線程的排隊、等待、喚醒等,上層的AQS都已經實現好了,我們不用關心。內置同步類ReentrantLock/ReentrantReadWriteLock/CountDownLatch/Semaphore/ 都是基于AQS實現的。

  • 自定義同步類,同一時刻最多有兩個線程在運行
public class TwinsLock implements Lock {

    private final Sync sync = new Sync(2);

    static class Sync extends AbstractQueuedSynchronizer {
        public Sync(int state){setState(state);}

        protected final int tryAcquireShared(int unused) {
            for (;;) {
                int c = getState();
                int newState = c - unused;
                if(newState < 0 || compareAndSetState(c, newState)){
                    return newState;
                }
            }
        }
        protected final boolean tryReleaseShared(int unused) {
            for (;;) {
                int c = getState();
                if (compareAndSetState(c, c + unused))
                    return true;
            }
        }
    }
    @Override
    public void lock() {
        sync.acquireShared(1);
    }
    @Override
    public void unlock() {
        sync.releaseShared(1);
    }
    //其它接口省略
    public static void main(String[] args) {
        TwinsLock twinsLock = new TwinsLock();
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                twinsLock.lock();
                try{
                    String name = Thread.currentThread().getName();
                    System.out.println(name+" start ...");
                    Thread.sleep(3000);
                    System.out.println(name+" end ...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    twinsLock.unlock();
                }
            }, "Thread-"+i).start();
        }
    }
}

3.原子操作類

JDK 1.5 新增原子操作類(java.util.concurrent.atomic包中),是CAS非阻塞算法的實現方式,相對于synchronized/lock 這種阻塞算法,它性能更好。

原子操作類主要解決變量并發訪問的同步問題。Atomic包里的類基本都是使用Unsafe實現。

(1)原子更新基本類型

  • AtomicBoolean:原子更新布爾類型, 常用方法:getAndSet(boolean), compareAndSet(boolean,boolean)
  • AtomicInteger:原子更新整形, 常用方法:addAndGet(int), getAndIncrement(), compareAndSet(int,int)
  • AtomicLong:原子更新長整形, 常用方法:addAndGet(long), getAndIncrement(), compareAndSet(long,long)
public class BasicType extends Thread{

    public static final AtomicInteger aInt = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            aInt.incrementAndGet(); //并發操作是原子性,無須加鎖
        }
        System.out.println(Thread.currentThread().getName()+":"+ aInt);
    }
    public static void main(String[] args) throws InterruptedException{
        Thread a = new BasicType();
        a.start();
        Thread b = new BasicType();
        b.start();
        a.join();
        b.join();
        System.out.println("main exit");
    }
}

(2)原子更新數組

  • AtomicIntegerArray:原子更新整型數組里的元素。

  • AtomicLongArray:原子更新長整型數組里的元素。

  • AtomicReferenceArray:原子更新引用類型數組里的元素.

int array[] = {1,2,3};
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(array);
int result = atomicIntegerArray.addAndGet(1, 100);
System.out.println(atomicIntegerArray.get(1));//輸出結果102
System.out.println(array[1]);//輸出結果2

AtomicIntegerArray(array) 會將當前數組復制一份,所以對AtomicIntegerArray 數組元素修改,不會影響傳入的數組

(3)原子更新引用

AtomicReference:原子更新引用類型。
AtomicReferenceFieldUpdater:原子更新引用類型里的字段。
AtomicMarkableReference:原子更新帶有標記位的引用類型

class Person {
    private String name;
        ...
}
public static void main(String[]args){
    Person old =  new Person("A");
  AtomicReference<Person> reference = new AtomicReference(old);
  reference.getAndSet(new Person("B")); //更新應用指向,不會改變舊引用值
  System.out.println(reference.get().getName());//輸出B
  System.out.println(old.getName());//輸出 A
}
class Person {
    volatile public String name; //更新屬性必須是volatile
    public Person(String name){this.name = name;}
}
public static void main(String[]args){
    //所在類需有訪問name屬性的權限
    AtomicReferenceFieldUpdater referenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Person.class,String.class,"name");
    Person initOld = new Person("C");
    referenceFieldUpdater.set(initOld,"D");
    System.out.println(referenceFieldUpdater.get(initOld)); //輸出D
    System.out.println(initOld.getName());//輸出D
}

String val = "hello";
AtomicMarkableReference<String> atomicMarkableReference = new AtomicMarkableReference(val, false);
atomicMarkableReference.compareAndSet(val,"hello world",false, true);
atomicMarkableReference.compareAndSet("hello world","hello",true, true);

(4)原子更新屬性類

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater:原子更新長整型字段的更新器。
  • AtomicStampedReference:原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用于原子的更新數據和數據的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。
class Person {
    volatile public int age; //更新屬性必須是volatile
    public Person(int age){this.age = age;}
}
public static void main(String[]args){
        AtomicIntegerFieldUpdater  atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Person.class, "age");
    Person initOld = new Person(10);
    atomicIntegerFieldUpdater.getAndSet(initOld,50);
    System.out.println(atomicIntegerFieldUpdater.get(initOld));//輸出50
    System.out.println(initOld.getAge());//輸出50
}
AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference(val,1);
int stamp =  atomicStampedReference.getStamp();
atomicStampedReference.compareAndSet(val, "hello world", stamp, stamp+1);
stamp = atomicStampedReference.getStamp();
atomicStampedReference.compareAndSet("hello world", "hello", stamp, stamp+1);

AtomicMarkableReference 與 AtomicStampedReference 一樣也可以解決 ABA的問題,兩者唯一的區別是,AtomicStampedReference 是通過 int 類型的版本號,而 AtomicMarkableReference 是通過 boolean 型的標識來判斷數據是否有更改過。可以理解成AtomicMarkableReference是AtomicStampedReference的簡化版

二、JAVA鎖的種類

  • 死鎖: 線程A 持有鎖A 并試圖獲取鎖B,線程B 持有鎖B 并試圖獲取鎖A

  • 樂觀鎖&悲觀鎖: 樂觀鎖認為自己使用數據時,不會有別的線程修改數據,典型算法是CAS算法,JAVA原子操作類就是通過CAS實現的

    悲觀鎖總是認為自己使用數據時候一定有別的線程修改數據,java中采用synchronized關鍵字和Lock的實現類都是悲觀鎖。

  • 自旋鎖&適應性自旋鎖:當同步資源的鎖定時間很短,采用自旋鎖避免了線程切換的開銷,提高效率。缺點:如果鎖被占用的時間很長,自旋的線程會浪費CPU資源。自旋的次數上限默認是10次,-XX:PreBlockSpin; JDK1.6 引入適應性自旋鎖,自旋的時間(次數)可以動態變化. 典型實現是JAVA原子操作類

  • 無鎖&偏向鎖&輕量級鎖&重量級鎖: JDK1.6 針對synchronized重量級鎖的優化,提出偏向鎖和輕量級鎖。

  • 公平鎖和非公平鎖:非公平鎖是指多個線程獲取鎖的順序并不是按照申請鎖的順序,有可能后申請的線程比先申請的線程優先獲得鎖,有可能造成優先級反轉或者饑餓現象。ReentrantLock 默認鎖是非公平鎖,非公平鎖效率更高,對于Synchronized而言,也是一種非公平鎖。

  • 可重入鎖&非可重入鎖:Java中ReentrantLock和synchronized都是可重入鎖,可重入鎖的一個優點是可一定程度避免死鎖。

  • 獨享鎖(也叫排它鎖或者互斥鎖)&共享鎖:獨享鎖的實現有Synchronized 和 ReentrantLock, ReentrantReadWriteLock 其讀鎖是共享鎖,其寫是獨享鎖。

  • 讀寫鎖:java中實現ReentrantReadWriteLock

  • 分段鎖:分段鎖其實是一種鎖的設計,并不是具體的一種鎖,對于ConcurrentHashMap而言,其并發的實現就是通過分段鎖的形式來實現高效的并發操作

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容