Java同步容器和併發容器

来源:https://www.cnblogs.com/yuxiang1/archive/2019/04/04/10656959.html
-Advertisement-
Play Games

同步容器 在 Java 中,同步容器主要包括 2 類: Vector、Stack、HashTableCollections 類中提供的靜態工廠方法創建的類(由 Collections.synchronizedXxxx 等方法) Vector 實現了 List 介面,Vector 實際上就是一個數組, ...


同步容器

在 Java 中,同步容器主要包括 2 類:

  • Vector、Stack、HashTableCollections 類中提供的靜態工廠方法創建的類(由 Collections.synchronizedXxxx 等方法)
    • Vector 實現了 List 介面,Vector 實際上就是一個數組,和 ArrayList 類似,但是 Vector 中的方法都是 synchronized 方法,即進行了同步措施。
    • Stack 也是一個同步容器,它的方法也用 synchronized 進行了同步,它實際上是繼承於 Vector 類。
    • HashTable 實現了 Map 介面,它和 HashMap 很相似,但是 HashTable 進行了同步處理,而 HashMap 沒有。

同步容器的缺陷

同步容器的同步原理就是在方法上用 synchronized 修飾。那麼,這些方法每次只允許一個線程調用執行。

性能問題

由於被 synchronized 修飾的方法,每次只允許一個線程執行,其他試圖訪問這個方法的線程只能等待。顯然,這種方式比沒有使用 synchronized 的容器性能要差。

安全問題

同步容器真的一定安全嗎?

答案是:未必。同步容器未必真的安全。在做複合操作時,仍然需要加鎖來保護。

常見覆合操作如下:

  • 迭代:反覆訪問元素,直到遍歷完全部元素;
  • 跳轉:根據指定順序尋找當前元素的下一個(下 n 個)元素;
  • 條件運算:例如若沒有則添加等;
不安全的示例
public class Test {
    static Vector<Integer> vector = new Vector<Integer>();
    public static void main(String[] args) throws InterruptedException {
        while(true) {
            for(int i=0;i<10;i++)
                vector.add(i);
            Thread thread1 = new Thread(){
                public void run() {
                    for(int i=0;i<vector.size();i++)
                        vector.remove(i);
                };
            };
            Thread thread2 = new Thread(){
                public void run() {
                    for(int i=0;i<vector.size();i++)
                        vector.get(i);
                };
            };
            thread1.start();
            thread2.start();
            while(Thread.activeCount()>10)   {

            }
        }
    }
}

 

執行時可能會出現數組越界錯誤。

Vector 是線程安全的,為什麼還會報這個錯?很簡單,對於 Vector,雖然能保證每一個時刻只能有一個線程訪問它,但是不排除這種可能:

當某個線程在某個時刻執行這句時:

for(int i=0;i<vector.size();i++)
    vector.get(i);

 

假若此時 vector 的 size 方法返回的是 10,i 的值為 9

然後另外一個線程執行了這句:

for(int i=0;i<vector.size();i++)
    vector.remove(i);

 

將下標為 9 的元素刪除了。

那麼通過 get 方法訪問下標為 9 的元素肯定就會出問題了。

安全示例

因此為了保證線程安全,必須在方法調用端做額外的同步措施,如下麵所示:

public class Test {
    static Vector<Integer> vector = new Vector<Integer>();
    public static void main(String[] args) throws InterruptedException {
        while(true) {
            for(int i=0;i<10;i++)
                vector.add(i);
            Thread thread1 = new Thread(){
                public void run() {
                    synchronized (Test.class) {   //進行額外的同步
                        for(int i=0;i<vector.size();i++)
                            vector.remove(i);
                    }
                };
            };
            Thread thread2 = new Thread(){
                public void run() {
                    synchronized (Test.class) {
                        for(int i=0;i<vector.size();i++)
                            vector.get(i);
                    }
                };
            };
            thread1.start();
            thread2.start();
            while(Thread.activeCount()>10)   {

            }
        }
    }
}

 

