[英]How to handle lock in cache (ConcurrentHashMap) using java
[英]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.