簡體   English   中英

同步HashMap與ConcurrentHashMap寫測試

[英]Synchronized HashMap vs ConcurrentHashMap write test

我學習java.util.concurrency,我發現了一篇關於性能的文章( http://www.javamex.com/tutorials/concurrenthashmap_scalability.shtml )。 我決定重復這些性能測試的一小部分用於研究目的。 我已經為HashMapConcurrentHashMap編寫了寫測試。 我有兩個問題:

  1. 是真的,為了獲得最佳性能,我應該使用數字線程相等數量的CPU核心?
  2. 據我所知,性能因平台而異。 但結果表明HashMapConcurrentHashMap快一點。 我認為應該是相同的,反之亦然。 也許我在我的代碼中犯了一個錯誤。

任何批評都是受歡迎的。

package Concurrency;

import java.util.concurrent.*;
import java.util.*;

class Writer2 implements Runnable {
    private Map<String, Integer> map;
    private static int index;
    private int nIteration; 
    private Random random = new Random();
    char[] chars = "abcdefghijklmnopqrstuvwxyz".toCharArray();
    private final CountDownLatch latch;

    public Writer2(Map<String, Integer> map, int nIteration, CountDownLatch latch) {
        this.map = map;
        this.nIteration = nIteration;
        this.latch = latch;
    }

    private synchronized String getNextString() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 5; i++) {
            char c = chars[random.nextInt(chars.length)];
            sb.append(c);
        }
        sb.append(index);
        if(map.containsKey(sb.toString()))
            System.out.println("dublicate:" + sb.toString());
        return sb.toString();
    }

    private synchronized int getNextInt() { return index++; }

    @Override
    public void run() {
        while(nIteration-- > 0) {
            map.put(getNextString(), getNextInt());
        }
        latch.countDown();
    }
}

public class FourtyTwo {
    static final int nIteration = 100000;
    static final int nThreads = 4;
    static Long testMap(Map<String, Integer> map) throws InterruptedException{
        String name = map.getClass().getSimpleName(); 
        CountDownLatch latch = new CountDownLatch(nThreads);
        long startTime = System.currentTimeMillis();
            ExecutorService exec = Executors.newFixedThreadPool(nThreads);
            for(int i = 0; i < nThreads; i++)
                exec.submit(new Writer2(map, nIteration, latch));
            latch.await();  
            exec.shutdown();
        long endTime = System.currentTimeMillis();
        System.out.format(name + ": that took %,d milliseconds %n", (endTime - startTime));
        return (endTime - startTime);
    }
    public static void main(String[] args) throws InterruptedException {
        ArrayList<Long> result = new ArrayList<Long>() {
            @Override
            public String toString() {
                Long result = 0L;
                Long size = new Long(this.size());
                for(Long i : this)
                    result += i;
                return String.valueOf(result/size);
            }
        }; 

        Map<String, Integer> map1 = Collections.synchronizedMap(new HashMap<String, Integer>());
        Map<String, Integer> map2 = new ConcurrentHashMap<>();

        System.out.println("Rinning test...");
        for(int i = 0; i < 5; i++) {
            //result.add(testMap(map1)); 
            result.add(testMap(map2));
        }
        System.out.println("Average time:" + result + " milliseconds");

    }

}

/*
OUTPUT:
ConcurrentHashMap: that took 5 727 milliseconds 
ConcurrentHashMap: that took 2 349 milliseconds 
ConcurrentHashMap: that took 9 530 milliseconds 
ConcurrentHashMap: that took 25 931 milliseconds 
ConcurrentHashMap: that took 1 056 milliseconds 
Average time:8918 milliseconds

SynchronizedMap: that took 6 471 milliseconds 
SynchronizedMap: that took 2 444 milliseconds 
SynchronizedMap: that took 9 678 milliseconds 
SynchronizedMap: that took 10 270 milliseconds 
SynchronizedMap: that took 7 206 milliseconds 
Average time:7213 milliseconds

*/

有多少線程不同,不是CPU,而是你正在做的事情。 例如,如果您對線程執行的操作是高度磁盤密集型的,那么您的CPU可能不會被最大化,因此執行8個線程可能只會導致嚴重的顛簸。 但是,如果你有大量的磁盤活動,接着是繁重的計算,然后是更多的磁盤活動,你可以從錯開線程,拆分活動和分組,以及使用更多線程中受益。 例如,在這種情況下,您可能希望將使用單個文件的文件活動組合在一起,但可能不是您從一堆文件中提取的活動(除非它們是在磁盤上連續寫入的)。 當然,如果你過度思考磁盤IO,你可能會嚴重損害你的性能,但我要說的是你不應該只是推卸它。 在這樣的程序中,我可能會有專用於磁盤IO的線程,專用於CPU工作的線程。 分而治之。 您擁有更少的IO線程和更多的CPU線程。