ConcurrentModificationException 異常

在對 Vector 等容器併發地進行迭代修改時,會報 ConcurrentModificationException 異常,關於這個異常將會在後續文章中講述。

但是在併發容器中不會出現這個問題。

併發容器

JDK 的 java.util.concurrent 包(即 juc)中提供了幾個非常有用的併發容器。

  • CopyOnWriteArrayList - 線程安全的 ArrayList
  • CopyOnWriteArraySet - 線程安全的 Set,它內部包含了一個 CopyOnWriteArrayList,因此本質上是由 CopyOnWriteArrayList 實現的。
  • ConcurrentSkipListSet - 相當於線程安全的 TreeSet。它是有序的 Set。它由 ConcurrentSkipListMap 實現。
  • ConcurrentHashMap - 線程安全的 HashMap。採用分段鎖實現高效併發。
  • ConcurrentSkipListMap - 線程安全的有序 Map。使用跳錶實現高效併發。
  • ConcurrentLinkedQueue - 線程安全的無界隊列。底層採用單鏈表。支持 FIFO。
  • ConcurrentLinkedDeque - 線程安全的無界雙端隊列。底層採用雙向鏈表。支持 FIFO 和 FILO。
  • ArrayBlockingQueue - 數組實現的阻塞隊列。
  • LinkedBlockingQueue - 鏈表實現的阻塞隊列。
  • LinkedBlockingDeque - 雙向鏈表實現的雙端阻塞隊列。

ConcurrentHashMap

要點

  • 作用:ConcurrentHashMap 是線程安全的 HashMap。
  • 原理:JDK6 與 JDK7 中,ConcurrentHashMap 採用了分段鎖機制。JDK8 中,摒棄了鎖分段機制,改為利用 CAS 演算法。

源碼

JDK7

ConcurrentHashMap 類在 jdk1.7 中的設計,其基本結構如圖所示:

每一個 segment 都是一個 HashEntry<K,V>[] table, table 中的每一個元素本質上都是一個 HashEntry 的單向隊列。比如 table[3]為首節點,table[3]->next 為節點 1,之後為節點 2,依次類推。

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
        implements ConcurrentMap<K, V>, Serializable {

    // 將整個hashmap分成幾個小的map,每個segment都是一個鎖;與hashtable相比,這麼設計的目的是對於put, remove等操作,可以減少併發衝突,對
    // 不屬於同一個片段的節點可以併發操作,大大提高了性能
    final Segment<K,V>[] segments;

    // 本質上Segment類就是一個小的hashmap,裡面table數組存儲了各個節點的數據,繼承了ReentrantLock, 可以作為互拆鎖使用
    static final class Segment<K,V> extends ReentrantLock implements Serializable {
        transient volatile HashEntry<K,V>[] table;
        transient int count;
    }

    // 基本節點,存儲Key, Value值
    static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;
    }
}

 

JDK8
  • jdk8 中主要做了 2 方面的改進
  • 取消 segments 欄位,直接採用 transient volatile HashEntry<K,V>[] table 保存數據,採用 table 數組元素作為鎖,從而實現了對每一行數據進行加鎖,進一步減少併發衝突的概率。
  • 將原先 table 數組+單向鏈表的數據結構,變更為 table 數組+單向鏈表+紅黑樹的結構。對於 hash 表來說,最核心的能力在於將 key hash 之後能均勻的分佈在數組中。如果 hash 之後散列的很均勻,那麼 table 數組中的每個隊列長度主要為 0 或者 1。但實際情況並非總是如此理想,雖然 ConcurrentHashMap 類預設的載入因數為 0.75,但是在數據量過大或者運氣不佳的情況下,還是會存在一些隊列長度過長的情況,如果還是採用單向列表方式,那麼查詢某個節點的時間複雜度為 O(n);因此,對於個數超過 8(預設值)的列表,jdk1.8 中採用了紅黑樹的結構,那麼查詢的時間複雜度可以降低到 O(logN),可以改進性能。
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 如果table為空,初始化;否則,根據hash值計算得到數組索引i,如果tab[i]為空,直接新建節點Node即可。註:tab[i]實質為鏈表或者紅黑樹的首節點。
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 如果tab[i]不為空並且hash值為MOVED,說明該鏈表正在進行transfer操作,返回擴容完成後的table。
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 針對首個節點進行加鎖操作,而不是segment,進一步減少線程衝突
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 如果在鏈表中找到值為key的節點e,直接設置e.val = value即可。
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            // 如果沒有找到值為key的節點,直接新建Node並加入鏈表即可。
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 如果首節點為TreeBin類型,說明為紅黑樹結構,執行putTreeVal操作。
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                // 如果節點數>=8,那麼轉換鏈表結構為紅黑樹結構。
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 計數增加1,有可能觸發transfer操作(擴容)。
    addCount(1L, binCount);
    return null;
}

 

