在寫多線程程序的時候,你就像個經理,手下有那么或多或少的職員,你負責協調職員之間的工作,如果你稍不留神,職員之間就陷入了相互等待的尷尬狀態。還好,大多數時候多線程都還在我們掌控之內,即便是遇到這樣的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.tkk7.com", 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.tkk7.com", 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 閱讀(1348)
評論(3) 編輯 收藏