Try using it in your preferred language.

English

  • English
  • 汉语
  • Español
  • Bahasa Indonesia
  • Português
  • Русский
  • 日本語
  • 한국어
  • Deutsch
  • Français
  • Italiano
  • Türkçe
  • Tiếng Việt
  • ไทย
  • Polski
  • Nederlands
  • हिन्दी
  • Magyar
translation

这是AI翻译的帖子。

제이온

[Java] Synchronized Collection vs Concurrent Collection

  • 写作语言: 韓国語
  • 基准国家: 所有国家 country-flag

选择语言

  • 汉语
  • English
  • Español
  • Bahasa Indonesia
  • Português
  • Русский
  • 日本語
  • 한국어
  • Deutsch
  • Français
  • Italiano
  • Türkçe
  • Tiếng Việt
  • ไทย
  • Polski
  • Nederlands
  • हिन्दी
  • Magyar

durumis AI 总结的文章

  • Vector、Hashtable、Collections.synchronizedXXX 等同步集合在内部使用 synchronized 关键字来保证同步, 但在执行多个操作或出现性能下降时可能会存在问题。
  • java.util.concurrent 包提供了并发集合,如 CopyOnWriteArrayList、ConcurrentMap、 ConcurrentHashMap 等,其性能优于同步集合。
  • CopyOnWriteArrayList、ConcurrentHashMap 等并发集合在读取性能方面优于同步集合, 适用于写入操作相对较少的情况。

同步集合

同步集合最具代表性的類別如下:


  • Vector
  • Hashtable
  • Collections.synchronizedXXX


這些類別都使用 synchronized 關鍵字在公開宣告的方法中,以確保一次只允許一個執行緒存取內部的值, 從而保證了同步性。


Vector