示例
public class ConcurrentHashMapDemo {

    public static void main(String[] args) throws InterruptedException {

        // HashMap 在併發迭代訪問時會拋出 ConcurrentModificationException 異常
        // Map<Integer, Character> map = new HashMap<>();
        Map<Integer, Character> map = new ConcurrentHashMap<>();

        Thread wthread = new Thread(() -> {
            System.out.println("寫操作線程開始執行");
            for (int i = 0; i < 26; i++) {
                map.put(i, (char) ('a' + i));
            }
        });
        Thread rthread = new Thread(() -> {
            System.out.println("讀操作線程開始執行");
            for (Integer key : map.keySet()) {
                System.out.println(key + " - " + map.get(key));
            }
        });
        wthread.start();
        rthread.start();
        Thread.sleep(1000);
    }
}

 

CopyOnWriteArrayList

要點

  • 作用:CopyOnWrite 字面意思為寫入時複製。CopyOnWriteArrayList 是線程安全的 ArrayList。
  • 原理:
    • 在 CopyOnWriteAarrayList 中,讀操作不同步,因為它們在內部數組的快照上工作,所以多個迭代器可以同時遍歷而不會相互阻塞(1,2,4)。
    • 所有的寫操作都是同步的。他們在備份數組(3)的副本上工作。寫操作完成後,後備陣列將被替換為複製的陣列,並釋放鎖定。支持數組變得易變,所以替換數組的調用是原子(5)。
    • 寫操作後創建的迭代器將能夠看到修改的結構(6,7)。
    • 寫時複製集合返回的迭代器不會拋出 ConcurrentModificationException,因為它們在數組的快照上工作,並且無論後續的修改(2,4)如何,都會像迭代器創建時那樣完全返回元素。

源碼

重要屬性
  • lock - 執行寫時複製操作,需要使用可重入鎖加鎖
  • array - 對象數組,用於存放元素
    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

 

重要方法
  • 添加操作
    • 添加的邏輯很簡單,先將原容器 copy 一份,然後在新副本上執行寫操作,之後再切換引用。當然此過程是要加鎖的。
  • public boolean add(E e) {
        //ReentrantLock加鎖,保證線程安全
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            //拷貝原容器,長度為原容器長度加一
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //在新副本上執行添加操作
            newElements[len] = e;
            //將原容器引用指向新副本
            setArray(newElements);
            return true;
        } finally {
            //解鎖
            lock.unlock();
        }
    }

     

    刪除操作
    • 刪除操作同理,將除要刪除元素之外的其他元素拷貝到新副本中,然後切換引用,將原容器引用指向新副本。同屬寫操作,需要加鎖。
public E remove(int index) {
    //加鎖
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        if (numMoved == 0)
            //如果要刪除的是列表末端數據,拷貝前len-1個數據到新副本上,再切換引用
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            //否則,將除要刪除元素之外的其他元素拷貝到新副本中,並切換引用
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,
                              numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        //解鎖
        lock.unlock();
    }
}

 

  • 讀操作
    • CopyOnWriteArrayList 的讀操作是不用加鎖的,性能很高。
