实现条件线程障碍的最佳方法

时间:2014-08-24 11:26:35

标签: java concurrency java.util.concurrent concurrenthashmap

任务看起来像这样。我们有一堆运行某些方法的线程。可以同时运行它,但只有在满足某些条件时才能运行它。如果不是 - 线程应该等待。

以下是我所谈论的一个例子。

我们有一个耗时的方法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中是否有一些以通用方式解决此任务的类?我的意思是一种障碍,只有在满足某些条件时才允许线程进入。

2 个答案:

答案 0 :(得分:2)

如果您能够一次性将所有密钥保存在内存中,并且作为单例(至少是您的示例如何工作),那么似乎一个非常简单的解决方案如下:

  1. 获取相应的密钥;
  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>