在寫多線程程序的時(shí)候,你就像個(gè)經(jīng)理,手下有那么或多或少的職員,你負(fù)責(zé)協(xié)調(diào)職員之間的工作,如果你稍不留神,職員之間就陷入了相互等待的尷尬狀態(tài)。還好,大多數(shù)時(shí)候多線程都還在我們掌控之內(nèi),即便是遇到這樣的deadlock情況,我們也能夠去修正,但是有的時(shí)候生活就是那么不盡人意,特別是NIO這種你不能掌控的時(shí)候,且看下面的代碼:
/**
* @(#)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();
}
}
}
}
乍看之下,我們的代碼沒有問題,但是運(yùn)行之后你會(huì)發(fā)現(xiàn),這句System.out.println("can reach here?when pigs fly!");永遠(yuǎn)無法執(zhí)行,也就是說register()方法被阻塞了!Oh god bless,讓我們看看JavaDoc是怎么說的:
...
可在任意時(shí)間調(diào)用此方法。如果調(diào)用此方法的同時(shí)正在進(jìn)行另一個(gè)此方法或 configureBlocking 方法的調(diào)用,則在另一個(gè)操作完成前將首先阻塞該調(diào)用。然后此方法將在選擇器的鍵集上實(shí)現(xiàn)同步,因此如果調(diào)用此方法時(shí)并發(fā)地調(diào)用了涉及同一選擇器的另一個(gè)注冊(cè)或選擇操作,則可能阻塞此方法的調(diào)用。
...
看這句“可在任意時(shí)間調(diào)用此方法。”,也就是說我們調(diào)用的時(shí)間沒有任何限制,而阻塞的情況只會(huì)出現(xiàn)在“如果調(diào)用此方法的同時(shí)正在進(jìn)行另一個(gè)此方法或 configureBlocking 方法的調(diào)用”的情況下,即便是阻塞了,我相信“正在進(jìn)行另一個(gè)此方法或configureBlocking”也不會(huì)花掉太多的時(shí)間,況且這里沒有上面這樣的情況出現(xiàn)。那register()是被誰擋住了?或者是BUG?
我們來分析一下程序,程序有兩個(gè)線程主線程和Service線程,主線程啟動(dòng)后啟動(dòng)了Service線程,Service線程啟動(dòng)Selector然后Service線程陷入select()的阻塞中,同時(shí),主線程調(diào)用Service的addChannel()方法來添加一個(gè)SocketChannel,嗯,兩個(gè)線程之間唯一的聯(lián)系就是selector,看來要從selector尋找線索,很可惜,selector的實(shí)現(xiàn)沒有源代碼可查,不過可以肯定是channel的register()會(huì)調(diào)用selector的register(),雖然此時(shí)持有selector的Service線程被select()方法所阻塞,但是并不影響其他線程對(duì)其操作吧?那么,剩下的解釋就是Selector的select()方法和register()方法公用了一個(gè)鎖,select()方法阻塞住了,所以register()拿不到這個(gè)鎖了,那么這樣一來我們就只能保證讓select()或者register()不能同時(shí)調(diào)用或者register()調(diào)用的時(shí)候select()不持有這個(gè)鎖,也就是說我們要用Service線程自己來執(zhí)行addChannel()方法,所以改進(jìn)如下:
/**
* @(#)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();
}
}
}
新的代碼,我們?cè)赟ervice的線程提供了一個(gè)待處理Channel隊(duì)列,然后在添加一個(gè)SocketChannel到隊(duì)列中時(shí)喚醒這個(gè)selector,取消阻塞,然后在Service的循環(huán)中處理這個(gè)pendingChannel,這樣就避免這個(gè)Deadlock的發(fā)生了。當(dāng)然我們亦可以在那個(gè)代碼上將select的超時(shí)時(shí)間設(shè)置非常的短,然后讓兩個(gè)線程去競爭,這樣做有太多的不可控性,不推薦了。
posted on 2007-12-13 18:31
ruislan 閱讀(1349)
評(píng)論(3) 編輯 收藏