同步服務器運行比核心/ CPU更多的線程是很常見的,因為大多數線程只能在很短的時間內工作,或者不會進行大量CPU密集型工作。 但是,擁有500個線程沒有用,如果你只有2個客戶端,那些多余線程的上下文切換會妨礙性能。 這是一種平衡行為,通常需要一點點調整。

簡而言之

  1. 想想你在做什么
    • 網絡活動很少,因此更多線程通常都很好
    • 如果你的線程比核心多2倍,那么CPU密集型的東西並沒有太大的好處...通常略高於1倍或略低於1倍是最佳的,但你必須進行測試,測試,測試
    • 擁有10個磁盤IO密集型線程可能會損害所有10個線程,就像擁有30個CPU密集型線程一樣......顛簸會傷害所有線程
  2. 盡量分散痛苦
    • 看看它是否有助於分散CPU,IO等,工作或集群是否更好......這取決於你在做什么
  3. 嘗試分組
    • 如果可以,請分離您的磁盤,IO和網絡任務,並為他們提供自己的線程,這些線程可以調整到這些任務

通常,線程不安全的方法運行得更快。 類似地,使用本地化同步比同步整個方法運行得更快。 因此,HashMap通常比ConcurrentHashMap快得多。 與StringBuilder相比,另一個例子是StringBuffer。 StringBuffer是同步的,不僅速度慢,而且同步更重(更多代碼等); 它應該很少使用。 但是,如果有多個線程命中它,StringBuilder是不安全的 話雖如此,StringBuffer和ConcurrentHashMap也可以競爭。 “線程安全”並不意味着您可以毫無思考地使用它,特別是這兩個類的操作方式。 例如,如果您同時進行讀寫操作(例如,在執行put或remove時使用contains(Object)),您仍然可以處於競爭狀態。 如果要防止此類事情,則必須使用自己的類或將調用同步到ConcurrentHashMap。

我通常使用非並發映射和集合,只需在我需要的地方使用自己的鎖。 你會發現它的速度要快得多,控制也很棒。 Atomics(例如AtomicInteger)有時很好,但對我的工作通常並不常用。 玩類,玩同步,你會發現你可以比ConcurrentHashMap,StringBuffer等的霰彈槍方法更有效地掌握你。如果你不這樣做,你可以有競爭條件,無論你是否使用這些類它是對的...但如果你自己做,你也可以更高效,更小心。


請注意,我們有一個新的Object,我們正在鎖定它。 在方法上使用此而不是synchronized

public final class Fun {
    private final Object lock = new Object();

    /*
     * (non-Javadoc)
     *
     * @see java.util.Map#clear()
     */
    @Override
    public void clear() {
        // Doing things...
        synchronized (this.lock) {
            // Where we do sensitive work
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see java.util.Map#put(java.lang.Object, java.lang.Object)
     */
    @Override
    public V put(final K key, @Nullable final V value) {
        // Doing things...
        synchronized (this.lock) {
            // Where we do sensitive work
        }
        // Doing things...
    }
}

從你的代碼......

我可能不會把sb.append(index)放在鎖中,或者可能有單獨的鎖用於索引調用,但是......

    private final Object lock = new Object();

    private String getNextString() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 5; i++) {
            char c = chars[random.nextInt(chars.length)];
            sb.append(c);
        }
        synchronized (lock) {
            sb.append(index);
            if (map.containsKey(sb.toString()))
                System.out.println("dublicate:" + sb.toString());
        }
        return sb.toString();
    }

    private int getNextInt() {
        synchronized (lock) {
            return index++;
        }
    }

您鏈接的文章並未聲明ConcurrentHashMap通常比同步HashMap更快,只是它更好地擴展; 即對於大量線程來說它更快。 正如您在圖表中看到的,對於4個線程,性能非常相似。

除此之外,您應該測試更多項目,正如我的結果所示,它們可能會有很大不同:

Rinning test...
SynchronizedMap: that took 13,690 milliseconds 
SynchronizedMap: that took 8,210 milliseconds 
SynchronizedMap: that took 11,598 milliseconds 
SynchronizedMap: that took 9,509 milliseconds 
SynchronizedMap: that took 6,992 milliseconds 
Average time:9999 milliseconds

Rinning test...
ConcurrentHashMap: that took 10,728 milliseconds 
ConcurrentHashMap: that took 7,227 milliseconds 
ConcurrentHashMap: that took 6,668 milliseconds 
ConcurrentHashMap: that took 7,071 milliseconds 
ConcurrentHashMap: that took 7,320 milliseconds 
Average time:7802 milliseconds

請注意,您的代碼不會清除循環之間的Map,每次添加更多項目...是您想要的嗎?

您的getNextInt / getNextString不需要synchronized ,因為它們不是從多個線程調用的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM