【问题标题】:Implementing a blocking queue in JavaME: how to optimize it?在 JavaME 中实现阻塞队列:如何优化它?
【发布时间】:2012-04-26 08:38:30
【问题描述】:

我正在尝试在 Java ME 中实现一个简单的阻塞队列。在 JavaME API 中,Java SE 的并发工具不可用,所以我必须像过去一样使用 wait-notify。

这是我的临时实现。我使用notify 而不是notifyAll,因为在我的项目中有多个生产者但只有一个消费者。我故意使用一个对象进行等待通知以提高可读性,尽管它浪费了参考:

    import java.util.Vector;

    public class BlockingQueue {    
        private Vector queue = new Vector();
        private Object queueLock = new Object();    

        public void put(Object o){
            synchronized(queueLock){
                queue.addElement(o);
                queueLock.notify();
            }       
        }

        public Object take(){
            Object ret = null;
            synchronized (queueLock) {
                while (queue.isEmpty()){
                    try {
                        queueLock.wait();
                    } catch (InterruptedException e) {}
                }

                ret = queue.elementAt(0);
                queue.removeElementAt(0);
            }
            return ret;
        }
    }

我的主要问题是关于put 方法。我可以把queue.addElement 线放在synchronized 块之外吗?如果是这样,性能会提高吗?

同样,take 也是如此:我可以将queue 上的两个操作从synchronized block 中取出吗?

还有其他可能的优化吗?

编辑:
正如@Raam 正确指出的那样,消费者线程在wait 中被唤醒时可能会饿死。那么有什么替代方法可以防止这种情况发生呢? (注意:在 JavaME 中,我没有来自 Java SE 的所有这些好类。将其视为旧的 Java v1.2)

【问题讨论】:

标签: java java-me concurrency producer-consumer


【解决方案1】:

Vector 类不保证线程安全,您应该同步访问它,就像您所做的那样。除非您有证据表明您当前的解决方案存在性能问题,否则我不会担心。

附带说明,我认为使用notifyAll 而不是notify 来支持多个消费者并没有什么坏处。

