在写多线程程序的时候,你就像个经理,手下有那么或多或少的职员,你负责协调职员之间的工作,如果你稍不留神,职员之间就陷入了相互等待的尴尬状态。还好,大多数时候多线程都还在我们掌控之内,即便是遇到这样的deadlock情况,我们也能够去修正,但是有的时候生活就是那么不尽人意,特别是NIO这种你不能掌控的时候,且看下面的代码:
/**
* @(#)DeadLock.java v0.1.0 2007-12-13
*/
package ruislan.rswing.test;
import java.net.InetSocketAddress;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.concurrent.Executors;
/**
* NIO DeadLock
*
* @author ruislan <a href="mailto:z17520@126.com"/>
* @version 0.1.0
*/
public class DeadLock {
public static void main(String[] args) throws Exception {
Service service = new Service();
Executors.newSingleThreadExecutor().execute(service);
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress("http://www.blogjava.net", 80));
service.addChannel(channel);
}
static class Service implements Runnable {
Selector selector;
public Service() {
}
public void run() {
try {
selector = Selector.open();
while (true) {
selector.select();
System.out.println(selector.selectedKeys().size());
}
} catch (Exception e) {
}
}
public void addChannel(SocketChannel channel) {
try {
channel.register(selector, SelectionKey.OP_CONNECT
| SelectionKey.OP_READ);
System.out.println("can reach here?when pigs fly!");
} catch (ClosedChannelException e) {
e.printStackTrace();
}
}
}
}
乍看之下,我们的代码没有问题,但是运行之后你会发现,这句System.out.println("can reach here?when pigs fly!");永远无法执行,也就是说register()方法被阻塞了!Oh god bless,让我们看看JavaDoc是怎么说的:
...
可在任意时间调用此方法。如果调用此方法的同时正在进行另一个此方法或 configureBlocking 方法的调用,则在另一个操作完成前将首先阻塞该调用。然后此方法将在选择器的键集上实现同步,因此如果调用此方法时并发地调用了涉及同一选择器的另一个注册或选择操作,则可能阻塞此方法的调用。
...
看这句“可在任意时间调用此方法。”,也就是说我们调用的时间没有任何限制,而阻塞的情况只会出现在“如果调用此方法的同时正在进行另一个此方法或 configureBlocking 方法的调用”的情况下,即便是阻塞了,我相信“正在进行另一个此方法或configureBlocking”也不会花掉太多的时间,况且这里没有上面这样的情况出现。那register()是被谁挡住了?或者是BUG?
我们来分析一下程序,程序有两个线程主线程和Service线程,主线程启动后启动了Service线程,Service线程启动Selector然后Service线程陷入select()的阻塞中,同时,主线程调用Service的addChannel()方法来添加一个SocketChannel,嗯,两个线程之间唯一的联系就是selector,看来要从selector寻找线索,很可惜,selector的实现没有源代码可查,不过可以肯定是channel的register()会调用selector的register(),虽然此时持有selector的Service线程被select()方法所阻塞,但是并不影响其他线程对其操作吧?那么,剩下的解释就是Selector的select()方法和register()方法公用了一个锁,select()方法阻塞住了,所以register()拿不到这个锁了,那么这样一来我们就只能保证让select()或者register()不能同时调用或者register()调用的时候select()不持有这个锁,也就是说我们要用Service线程自己来执行addChannel()方法,所以改进如下:
/**
* @(#)DeadLock.java v0.1.0 2007-12-13
*/
package ruislan.rswing.test;
import java.net.InetSocketAddress;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* NIO DeadLock
*
* @author ruislan <a href="mailto:z17520@126.com"/>
* @version 0.1.0
*/
public class DeadLock {
public static void main(String[] args) {
Service service = new Service();
new Thread(service).start();
for (int i = 0; i < 5; i++) {
new Thread(new ChannelAdder(service)).start();
}
}
static class ChannelAdder implements Runnable {
private Service service;
public ChannelAdder(Service service) {
this.service = service;
}
@Override
public void run() {
try {
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress(
"http://www.blogjava.net", 80));
service.addChannel(channel);
} catch (Exception e) {
e.printStackTrace();
}
}
}
static class Service implements Runnable {
private Selector selector;
private Queue<SocketChannel> pendingRegisters;
public Service() {
pendingRegisters = new LinkedBlockingQueue<SocketChannel>();
}
public void run() {
try {
selector = Selector.open();
while (true) {
selector.select();
System.out.println(selector.selectedKeys().size());
handlePendingRegisters();
}
} catch (Exception e) {
}
}
public void handlePendingRegisters() {
while (!pendingRegisters.isEmpty()) {
SocketChannel channel = pendingRegisters.poll();
try {
channel.register(selector, SelectionKey.OP_CONNECT);
System.out.println("can reach here?yeah!");
} catch (ClosedChannelException e) {
e.printStackTrace();
}
}
}
public void addChannel(SocketChannel channel) {
pendingRegisters.offer(channel);
selector.wakeup();
}
}
}
新的代码,我们在Service的线程提供了一个待处理Channel队列,然后在添加一个SocketChannel到队列中时唤醒这个selector,取消阻塞,然后在Service的循环中处理这个pendingChannel,这样就避免这个Deadlock的发生了。当然我们亦可以在那个代码上将select的超时时间设置非常的短,然后让两个线程去竞争,这样做有太多的不可控性,不推荐了。
posted on 2007-12-13 18:31
ruislan 阅读(1337)
评论(3) 编辑 收藏