任务看起来像这样。我们有一堆运行某些方法的线程。可以同时运行它,但只有在满足某些条件时才能运行它。如果不是 - 线程应该等待。
以下是我所谈论的一个例子。
我们有一个耗时的方法doStuff,它将Key实例作为参数。仅当提供的密钥不相等时,它才能在多线程envoronment中正常工作。如果同时处理两个相等的键,则失败。我们需要编写一个代码来停止具有相同键的线程同时调用此方法。我写了三个实现:通过ConcurrentHashMap和这些键,通过关键索引的AtomicIntegerArray和通过简单的synchronized块来检查正在处理的键集。
public class KeyProblem {
static class Key {
private int index;
Key() {
this.index = (int) (Math.random() * 10) % 10;
}
public int getIndex() {
return index;
}
@Override
public String toString() {
return "Key{" +
"index=" + index +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key key = (Key) o;
if (index != key.index) return false;
return true;
}
@Override
public int hashCode() {
return index;
}
}
private static ConcurrentHashMap<Key, Object> keysProcessedRightNowForCheck = new ConcurrentHashMap<Key, Object>();
public static void doStuff(Key key) {
Object sentinel = keysProcessedRightNowForCheck.putIfAbsent(key, new Object());
if (sentinel != null) {
System.out.println("ERROR! Equal keys! " + key + " " + Thread.currentThread().getName());
}
try {
System.out.println(String.format(" started by %s with %s ", Thread.currentThread().getName(), key));
Thread.sleep(500);
System.out.println(String.format(" finished by %s with %s ", Thread.currentThread().getName(), key));
} catch (InterruptedException e) {
}
keysProcessedRightNowForCheck.remove(key);
}
//first version: via ConcurrentHashMap
private static ConcurrentHashMap map = new ConcurrentHashMap();
private static Object waiter = new Object();
public static void viaConcurrentHashMap(Key key) throws InterruptedException {
while (map.putIfAbsent(key, new Object()) != null) {
synchronized (waiter) {
System.out.println("wait with key " + key);
waiter.wait();
System.out.println("done waiting with key " + key);
}
}
System.out.println("started stuff with " + key);
doStuff(key);
map.remove(key);
synchronized (waiter) {
System.out.println("notified after stuff with " + key);
waiter.notifyAll();
System.out.println("done waiting with key " + key);
}
}
//second version: via AtomicIntegerArray for a fixed number of keys
private static AtomicIntegerArray keyProcessed = new AtomicIntegerArray(10);
public static void viaAtomicIntegerArray(Key key) throws InterruptedException {
while (!keyProcessed.compareAndSet(key.getIndex(), 0, 1)) {
synchronized (waiter) {
System.out.println("wait with key " + key);
waiter.wait();
}
}
doStuff(key);
keyProcessed.decrementAndGet(key.getIndex());
synchronized (waiter) {
System.out.println("notified after stuff with " + key);
waiter.notifyAll();
}
}
//third version: via a simple lock
private static Object lock = new Object();
private static Set<Key> keys = new HashSet<Key>();
public static void viaSimpleSynchronized(Key key) throws InterruptedException {
synchronized (lock) {
while (keys.contains(key)) {
lock.wait();
}
keys.add(key);
}
doStuff(key);
synchronized (lock) {
keys.remove(key);
lock.notifyAll();
}
}
private static CyclicBarrier barrier;
public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
final int MAX = 100;
List<Key> keys = new ArrayList<Key>() {{
for (int i = 0; i < MAX; i++) add(new Key());
}};
barrier = new CyclicBarrier(MAX + 1);
long start = System.currentTimeMillis();
for (final Key key : keys) {
Thread t = new Thread() {
public void run() {
try {
// viaConcurrentHashMap(key);
viaSimpleSynchronized(key);
// viaAtomicIntegerArray(key);
barrier.await();
} catch (InterruptedException e) {
} catch (BrokenBarrierException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
}
}
};
t.start();
}
barrier.await();
System.out.println("system time [ms] " + (System.currentTimeMillis() - start));
//7 for array
}
}
对于100个线程,运行时间约为7秒,第三个版本的运行时间略长于预期。
最后,问题是:
1)我的代码是否正确且是线程安全的?
2)你能建议更好的实施吗?
3)java.util.concurrent中是否有一些以通用方式解决此任务的类?我的意思是一种障碍,只有在满足某些条件时才允许线程进入。
答案 0 :(得分:2)
如果您能够一次性将所有密钥保存在内存中,并且作为单例(至少是您的示例如何工作),那么似乎一个非常简单的解决方案如下:
synchronized (key) {}
块内的逻辑。答案 1 :(得分:0)
您的viaSimpleSynchronized
方法是唯一没有被破坏的方法。另外两个遭受同样的错误:你假设执行一个动作并且之后输入一个synchronized
块在某种程度上是紧密的,而事实上,它们是截然不同的,非耦合的动作。
例如:
while (map.putIfAbsent(key, new Object()) != null) {
// in-between right at this point the other thread can do both, remove
// the key AND execute it’s synchronized(waiter) { waiter.notifyAll(); }
synchronized (waiter) {
System.out.println("wait with key " + key);
waiter.wait(); // therefore this can enter a wait lasting forever
System.out.println("done waiting with key " + key);
}
}
你没有发现你的测试有很多线程执行大量操作的错误,因为每个无竞争线程在调用notifyAll()
时会释放所有等待线程隐藏错误线程等待已经发生的通知。
所以在这里我们有代码,当有小活动时,它会惊人地破解。
通常,当您在synchronized
块之外执行检查时,即使它使用类似ConcurrentHashMap
的线程安全构造,您也必须重新检查条件在synchronized
区块内。
唯一的例外是已知永远不会被还原的条件(例如,“就绪”标志将从false
转到true
但从不回到false
。< / p>