簡體   English   中英

Java如何實現對ConcurrentHashMap讀取的鎖定

[英]Java How to implement lock on ConcurrentHashMap read

TL;DR:在 Java 中,我有 N 個線程,每個線程使用一個共享集合。 ConcurrentHashMap 允許我鎖定寫入,但不能鎖定讀取。 我需要的是鎖定集合的特定項目,讀取以前的數據,進行一些計算並更新值。 如果兩個線程收到來自同一個發送者的兩條消息,則第二個線程必須等待第一個完成,然后才能執行其操作。


長版:

這些線程接收按時間順序排列的消息,它們必須根據messageSenderID更新集合。

我的代碼簡化如下:

public class Parent {
    private Map<String, MyObject> myObjects;

    ExecutorService executor;
    List<Future<?>> runnables = new ArrayList<Future<?>>();

    public Parent(){
        myObjects= new ConcurrentHashMap<String, MyObject>();

        executor = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            WorkerThread worker = new WorkerThread("worker_" + i);
            Future<?> future = executor.submit(worker);
            runnables.add(future);
        }
    }

    private synchronized String getMessageFromSender(){
        // Get a message from the common source
    }

    private synchronized MyObject getMyObject(String id){
        MyObject myObject = myObjects.get(id);
        if (myObject == null) {
            myObject = new MyObject(id);
            myObjects.put(id, myObject);
        }
        return myObject;
    }

    private class WorkerThread implements Runnable {
        private String name;

        public WorkerThread(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            while(!isStopped()) {
                JSONObject message = getMessageFromSender();
                String id = message.getString("id");
                MyObject myObject = getMyObject(id);
                synchronized (myObject) {
                    doLotOfStuff(myObject);
                }
            }
        }
    }
}

所以基本上我有一個生產者和 N 個消費者,以加快處理速度,但 N 個消費者必須處理一個共同的數據基礎,並且必須遵守時間順序。

我目前正在使用ConcurrentHashMap ,但如果需要,我願意更改它。

如果具有相同 ID 的消息間隔足夠遠(> 1 秒),代碼似乎可以工作,但是如果我在幾微秒的距離內收到兩條具有相同 ID 的消息,我將得到兩個線程處理集合中的同一項目。

我想要的行為是:

Thread 1                        Thread 2
--------------------------------------------------------------
read message 1
find ID
lock that ID in collection
do computation and update
                                read message 2
                                find ID
                                lock that ID in collection
                                do computation and update

雖然我認為這是發生了什么:

Thread 1                        Thread 2
--------------------------------------------------------------
read message 1
                                read message 2
                                find ID
                                lock that ID in collection
                                do computation and update
find ID
lock that ID in collection
do computation and update

我想過做類似的事情

JSONObject message = getMessageFromSender();
synchronized(message){
    String id = message.getString("id");
    MyObject myObject = getMyObject(id);
    synchronized (myObject) {
        doLotOfStuff(myObject);
    } // well maybe this inner synchronized is superfluous, at this point
}

但我認為這會破壞多線程結構的全部目的,因為我一次只讀取一條消息,而工作人員不會做任何其他事情; 就像我使用 SynchronizedHashMap 而不是 ConcurrentHashMap 一樣。


作為記錄,我在此報告我最終實施的解決方案。 我不確定它是否是最佳的,我仍然需要測試性能,但至少輸入是正確的。

public class Parent implements Runnable {

    private final static int NUM_WORKERS = 10;
    ExecutorService executor;
    List<Future<?>> futures = new ArrayList<Future<?>>();
    List<WorkerThread> workers = new ArrayList<WorkerThread>();

    @Override
    public void run() {
        executor = Executors.newFixedThreadPool(NUM_WORKERS);
        for (int i = 0; i < NUM_WORKERS; i++) {
            WorkerThread worker = new WorkerThread("worker_" + i);
            Future<?> future = executor.submit(worker);
            futures.add(future);
            workers.add(worker);
        }

        while(!isStopped()) {
            byte[] message = getMessageFromSender();
            byte[] id = getId(message);
            int n = Integer.valueOf(Byte.toString(id[id.length-1])) % NUM_WORKERS;
            if(n >= 0 && n <= (NUM_WORKERS-1)){
                workers.get(n).addToQueue(line);
            }
        }
    }

    private class WorkerThread implements Runnable {
        private String name;
        private Map<String, MyObject> myObjects;
        private LinkedBlockingQueue<byte[]> queue;

        public WorkerThread(String name) {
            this.name = name;
        }

        public void addToQueue(byte[] line) {
            queue.add(line);
        }

        @Override
        public void run() {
            while(!isStopped()) {
                byte[] message= queue.poll();
                if(line != null) {
                    String id = getId(message);
                    MyObject myObject = getMyObject(id);
                    doLotOfStuff(myObject);
                }
            }
        }
    }
}

從概念上講,這是一種路由問題。 你需要的是:

讓您的主線程(單線程)讀取隊列的消息並將數據推送到每個 id 的 FIFO 隊列。 獲取一個線程來消費每個隊列中的消息。

鎖定示例將(可能)不起作用,因為即使fair=true也無法保證第二個消息順序。

來自 Javadoc: Even when this lock has been set to use a fair ordering policy, a call to tryLock() will immediately acquire the lock if it is available, whether or not other threads are currently waiting for the lock.