public class Vector extends AbstractList
    implements List, RandomAccess, Cloneable, java.io.Serializable {
    ...
    public synchronized boolean add(E e) {
                modCount++;
                add(e, elementData, elementCount);
                return true;
    }
    ...

在 Vector 類別中,觀察添加元素的 add() 方法,可以發現 synchronized 關鍵字。也就是說,在 Vector 内部執行元素插入操作時,會確保同步性。

Hashtable

public class Hashtable extends Dictionary
    implements Map, Cloneable, java.io.Serializable {
        ...
        public synchronized boolean contains(Object value) {
        if (value == null) {
            throw new NullPointerException();
        }

        Entry tab[] = table;
        for (int i = tab.length ; i-- > 0 ;) {
            for (Entry e = tab[i] ; e != null ; e = e.next) {
                if (e.value.equals(value)) {
                    return true;
                }
            }
        }
        return false;
    }
        ...


在 Hashtable 類別中,觀察檢查是否存在相同值的 contains() 方法,可以發現它與 Vector 類別一樣,使用了 synchronized 關鍵字。


Collections.synchronizedXXX

讓我們觀察使用 Collections.synchronizedList() 方法建立的 SynchronizedList 類別。


static class SynchronizedList extends SynchronizedCollection
        implements List {

        final Object mutex;

        ...
        public E get(int index) {
                synchronized (mutex) {return list.get(index);}
        }

        public E set(int index, E element) {
                synchronized (mutex) {return list.set(index, element);}
        }

        public void add(int index, E element) {
                synchronized (mutex) {list.add(index, element);}
        }

        public E remove(int index) {
                synchronized (mutex) {return list.remove(index);}
        }
        ...


觀察 SynchronizedList 類別的方法,可以發現它們都使用了 synchronized 關鍵字。然而,它們使用 synchronized 區塊和互斥鎖來實現同步。所有方法都共用 mutex 物件,因此當一個執行緒進入 synchronized 區塊時,其他方法的 synchronized 區塊都會被鎖定。


同步集合的缺點

在多執行緒環境中,可能需要使用同步集合,但盡可能避免使用其他同步方式。主要原因有兩個。


將多個操作組合在一起使用,如同單一操作

同步集合類別在多執行緒環境中確保同步性。然而,如果需要將多個操作組合在一起使用,如同一個單一操作,就會出現問題。即使使用這些同步集合,也可能無法正常運作。


final List list = Collections.synchronizedList(new ArrayList());
final int nThreads = 2;
ExecutorService es = Executors.newFixedThreadPool(nThreads);

for (int i = 0; i < nThreads; i++) {
    es.execute(new Runnable() {

        public void run() {
            while(true) {
                try {
                    list.clear();
                    list.add("888");
                    list.remove(0);
                } catch(IndexOutOfBoundsException e) {
                    e.printStackTrace();
                }
            }
        }
    });


執行上述程式碼時,如果執行緒 A 執行 remove(0) 操作,而執行緒 B 執行 clear() 操作,就會發生錯誤。因此,需要像以下這樣用 synchronized 區塊將它們括起來。


synchronized (list) {
    list.clear();
    list.add("888");
    list.remove(0);


效能下降

如果將所有想要使用共用物件的方法都變成 synchronized 方法,或者在方法内部定義相同的 synchronized 區塊,當一個執行緒取得鎖定時,其他執行緒就無法使用所有同步方法,只能處於阻塞狀態。這種重複的現象會導致效能下降。


併發集合

java.util.concurrent 包提供以下併發集合類型,本文僅會討論其中一部分。


  • CopyOnWriteArrayList
    • 它是 List 類別的子類別,是一種以迭代訪問物件清單的效能為優先的併發集合。
  • ConcurrentMap
    • 一種併發集合,介面定義了 put-if-absent、replace 和條件移除操作,這些操作在新增項目時, 只有在項目不存在的情況下才會新增。
  • ConcurrentHashMap
    • 它是 ConcurrentMap 的子類別,一種用於取代 HashMap 並確保併發性的併發集合。
  • ConcurrentLinkedQueue
    • 一種 FIFO 方式的 Queue,是一種確保併發性的併發集合。如果 Queue 中沒有元素,它會立即返回, 並執行其他操作。
  • LinkedBlockingQueue
    • 與 ConcurrentLinkedQueue 類似,但如果 Queue 為空,則從 Queue 中取出項目的操作會等待 直到有新的項目新增,反之,如果 Queue 設定了大小,且 Queue 已滿,則新增項目的操作會等待 直到 Queue 有空位。
  • ConcurrentSkipListMap, ConcurrentSkipListSet
    • 分別是 SortedMap 和 SortedSet 類別的改進版本,它們增強了併發性。


只需將現有的同步集合類別替換為併發集合,就能在不引入其他風險因素的情況下,顯著提升整體效能。

讓我們仔細觀察併發集合與同步集合的對比。


CopyOnWriteArrayList

建立同步 ArrayList 的方法有以下兩種:


  • Collections.synchronizedList()
  • CopyOnWriteArrayList


Collections.synchronizedList() 是在 JDK 1.2 版本中新增的。此集合對所有讀取和寫入操作都進行了同步, 因此可以認為它是一種缺乏彈性的設計。因此,出現了 CopyOnWriteArrayList 來彌補它的不足。


讀取操作

SynchronizedList 在讀取和寫入操作時,會對自身實例加鎖。然而,CopyOnWriteArrayList 在執行所有寫入操作時, 都會複製原始陣列中的元素,建立一個新的臨時陣列,在臨時陣列上執行寫入操作,最後更新原始陣列。由於這個設計, 讀取操作不會被鎖定,因此效能比 SynchronizedList 更高。


public class CopyOnWriteArrayList implements List, RandomAccess, Cloneable, java.io.Serializable {

    final transient Object lock = new Object();
    private transient volatile Object[] array;

        ...
        public E get(int index) {
        return elementAt(getArray(), index);
    }
        ...


以上是 get() 方法,由于没有 synchronized,因此不会被锁定。


寫入操作

CopyOnWriteArrayList 在執行寫入操作時,會使用顯式鎖定。最終,兩種集合都會在此操作中被鎖定。這時, CopyOnWriteArrayList 會執行複製陣列的昂貴操作,因此如果執行大量寫入操作,可能會出現效能問題。


public class CopyOnWriteArrayList implements List, RandomAccess, Cloneable, java.io.Serializable {

    final transient Object lock = new Object();
    private transient volatile Object[] array;

        public void add(int index, E element) {
        synchronized (lock) {
            ...
            int numMoved = len - index;
            if (numMoved == 0)
                newElements = Arrays.copyOf(es, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(es, 0, newElements, 0, index);
                System.arraycopy(es, index, newElements, index + 1,
                                 numMoved);
            }
                        ...
        }
    }


以上是 add() 方法,它使用 synchronized 區塊來鎖定並執行複製陣列的操作。


Iterator

CopyOnWriteArrayList 在提取迭代器時,會基於集合資料在提取時點的資料進行迭代,在迭代過程中, 即使集合中新增或刪除資料,也會在迭代器無關的複製本上反映變化,因此不會影響同步使用。


CopyOnWriteArraySet

建立同步 Set 的方法有以下兩種:


  • Collections.synchronizedSet()
  • CopyOnWriteArraySet


正如方法名稱所示,CopyOnWriteArraySet 的工作方式與 CopyOnWriteArrayList 幾乎相同, 除了資料結構特性。


讀取操作

public class CopyOnWriteArraySet extends AbstractSet implements java.io.Serializable {

    private final CopyOnWriteArrayList al;

        public boolean contains(Object o) {
        return al.contains(o);
    }


以上是 contains() 方法,可以發現 CopyOnWriteArraySet 在内部定義了 CopyOnWriteArrayList, 並使用 CopyOnWriteArrayList 的方法。


public boolean contains(Object o) {
        return indexOf(o) >= 0;
}

public int indexOf(Object o) {
        Object[] es = getArray();
        return indexOfRange(o, es, 0, es.length);
}

    private static int indexOfRange(Object o, Object[] es, int from, int to) {
        if (o == null) {
            for (int i = from; i < to; i++)
                if (es[i] == null)
                    return i;
        } else {
            for (int i = from; i < to; i++)
                if (o.equals(es[i]))
                    return i;
        }
        return -1;


觀察 CopyOnWriteArrayList 的 contains() 方法,可以發現它沒有被鎖定。


寫入操作

public class CopyOnWriteArraySet extends AbstractSet implements java.io.Serializable {

        private final CopyOnWriteArrayList al;

        public boolean add(E e) {
        return al.addIfAbsent(e);
    }


add() 方法也借用了 CopyOnWriteArrayList 的方法。


public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOfRange(e, snapshot, 0, snapshot.length) < 0
        && addIfAbsent(e, snapshot);
}

private boolean addIfAbsent(E e, Object[] snapshot) {
        synchronized (lock) {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) {
                // Optimize for lost race to another addXXX operation
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++)
                    if (current[i] != snapshot[i]
                        && Objects.equals(e, current[i]))
                        return false;
                if (indexOfRange(e, current, common, len) >= 0)
                        return false;
            }
            Object[] newElements = Arrays.copyOf(current, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        }


觀察 addIfAbsent() 方法,可以發現寫入操作時會鎖定並執行複製陣列的操作。因此,CopyOnWriteArraySet 與 CopyOnWriteArrayList 一樣,最好不要執行太多寫入操作。


ConcurrentHashMap

建立同步 HashMap 的方法有以下兩種:


  • Collections.synchronizedMap(new HashMap<>())
  • ConcurrentHashMap


ConcurrentHashMap 是一種基於雜湊的 Map,與 HashMap 相同。與 synchronizedMap 相比,它 以更有效的方式確保同步性。


在 Java 8 之前,它使用繼承自 ReentrantLock 的 Segment 來區分區域,並通過鎖定每個區域來確保同步性。


從 Java 8 開始,它採用對每個表格桶進行獨立鎖定的方式。如果要插入一個空的桶,則使用 CAS 演算法 而不是鎖定。其他更改會在每個桶的第一個節點上獲得部分鎖定(synchronized 區塊),以最大限度地 減少執行緒競爭並確保同步性。


讓我們觀察 ConcurrentHashMap 插入新節點的 putVal() 方法的程式碼,以了解它是如何確保同步性的。 請注意,以下示例程式碼是基於 Java 11。


putVal() 方法主要分為以下兩種情況(分支共有四部分):


  • 在空的雜湊桶中插入節點
  • 雜湊桶中已存在節點


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[] tab = table;;) {
            Node f; int n, i, fh; K fk; V fv;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
                        // (1)
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                                // (2)
                if (casTabAt(tab, i, null, new Node(hash, key, value)))
                    break;
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else if (onlyIfAbsent // check first node without acquiring lock
                     && fh == hash
                     && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                     && (fv = f.val) != null)
                return fv;
                        // (3)
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node e = f;; ++binCount) {
                                K ek;
                                                                // (4)
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node pred = e;
                                                                // (5)
                                if ((e = e.next) == null) {
                                    pred.next = new Node(hash, key, value);
                                    break;
                                }
                            }
                        }
                                                // (6)
                        else if (f instanceof TreeBin) {
                            Node p;
                            binCount = 2;
                            if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                                ...
            }
        }
                ...


在空的雜湊桶中插入節點

(1) 要插入一個新節點,會取得對應桶的值(tabAt()),並檢查它是否為空。


static final  Node tabAt(Node[] tab, int i) {
        return (Node)U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE);


(2) 存取節點中包含的 volatile 變數,並將其與原始值(null)進行比較。如果相同,則存儲新的節點。 如果不相同,則返回 for 迴圈。這種方式就是 CAS 演算法。


static final  boolean casTabAt(Node[] tab, int i, Node c, Node v) {
        return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);


使用 CAS 演算法解決原子性和可見性問題,以確保同步性。


雜湊桶中已存在節點

(3) 如果已存在節點,則使用 synchronized 區塊,以確保一次只允許一個執行緒訪問。這時,由於對非空 Node 類型的雜湊桶加鎖,因此訪問相同桶的執行緒會處於阻塞狀態。

(4) 用新的節點替換。

(5) 如果發生雜湊衝突,則添加到 Separate Chaing 中。

(6) 如果發生雜湊衝突,則添加到樹中。


參考


預期面試問題和答案

Vector、HashTable 和 Collections.SynchronziedXXX 的缺點是什麼?

Vector、HashTable 和 SynchronziedXxx 類別使用 synchronized 方法或區塊,並共用一個鎖定物件。因此, 當一個執行緒取得鎖定時,其他執行緒將無法使用所有方法,只能處於阻塞狀態。這會導致應用程式效能下降。


SynchronizedList 和 CopyOnArrayList 之間的差異是什麼?

SynchronizedList 在讀取和寫入操作時,會對自身實例加鎖。然而,CopyOnArrayList 在執行寫入操作時, 會對相應的區塊加鎖,並複製原始陣列中的元素,建立一個新的臨時陣列,在臨時陣列上執行寫入操作,最後更新原始 陣列。由於這個設計,讀取操作不會被鎖定,因此效能比 SynchronizedList 更高。但寫入操作會執行複製陣列的 昂貴操作,因此效能比 SynchronizedList 更低。

因此,如果寫入操作比讀取操作少,則使用 CopyOnArrayList 會更有效。


SynchronizedMap 和 ConcurrentHashMap 之間的差異是什麼?

SynchronziedMap 在讀取和寫入操作時,會對自身實例加鎖。然而,ConcurrentHashMap 使用對每個表格桶進行 獨立鎖定的方式。如果要插入一個空的桶,則使用 CAS 演算法而不是鎖定。其他更改會在訪問的桶上獲得部分鎖定 (synchronized 區塊),以最大限度地減少執行緒競爭並確保同步性。

제이온
제이온
제이온
제이온
[Spring] @Async 使用方法 了解如何使用 Spring @Async 簡化 Java 異步處理的實作。透過 @Async 注解,您可以將同步方法轉換為異步方法,並透過執行緒池設定來提高效率。本文也會探討如何使用 Future、ListenableFuture 和 CompletableFuture 來有效管理異步處理結果。

2024年4月25日

[Effective Java] 項目 6. 避免不必要的物件建立 這是一份關於在 Java 中減少不必要物件建立的指南。對於 String、Boolean 等不變物件,最好使用字面值;對於正規表示式,最好快取 Pattern 物件。此外,自動裝箱會導致效能下降,因此最好使用基本類型。有關更多資訊,請參閱「Effective Java」。

2024年4月28日

Java Collections Framework(JCF)是什麼? - JCF 的定義和特點 (JAVA) Java Collections Framework (JCF) 是一組提供標準化方式來有效處理大量數據的 Java 類。JCF 通過將數據存儲結構和算法實 現為類來提高代碼可重用性、性能提升和 API 相互操作性。

2024年4月27日

[并发] 原子操作:内存栅栏和内存顺序 这篇博文将解释在原子操作中如何考虑内存顺序,以及排序选项的重要性。 它将详细解释各种排序选项,例如 Relaxed、Acquire、Release、AcqRel 和 SecCst,以及每个选项的优缺点, 并提供使用示例代码。
곽경직
곽경직
곽경직
곽경직
곽경직

2024年4月12日

[非计算机专业,如何成为一名开发者] 14. 新手开发者常问的技术面试内容总结 本指南旨在为新手开发者提供技术面试准备指导。涵盖了面试中常见的概念,例如主内存区域、数据结构、关系型数据库 (RDBMS) 和 NoSQL、过程式编程和面向对象编程、重写和重载、页面替换算法、进程和线程、OSI 七层模型、TCP 和 UDP 等。
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자

2024年4月3日

Rust 如何防止并发错误 Rust 是一种强大的语言,可以解决并发编程的难题。 由于其类型系统和所有权模型,线程之间的数据传递和共享是安全的。 通过内部可变性模式,例如 Mutex、Channel 和 Atomic,可以定义共享变量并安全地使用它们。 Rust 积极解决并发问题,使其广泛应用于大型系统开发。
곽경직
곽경직
곽경직
곽경직
곽경직

2024年3月28日

[Javascript] 物件的結構 (V8) JavaScript 的 Object 在 V8 引擎中根據狀態可以被優化為類似結構體的 Fast 模式或以雜湊表運作的 Dictionary 模式。Fast 模式是針對幾乎固定形式的鍵和值進行優化,速度很快,但當新增新鍵或刪除元素等操作時, 會轉換為 Dictionary 模式,速度會變慢。
곽경직
곽경직
곽경직
곽경직
곽경직

2024年3月18日

物理資料模型 物理資料模型是將關係型資料庫的表格設計成實際可使用的過程,透過儲存空間效率、資料分割、索引設計等,以達成效能最佳化為目標。透過慢查詢分析、索引運用、快取應用等,可以解決效能問題,必要時也可以使用反正規化技術。
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그

2024年4月9日

巫師卡牌遊戲,昆特牌術語整理 巫師昆特牌是一款專為奇幻小說、電影、遊戲愛好者打造的免費策略卡牌遊戲。由單位、陷阱、法術、計謀等多種卡牌組成, 戰鬥和管理你的卡牌是遊戲的核心。卡牌效果和佈置順序決定勝負,提供全新的遊戲體驗。
길리
길리
길리
길리

2024年4月7日