线程安全发布者

时间:2014-07-08 13:34:17

标签: java multithreading concurrency

我遇到了Java并发问题,我认为我已经解决了,但现在我觉得我的解决方案存在问题。

问题:

以线程安全且高效的方式实现缺少的方法。系统应该只按照T key的第一个请求进行订阅,并且一旦给定密钥不再有听众,就应该取消订阅。

interface Listener {
    void onData();
}

abstract class Publisher<T> {       
    public void subscribe(T key, Listener l) {
        // TODO complete
    }

    public void unsubscribe(T key, Listener l) {
        // TODO complete
    }

    public void publish(T key) {
        // TODO complete
    }

    public abstract void reallyLongSubscribeRequest(T key);
    public abstract void reallyLongUnsubscribeRequest(T key);
}

我的解决方案是将密钥和侦听器存储在ConcurrentHashMap中,并使用线程池执行程序来运行对非常长的订阅和取消订阅方法的调用:

abstract class Publisher<T> {
    private final int POOL_SIZE = 5;

    private final ConcurrentHashMap<T, List<Listener>> listeners = new ConcurrentHashMap<>();
    private final ScheduledExecutorService stpe = Executors.newScheduledThreadPool(POOL_SIZE);

    public void subscribe(T key, Listener l) {
        if (listeners.containsKey(key)) {
            listeners.get(key).add(l);
        } else {
            final T keyToAdd = key;
            final List<Listener> list = new LinkedList<Listener>();
            list.add(l);

            Runnable r = new Runnable() {
                public void run() {
                    reallyLongSubscribeRequest(keyToAdd);
                    listeners.putIfAbsent(keyToAdd, list);
                }
            };

            stpe.execute(r);
        }
    }

    public void unsubscribe(T key, Listener l) {
        if (listeners.containsKey(key)) {
            List<Listener> list = listeners.get(key);
            list.remove(l);

            if (list.size() == 0) {
                final T keyToRemove = key;
                Runnable r = new Runnable() {
                    public void run() {
                        reallyLongUnsubscribeRequest(keyToRemove);
                    }
                };
                stpe.execute(r);
            }
        }
    }

    public void publish(T key) {
        if (listeners.containsKey(key)) {
            final List<Listener> list = listeners.get(key);
            for (Listener l : list) {
                l.onData();
            }
        }
    }

    public abstract void reallyLongSubscribeRequest(T key);
    public abstract void reallyLongUnsubscribeRequest(T key);
}

我现在担心这不再是线程安全的,因为

  1. subscribe 中,可以交换活动线程/在进入false分支和执行Runnable之间使其时间片结束。如果下一个线程进行相同的调用(相同的键),那么我们将有两个线程想要订阅并写入映射。 putIfAbsent保持地图一致,但真的很长的方法将被调用两次(如果它改变了类状态,那就太糟糕了。)

  2. 与#1类似,在 unssubscribe 中如果在输入嵌套if的真分支和执行Runnable之间换出线程会怎样?

  3. 所以我的问题是

    1. 我的上述问题是否有效,或者我是否使问题复杂化(或者我误解了时间切片是如何工作的)?
    2. 如果是,可以轻松修复,还是有更好/更简单/更简单的解决方案?

2 个答案:

答案 0 :(得分:1)

你有几个问题。您的列表不是线程安全的,并且您是正确的,您可以多次运行请求。 ConcurrentHashMap是获得并行但线程安全的地图访问的好方法。但是,您需要实现某种“按键”同步,以确保(un)订阅操作正确发生(更不用说列表更新)。

答案 1 :(得分:1)

你有两个问题:

  • 您的subscribeunsubscribepublish方法应为synchronized,以确保其线程安全。

  • 您应该只有一个线程来执行等待reallyLong...()的{​​{1}}个调用。您向Queue发布了一条消息,告诉您要执行其中一项操作,并且确实如此。队列将确保它们一个接一个地发生。

您的代码中也存在错误。只有在地图中没有密钥时才会执行Queue,但是当您删除最后一个侦听器时,您不会从地图中删除密钥。