您需要決定的一件事是,您是要為每個隊列創建一個線程(一旦隊列為空,它將退出)還是保持固定大小的線程池並管理獲取額外位以將線程分配給隊列。

因此,您從原始隊列中讀取單個線程並寫入每個 id 隊列,並且您還從單個隊列中每個 id 讀取一個線程。 這將確保任務序列化。

在性能方面,只要傳入的消息具有良好的分布(id-wise),您就會看到顯着的加速。 如果您獲得大部分相同 ID 的消息,那么任務將被序列化,並且還包括控制對象創建和同步的開銷。

您可以為您的鎖使用單獨的Map 還有一個WeakHashMap會在鍵不再存在時自動丟棄條目。

static final Map<String, Lock> locks = Collections.synchronizedMap(new WeakHashMap<>());

public void lock(String id) throws InterruptedException {
    // Grab a Lock out of the map.
    Lock l = locks.computeIfAbsent(id, k -> new ReentrantLock());
    // Lock it.
    l.lockInterruptibly();
}

public void unlock(String id) throws InterruptedException {
    // Is it locked?
    Lock l = locks.get(id);
    if ( l != null ) {
        l.unlock();
    }
}

我認為您對synchronized塊的想法是正確的,除非您分析錯誤並且在任何情況下都走得太遠。 外部synchronized塊不應該強迫您一次只處理一條消息,它只是防止多個線程同時訪問同一條消息 但你不需要它。 您實際上只需要MyObject實例上的內部synchronized塊。 這將確保一次只有一個線程可以訪問任何給定的MyObject實例,同時允許其他線程根據需要訪問消息、 Map和其他MyObject實例。

JSONObject message = getMessageFromSender();
String id = message.getString("id");
MyObject myObject = getMyObject(id);
synchronized (myObject) {
    doLotOfStuff(myObject);
}

如果您不喜歡那樣,並且MyObject實例的更新都涉及單方法調用,那么您可以只synchronize所有這些方法。 您仍然在Map保留並發性,但您正在保護MyObject本身免受並發更新。

class MyObject {
  public synchronize void updateFoo() {
    // ...
  }

  public synchronize void updateBar() {
    // ...
  }
}

當任何Thread訪問任何updateX()方法時,它將自動鎖定任何其他Thread訪問該方法或任何其他synchronized方法。 如果您的更新與該模式匹配,那將是最簡單的。

如果沒有,那么您需要使用某種鎖定協議使所有工作Threads協作。 OldCurmudgeon 建議的ReentrantLock是一個不錯的選擇,但我會把它放在MyObject本身上。 為了保持正確排序,您應該使用公平參數(請參閱http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.html#ReentrantLock-boolean- )。 “當設置為 true 時,在爭用情況下,鎖有利於授予對等待時間最長的線程的訪問權限。”

class MyObject {
  private final ReentrantLock lock = new ReentrantLock(true);

  public void lock() {
    lock.lock();
  }

  public void unlock() {
    lock.unlock();
  }

  public void updateFoo() {
    // ...
  }

  public void updateBar() {
    // ...
  }
}

然后你可以更新這樣的東西:

JSONObject message = getMessageFromSender();
String id = message.getString("id");
MyObject myObject = getMyObject(id);
myObject.lock();
try {
    doLotOfStuff(myObject);
}
finally {
    myObject.unlock();
}

重要的一點是您不需要控制對消息的訪問,也不需要控制Map 您需要做的就是確保任何給定的MyObject一次最多被一個線程更新。

如果您將 JSON 解析與doLotsOfStuff()分開,您可以獲得一些加速。 一個線程偵聽消息,解析它們,然后將解析的消息放在隊列中以保持時間順序。 第二個線程從該隊列中讀取並執行LotsOfStuff,無需鎖定。

但是,由於您顯然需要 2 倍以上的加速,這可能還不夠。

添加

另一種可能性是多個 HashMap。 例如,如果所有 ID 都是整數,則為以 0,1,2 結尾的 ID 創建 10 個 HashMap...傳入消息將被定向到 10 個線程之一,這些線程解析 JSON 並更新其相關 Map。 在每個 Map 中維護順序,並且不存在鎖定或爭用問題。 假設消息 ID 是隨機分布的,這會產生高達 10 倍的加速,盡管還有一層額外的開銷來獲取您的 Map。 例如

Thread JSON                     Threads 0-9
--------------------------------------------------------------
while (notInterrupted) {
   read / parse next JSON message
   mapToUse = ID % 10
   pass JSON to that Thread's queue
}
                                while (notInterrupted) {
                                   take JSON off queue
                                   // I'm the only one with writing to Map#N
                                   do computation and update ID
                                }

實際上這是一個設計理念:當消費者接受處理您的對象的請求時,它實際上應該從您的對象列表中刪除具有該 ID 的對象,然后在處理完成后將其重新插入。 然后,任何其他消費者請求處理具有相同 ID 的對象都應該處於阻塞模式,等待具有該 ID 的對象重新出現在您的列表中。 您將需要添加一個管理來記錄所有現有對象,以便您可以區分已經存在但當前不在列表中的對象(即正在被其他消費者處理)和尚不存在的對象。

暫無
暫無

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

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