併發編程之Java鎖

1、重入鎖

鎖做爲併發共享數據,保證一致性的工具,在JAVA平臺有多種實現(如 synchronized(重量級) 和 ReentrantLock(輕量級)等等 ) 。這些已經寫好提供的鎖爲咱們開發提供了便利。java

重入鎖,也叫作遞歸鎖,指的是同一線程 外層函數得到鎖以後 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。mysql

在JAVA環境下 ReentrantLock 和synchronized 都是 可重入鎖算法

synchronized:sql

public class Test implements Runnable {
    public  synchronized void get() {
        System.out.println("name:" + Thread.currentThread().getName() + " get();");
    }

    public synchronized  void set() {
        System.out.println("name:" + Thread.currentThread().getName() + " set();");
        get();
    }

    @Override
    public void run() {
        set();
    }

    public static void main(String[] args) {
        Test ss = new Test();
        new Thread(ss).start();
        new Thread(ss).start();
        new Thread(ss).start();
        new Thread(ss).start();
    }
}複製代碼

ReentrantLock:數據庫

public class Test02 extends Thread {
    ReentrantLock lock = new ReentrantLock();
    public void get() {
        try{
            lock.lock();
            System.out.println(Thread.currentThread().getId());
        }catch (Exception e){

        }finally {
            lock.unlock();
        }
    }
    public void set() {
        try{
            lock.lock();
            System.out.println(Thread.currentThread().getId());
            get();
        }catch (Exception e){

        }finally {
            lock.unlock();
        }
    }
    @Override
    public void run() {
        set();
    }
    public static void main(String[] args) {
        Test ss = new Test();
        new Thread(ss).start();
        new Thread(ss).start();
        new Thread(ss).start();
    }

}複製代碼

正常狀況下在函數自行完畢後會釋放鎖,以上兩種方式均在已得到鎖的函數中從新調用了須要鎖的函數,若是沒有可重入性,那麼調用新的須要鎖的函數時,將會形成死鎖。可重用鎖在遞歸調用中很是重要。編程

2、讀寫鎖

程序中涉及到對一些共享資源的讀和寫操做,且寫操做沒有讀操做那麼頻繁。在沒有寫操做的時候,兩個線程同時讀一個資源沒有任何問題,因此應該容許多個線程能在同時讀取共享資源。安全

可是若是有一個線程想去寫這些共享資源,就不該該再有其它線程對該資源進行讀或寫(也就是說:讀-讀能共存,讀-寫不能共存,寫-寫不能共存)。數據結構

這就須要一個讀/寫鎖來解決這個問題。Java5在java.util.concurrent包中已經包含了讀寫鎖。併發

public class Cache {
    static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();

    // 獲取一個key對應的value
    public static final Object get(String key) {
        
        try {
            r.lock();
            System.out.println("正在作讀的操做,key:" + key + " 開始");
            Thread.sleep(100);
            Object object = map.get(key);
            System.out.println("正在作讀的操做,key:" + key + " 結束");
            System.out.println();
            return object;
        } catch (InterruptedException e) {

        } finally {
            r.unlock();
        }
        return key;
    }

    // 設置key對應的value,並返回舊有的value
    public static final Object put(String key, Object value) {
        
        try {
            w.lock();
            System.out.println("正在作寫的操做,key:" + key + ",value:" + value + "開始.");
            Thread.sleep(100);
            Object object = map.put(key, value);
            System.out.println("正在作寫的操做,key:" + key + ",value:" + value + "結束.");
            System.out.println();
            return object;
        } catch (InterruptedException e) {

        } finally {
            w.unlock();
        }
        return value;
    }

    // 清空全部的內容
    public static final void clear() {
        try {
            w.lock();
            map.clear();
        } finally {
            w.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {

            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    Cache.put(i + "", i + "");
                }

            }
        }).start();
        new Thread(new Runnable() {

            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    Cache.get(i + "");
                }

            }
        }).start();
    }
}複製代碼

3、悲觀鎖、樂觀鎖

一、樂觀鎖

老是認爲不會產生併發問題,每次去取數據的時候總認爲不會有其餘線程對數據進行修改,所以不會上鎖,可是在更新時會判斷其餘線程在這以前有沒有對數據進行修改,通常會使用版本號機制或CAS操做實現。ide

version方式:通常是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,不然重試更新操做,直到更新成功。

核心SQL語句:

update table set x=x+1, version=version+1 where id=#{id} and version=#{version};

CAS操做方式:即compare and swap 或者 compare and set,涉及到三個操做數,數據所在的內存值,預期值,新值。當須要更新時,判斷當前內存值與以前取到的值是否相等,若相等,則用新值更新,若失敗則重試,通常狀況下是一個自旋操做,即不斷的重試。

