我正在尝试在Java ME中实现一个简单的阻塞队列。在JavaME API中,Java SE的并发实用程序不可用,因此我必须像以前那样使用wait-notify。
这是我的临时实施。我正在使用notify
而不是notifyAll
,因为在我的项目中有多个生产者,但只有一个消费者。我故意使用一个对象进行wait-notify来提高可读性,尽管它浪费了一个引用:
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)
答案 0 :(得分:1)
Vector类不保证线程安全,你应该像你一样同步对它的访问。除非您有证据表明您当前的解决方案存在性能问题,否则我不会担心。
另外,我认为使用notifyAll
而不是notify
来支持多个消费者并没有什么坏处。
答案 1 :(得分:1)
synchronized
用于保护对共享状态的访问并确保原子性。
请注意Vector
的方法已经同步,因此Vector
会保护自己的共享状态。因此,只需要您的同步块来确保操作的原子性。
您当然无法在queue
方法的同步块中移动take()
上的操作,因为原子性对于该方法的正确性至关重要。但是,据我所知,你可以在put()
方法中从同步块中移动队列操作(我无法想象它出错的情况)。
但是,上面的推理纯粹是理论上的,因为在所有情况下你都有双重同步:queueLock
上的同步和Vector
的方法在queue
上隐式同步。 因此建议的优化没有意义,其正确性取决于双重同步的存在。
为避免双重同步,您还需要在queue
上同步:
synchronized (queue) { ... }
另一种选择是使用非同步集合(例如ArrayList
)而不是Vector
,但JavaME不支持它。在这种情况下,您将无法使用建议的优化,因为同步块也可以保护非同步集合的共享状态。
答案 2 :(得分:0)
除非您因垃圾收集而特别遇到性能问题,否则我宁愿使用链表而不是Vector来实现队列(先进先出)。
我还会编写在您的项目(或其他)获得多个消费者时可以重用的代码。虽然在这种情况下,您需要注意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;
}
}
我认为如果你尝试在同步块中放入更少的代码,那么该类将不再正确。
答案 3 :(得分:-2)
这种方法似乎存在一些问题。即使队列中有元素,您也可以拥有消费者可能会错过通知并等待队列的情况。 按时间顺序考虑以下顺序
T1 - Consumer获取queueLock然后调用wait。等待将释放锁并使线程等待通知
T2 - 一个生产者获取queueLock并向队列添加一个元素并调用notify
T3 - 通知Consumer线程并且尝试获取queueLock BUT失败,因为另一个生产者同时出现。 (来自notify java doc - 唤醒线程将以通常的方式与可能主动竞争同步此对象的任何其他线程竞争;例如,唤醒线程在下一个要锁定的线程中没有可靠的特权或劣势这个对象。)
T4 - 第二个生产者现在添加另一个元素并调用notify。当消费者在队列锁上等待时,此通知将丢失。
理论上,消费者可能会饿死(永远不会试图获取queueLock),也可能会遇到内存问题,多个生产者向队列中添加了未被队列读取和删除的元素。
我建议的一些更改如下 -