public E get(int index) {
    return get(getArray(), index);
}
private E get(Object[] a, int index) {
    return (E) a[index];
}

 

示例

public class CopyOnWriteArrayListDemo {

    static class ReadTask implements Runnable {

        List<String> list;

        ReadTask(List<String> list) {
            this.list = list;
        }

        public void run() {
            for (String str : list) {
                System.out.println(str);
            }
        }
    }

    static class WriteTask implements Runnable {

        List<String> list;
        int index;

        WriteTask(List<String> list, int index) {
            this.list = list;
            this.index = index;
        }

        public void run() {
            list.remove(index);
            list.add(index, "write_" + index);
        }
    }

    public void run() {
        final int NUM = 10;
        // ArrayList 在併發迭代訪問時會拋出 ConcurrentModificationException 異常
        // List<String> list = new ArrayList<>();
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < NUM; i++) {
            list.add("main_" + i);
        }
        ExecutorService executorService = Executors.newFixedThreadPool(NUM);
        for (int i = 0; i < NUM; i++) {
            executorService.execute(new ReadTask(list));
            executorService.execute(new WriteTask(list, i));
        }
        executorService.shutdown();
    }

    public static void main(String[] args) {
        new CopyOnWriteArrayListDemo().run();
    }
}

免費Java資料需要自己領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分散式等教程。
傳送門:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.概念 值傳遞:方法調用時,實際傳入的是它的副本,在方法中對值的修改,不影響調用者的值。 引用傳遞:方法調用時,實際傳入的是參數的實際記憶體地址,調用者和調用方法所操作的參數都指向同一記憶體地址,所以方法中操作會影響調用者。 2.問題 ① 值傳遞傳入的值,是它的副本是什麼意思? 列印結果: 0 此處調 ...
  • 死磕 java集合之TreeMap源碼分析(二) 紅黑樹插入元素的時間複雜度如何? 為什麼插入元素之後要做平衡? 以什麼樣的形式平衡最省時間? 如果插入元素的順序不一樣,會得到同樣的樹嗎? ...
  • 第三天學習內容 今日內容 1、整型(int) 2、布爾類型(bool) 3、字元串(str) 內容詳細 1、整型 Python中的整型用int表示。 1.python2中: 在32位機器上,整數的位數為32位,取值範圍為 2 31~2 31 1,即 2147483648~2147483647 在64 ...
  • 爬蟲常用庫urllib 註:運行環境為PyCharm urllib是Python3內置的HTTP請求庫 urllib.request:請求模塊 urllib.error:異常處理模塊 urllib.parse:url解析模塊 urllib.robotparse:robot.txt解析模塊 1、url ...
  • 1 package quicksort; 2 3 import org.junit.Test; 4 5 import java.util.Arrays; 6 7 public class QuickSort { 8 9 @Test 10 public void setUp() throws Exce... ...
  • 文章大綱 一、JSON介紹二、常見框架介紹與實戰三、Studio中GsonFormat插件使用四、項目源碼下載(含參考資料)五、參考文檔 一、JSON介紹 1. 簡介 JSON 的全稱是 JavaScript Object Notation,是一種輕量級的數據交換格 式。 2. 特點 (1)JSON ...
  • 本文將主要講述 的內部結構和實現邏輯,在看本文之前最好先瞭解一下 隊列鎖, 就是根據 隊列鎖的變種實現的,因為本身 比較複雜不容易看清楚他本身的實現邏輯,所以查看 隊列鎖的實現,可以幫助我們理清楚他內部的關係;關於隊列鎖的內容可以參考 , "CLH、MCS 隊列鎖簡介" ; 一、AQS 結構概述 在 ...
  • 上一篇小樂帶大家學過 Java8新特性-Lambda表達式,那什麼時候可以使用Lambda?通常Lambda表達式是用在函數式介面上使用的。從Java8開始引入了函數式介面,其說明比較簡單:函數式介面(Functional Interface)就是一個有且僅有一個抽象方法,但是可以有多個非抽象方法的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...