二、悲觀鎖

老是假設最壞的狀況,每次取數據時都認爲其餘線程會修改,因此都會加鎖(讀鎖、寫鎖、行鎖等),當其餘線程想要訪問數據時,都須要阻塞掛起。能夠依靠數據庫實現,如行鎖、讀鎖和寫鎖等,都是在操做以前加鎖,在Java中,synchronized的思想也是悲觀鎖。

4、原子類

java.util.concurrent.atomic包:原子類的小工具包,支持在單個變量上解除鎖的線程安全編程

原子變量類至關於一種泛化的 volatile 變量,可以支持原子的和有條件的讀-改-寫操做。

AtomicInteger 表示一個int類型的值,並提供了 get 和 set 方法,這些 Volatile 類型的int變量在讀取和寫入上有着相同的內存語義。它還提供了一個原子的 compareAndSet 方法(若是該方法成功執行,那麼將實現與讀取/寫入一個 volatile 變量相同的內存效果),以及原子的添加、遞增和遞減等方法。AtomicInteger 表面上很是像一個擴展的 Counter 類,但在發生競爭的狀況下能提供更高的可伸縮性,由於它直接利用了硬件對併發的支持。

爲何會有原子類CAS:Compare and Swap,即比較再交換。

jdk5增長了併發包java.util.concurrent.*,其下面的類使用CAS算法實現了區別於synchronouse同步鎖的一種樂觀鎖。相似於mysql中的version字段的樂觀鎖

若是同一個變量要被多個線程訪問,則可使用該包中的類

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong
  • AtomicReference

5、CAS無鎖模式

CAS:Compare and Swap,即比較再交換。jdk5增長了併發包java.util.concurrent.*,其下面的類使用CAS算法實現了區別於synchronouse同步鎖的一種樂觀鎖。JDK 5以前Java語言是靠synchronized關鍵字保證同步的,這是一種獨佔鎖,也是是悲觀鎖。

一、CAS算法理解

  • (1)與鎖相比,使用比較交換(下文簡稱CAS)會使程序看起來更加複雜一些。但因爲其非阻塞性,它對死鎖問題天生免疫,而且,線程間的相互影響也遠遠比基於鎖的方式要小。更爲重要的是,使用無鎖的方式徹底沒有鎖競爭帶來的系統開銷,也沒有線程間頻繁調度帶來的開銷,所以,它要比基於鎖的方式擁有更優越的性能。
  • (2)無鎖的好處:
    第一,在高併發的狀況下,它比有鎖的程序擁有更好的性能;
    第二,它天生就是死鎖免疫的。
  • (3)CAS算法的過程是這樣:它包含三個參數CAS(V,E,N): V表示要更新的變量,E表示預期值,N表示新值。僅當V值等於E值時,纔會將V的值設爲N,若是V值和E值不一樣,則說明已經有其餘線程作了更新,則當前線程什麼都不作。最後,CAS返回當前V的真實值。 (V表示要更新的變量 主線程的值 主內存,E表示預期值 本地內存 工做內存,N表示新值)
  • (4)CAS操做是抱着樂觀的態度進行的,它老是認爲本身能夠成功完成操做。當多個線程同時使用CAS操做一個變量時,只有一個會勝出,併成功更新,其他均會失敗。失敗的線程不會被掛起,僅是被告知失敗,而且容許再次嘗試,固然也容許失敗的線程放棄操做。基於這樣的原理,CAS操做即便沒有鎖,也能夠發現其餘線程對當前線程的干擾,並進行恰當的處理。
  • (5)簡單地說,CAS須要你額外給出一個指望值,也就是你認爲這個變量如今應該是什麼樣子的。若是變量不是你想象的那樣,那說明它已經被別人修改過了。你就從新讀取,再次嘗試修改就行了。
  • (6)在硬件層面,大部分的現代處理器都已經支持原子化的CAS指令。在JDK 5.0之後,虛擬機即可以使用這個指令來實現併發操做和併發數據結構,而且,這種操做在虛擬機中能夠說是無處不在。

二、CAS缺點

CAS存在一個很明顯的問題,即ABA問題。

問題:若是變量V初次讀取的時候是A,而且在準備賦值的時候檢查到它仍然是A,那能說明它的值沒有被其餘線程修改過了嗎?

若是在這段期間曾經被改爲B,而後又改回A,那CAS操做就會誤認爲它歷來沒有被修改過。針對這種狀況,java併發包中提供了一個帶有標記的原子引用類AtomicStampedReference,它能夠經過控制變量值的版原本保證CAS的正確性。

我的博客 蝸牛

相關文章
相關標籤/搜索