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);
}
}
}
}
}
答案 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的对象重新出现在列表中。您将需要添加管理以保留所有现有对象的记录,以便您可以区分已存在但当前不在列表中的对象(即由其他消费者处理)和尚不存在的对象。