【讨论】:

    【解决方案2】:

    synchronized用于保护对共享状态的访问,保证原子性。

    注意Vector 的方法已经同步,因此Vector 保护它自己的共享状态本身。因此,您的同步块只需要确保您的操作的原子性。

    您当然不能将queue 上的操作从take() 方法中的同步块中移动,因为原子性对于该方法的正确性至关重要。但是,据我了解,您可以在 put() 方法中从同步块中移动队列操作(我无法想象会出错的情况)。

    然而,上面的推理纯属理论,因为在所有情况下你都有双重同步:queueLock 上的同步和Vector 的方法隐式同步queue因此提出的优化没有意义,其正确性取决于双重同步的存在。

    为避免双重同步,您还需要在 queue 上进行同步:

    synchronized (queue) { ... }
    

    另一种选择是使用非同步集合(例如ArrayList)而不是Vector,但JavaME 不支持它。在这种情况下,您也将无法使用建议的优化,因为同步块也保护非同步集合的共享状态。

    【讨论】:

    【解决方案3】:

    除非你因为垃圾回收而特别有性能问题,否则我宁愿使用链表而不是向量来实现队列(先进先出)。

    我还会编写可在您的项目(或其他项目)获得多个消费者时重复使用的代码。尽管在这种情况下,您需要注意 Java 语言规范并没有强加实现监视器的方法。实际上,这意味着您无法控制通知哪个消费者线程(现有 Java 虚拟机中有一半使用 FIFO 模型实现监视器,另一半使用 LIFO 模型实现监视器)

    我还认为使用阻塞类的人也应该处理 InterruptedException。毕竟,否则客户端代码将不得不处理 null Object 返回。

    所以,是这样的:

    
    /*package*/ class LinkedObject {
    
        private Object iCurrentObject = null;
        private LinkedObject iNextLinkedObject = null;
    
        LinkedObject(Object aNewObject, LinkedObject aNextLinkedObject) {
            iCurrentObject = aNewObject;
            iNextLinkedObject = aNextLinkedObject;
        }
    
        Object getCurrentObject() {
            return iCurrentObject;
        }
    
        LinkedObject getNextLinkedObject() {
            return iNextLinkedObject;
        }
    }
    
    public class BlockingQueue {
        private LinkedObject iLinkedListContainer = null;
        private Object iQueueLock = new Object();
        private int iBlockedThreadCount = 0;
    
        public void appendObject(Object aNewObject) {
            synchronized(iQueueLock) {
                iLinkedListContainer = new iLinkedListContainer(aNewObject, iLinkedListContainer);
                if(iBlockedThreadCount > 0) {
                    iQueueLock.notify();//one at a time because we only appended one object
                }
            } //synchonized(iQueueLock)
        }
    
        public Object getFirstObject() throws InterruptedException {
            Object result = null;
            synchronized(iQueueLock) {
                if(null == iLinkedListContainer) {
                    ++iBlockedThreadCount;
                    try {
                        iQueueLock.wait();
                        --iBlockedThreadCount; // instead of having a "finally" statement
                    } catch (InterruptedException iex) {
                        --iBlockedThreadCount;
                        throw iex;
                    }
                }
                result = iLinkedListcontainer.getCurrentObject();
                iLinkedListContainer = iLinkedListContainer.getNextLinkedObject();
                if((iBlockedThreadCount > 0)  && (null != iLinkedListContainer )) {
                    iQueueLock.notify();
                }
            }//synchronized(iQueueLock)
            return result;
        }
    
    }
    

    我认为,如果您尝试在同步块中放入更少的代码,则该类将不再正确。

    【讨论】:

    • 不错的一个。但是如果消费者线程被中断会发生什么?会终止吗?线程是否应该在其run 方法中处理InterruptedException
    • 这绝对是一种处理中断的方法,因为它可能是由于 MIDlet 在其线程之一被阻塞时被破坏而引起的。
    【解决方案4】:

    这种方法似乎存在一些问题。即使队列中有元素,消费者也可能会错过通知并在队列中等待。 按时间顺序考虑以下序列

    T1 - Consumer 获取 queueLock,然后调用 wait。 Wait 会释放锁并导致线程等待通知

    T2 - 一个生产者获取 queueLock 并将一个元素添加到队列并调用 notify

    T3 - 通知消费者线程并尝试获取 queueLock 但失败,因为另一个生产者同时到来。 (来自 notify java doc - 被唤醒的线程将以通常的方式与可能积极竞争以同步该对象的任何其他线程竞争;例如,被唤醒的线程在成为下一个要锁定的线程时没有可靠的特权或劣势这个对象。)

    T4 - 第二个生产者现在添加另一个元素并调用 notify。由于消费者正在等待 queueLock,因此该通知丢失。

    因此从理论上讲,消费者可能会饿死(永远卡在尝试获取 queueLock),您也可能会遇到内存问题,多个生产者将元素添加到队列中,而这些元素没有被读取并从队列中删除。

    我建议的一些更改如下 -

    • 保持可添加到队列中的项目数的上限。
    • 确保消费者始终阅读所有元素。 Here 是一个程序,它展示了如何对生产者 - 消费者问题进行编码。

    【讨论】:

    • 在T4中,第二个producer添加元素,调用notify并释放锁。然后,消费者(在收到通知后被阻止)再次获得queueLock,它恢复并继续。我认为这里没有问题。
    • 理论上消费者可能会饿死,对吧?如果生产者持续流入,它可能会永远陷入等待锁定的状态。
    • 从昨天开始我就一直在考虑这个问题,现在我明白你有一个观点。但我无法想象有什么替代方法可以阻止它(我不会接受调整优先级作为解决方案)。
    • 我的答案中的链接显示了一种防止这种情况的方法。除了等待通知之外,它们还使用布尔值来确保生产者不会超出消费者。我能想到的另一个选择是限制队列的大小,因此如果生产者发现队列已满,它将等待队列耗尽后再添加。这具有确保应用程序的内存使用保持可预测的额外优势,并且还使单个消费者有机会跟上生产者的步伐。
    • 另一方面,反对票令人沮丧,指出不那么明显的问题肯定不是错的吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-04-19
    • 1970-01-01
    • 1970-01-01
    • 2014-10-16
    • 2018-05-01
    • 2013-12-05
    • 2020-09-26
    相关资源
    最近更新 更多