Java如何实现对ConcurrentHashMap的锁定读取

时间:2016-03-09 15:21:36

标签: java multithreading

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的消息,我会得到两个处理相同项目的线程。集合。

GUESS 我希望的行为是:

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

当我 THINK 时,会发生这种情况:

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);
                }
            }
        }
    }
}

5 个答案:

答案 0 :(得分:1)

从概念上讲,这是一种路由问题。你需要的是:

获取您的主线程(单线程)读取队列的消息,并将数据推送到每个ID的FIFO队列。 获取单个线程来使用来自每个队列的消息。

锁定示例(可能)不起作用,因为即使var db = new alasql.Database("db"); db.exec('CREATE TABLE IF NOT EXISTS Myonetwo;'); var aaa = db.exec('select * into Myonetwo from json("http://localhost:8080/app1")'); var bbb = db.exec('select * from Myonetwo;'); console.log(bbb.length); 无法保证第二个消息顺序。

来自Javadoc: fair=true

您要决定的一件事是,您是否要为每个队列创建一个线程(一旦队列为空将退出)或保留固定大小的线程池并管理获取额外的位以将线程分配给队列。

因此,您从原始队列中获取单个线程并写入per-id-queues,并且每个ID也从单个队列中读取一个线程。这将确保任务序列化。

就性能而言,只要传入的消息具有良好的分布(id-wise),就应该看到显着的加速。如果您获得大多数相同的id消息,那么任务将被序列化,并且还包括控制对象创建和同步的开销。

答案 1 :(得分:0)

您可以使用单独的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();
    }
}

答案 2 :(得分:0)

我认为你对synchronized块有正确的想法,除了你误解了一点,无论如何都走得太远了。外部synchronized块不应强制您一次只处理一条消息,它只是让多个线程同时访问相同的消息。但你不需要它。您实际上只需要synchronized实例上的内部MyObject块。这将确保一次只有一个线程可以访问任何给定的MyObject实例,同时允许其他线程根据需要访问消息,Map和其他MyObject实例。 / p>

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本身。为了正确排序,您应该使用 fairness 参数(请参阅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

答案 3 :(得分:0)

如果从doLotsOfStuff()拆分JSON解析,则可以获得某些加速。一个线程侦听消息,解析它们,然后将解析的消息放在队列上以维持时间顺序。第二个线程从该Queue和didLotsOfStuff读取而不需要锁定。

然而,由于你显然需要超过2倍的加速,这可能是不够的。

<强>加

另一种可能性是多个HashMaps。例如,如果所有ID都是整数,则为ID为0,1,2的ID生成10个HashMaps ...传入的消息将定向到10个线程中的一个,该线程解析JSON并更新其相关的Map。订单在每个地图内维护,并且没有锁定或争用问题。假设消息ID是随机分布的,这可以产生高达10倍的加速,尽管在Map上有一个额外的开销层。 e.g。

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
                                }

答案 4 :(得分:0)

实际上这里有一个设计理念:当一个消费者接受一个处理你的对象的请求时,它实际上应该从你的对象列表中删除具有该ID的对象,然后在处理完成后重新插入它。然后任何其他消费者获得处理具有相同id的对象的请求应该处于阻塞模式,等待具有该ID的对象重新出现在列表中。您将需要添加管理以保留所有现有对象的记录,以便您可以区分已存在但当前不在列表中的对象(即由其他消费者处理)和尚不存在的对象。