1.在写nio的例子时,服务端采用线程池处理请求,遇到一个full gc问题,下面给代码贴出来。
nioserver端代码
package com.nio.study;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 类NIOServer.java的实现描述:TODO 类实现描述
*
* @author macun 2015年11月29日 下午4:18:46
*/
public class NIOServer {
private static final int DEFAULT_PORT = 8888;
private static Selector selector;
private Object lock = new Object();
private Boolean isStart = false;
private ExecutorService threadPool = null;
public void init() throws IOException {
selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
ServerSocket ss = serverChannel.socket();
ss.bind(new InetSocketAddress(DEFAULT_PORT));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
threadPool = Executors.newFixedThreadPool(10);
// threadPool = new ThreadPoolExecutor(10, 10,
// 1000L, TimeUnit.MILLISECONDS,
// new ArrayBlockingQueue(100));
synchronized (lock) {
isStart = true;
}
System.out.println("nio server init successful.");
}
public void run() throws IOException {
synchronized (lock) {
if (!isStart) {
try {
init();
} catch (IOException e) {
throw new IOException("nio server init failed.");
}
}
}
while (true) {
selector.select();
Iterator iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey key = (SelectionKey) iterator.next();
iterator.remove();
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
SelectionKey clientKey = client.register(selector, SelectionKey.OP_READ);
ByteBuffer buffer = ByteBuffer.allocate(100);
clientKey.attach(buffer);
}
if (key.isReadable()) {
NIOHandler handler = new NIOHandler(key);
threadPool.execute(handler);
}
}
}
}
}
NIOHandler代码
package com.nio.study;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
/**
* 类NIOHandler.java的实现描述:TODO 类实现描述
*
* @author macun 2015年11月29日 下午4:40:35
*/
public class NIOHandler implements Runnable {
private SelectionKey key;
public NIOHandler(SelectionKey key){
this.key = key;
}
@Override
public void run() {
System.out.println("handler:"+key);
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
try {
int n = client.read(buffer);
String temp = null;
if (n > 0) {
temp = new String(buffer.array());
System.out.println(temp);
buffer.clear();
buffer.put(temp.getBytes());
buffer.flip();
client.write(buffer);
buffer.clear();
}
// key.cancel();
// client.close();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (IOException e) {
try {
client.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
System.out.println(e.getMessage());
}
}
public void setSelectionKey(SelectionKey key) {
this.key = key;
}
}
Server 启动测试类
public class NIOServerTest {
public static void main(String[] args){
NIOServer server = new NIOServer();
try {
server.run();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
client端10个socket请求
public class NIOClient1 {
public static final String IP_ADDR = "localhost"; // ip adress
public static final int PORT = 8888; // listen port
public static void main(String[] args) throws Exception {
System.out.println("client start .....");
int i = 1;
Socket socket = null;
for (int j = 0; j < 20; j++) {
try {
socket = new Socket(IP_ADDR, PORT);
DataInputStream input = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
String str = "send " + i + " message.";
i++;
out.writeUTF(str);
String ret = input.readUTF();
System.out.println("from server:" + ret);
socket.close();
// Thread.sleep(1000);
} catch (Exception e) {
System.out.println("client " + e.getMessage());
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
socket = null;
System.out.println("client finally " + e.getMessage());
}
}
}
}
}
}
2.在没有用线程池的时候,发现正常;当用线程池处理的时候,发现程序卡死了。一看cpu使用率超过300%,再看full gc一直释放不掉垃圾。
问题现象:full gc,并且一直释放不掉垃圾。
分析:
1.在Handler处理逻辑部分,Thread.sleep(1000),让其休眠1秒钟。
2.线程池是由Executors工厂生成的10个固定大小的线程池和无界队列。
3.Selector监听是无条件循环的。
由以上3个条件猜测是10个固定大小的线程池在等待1秒的时候,Selector无条件循环接受到的SelectionKey放入到无界队列里,导致队列爆满,并且这些又是强引用,fullgc无法释放,导致越积越多,程序卡死。
猜测验证,把无界队列换成有界队列ArrayBlockingQueue,大小1000,很快就会抛出异常,包队列大小不够。
解决方法:
1.无界队列改成有界队列
2.因为selector是无条件循环,当SelectionKey的channel读写没有完成,它会一直取出来。handler里sleep(1000)的时候,它的SelectionKey仍然会被NIOServer 里的selector读取,那么就需要保持SelectionKey不被重复读取。
3.在handler完成后,需要把SelectionKey 从Holder里remove,并cancel掉。
基于以上3点,针对 NIOServer和Handler类进行改造,代码如下:
NIOServer代码片段更改如下
// threadPool = Executors.newFixedThreadPool(10);
threadPool = new ThreadPoolExecutor(10, 10,
1000L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue(100));
SelectionKeyHolder保持SelectionKey
if (key.isReadable()) {
if(SelectionKeyHolder.isContainKey(key)){
continue;
}
SelectionKeyHolder.put(key);
System.out.println(key);
NIOHandler handler = new NIOHandler(key);
threadPool.execute(handler);
SelectionKeyHolder类,代码片段如下:
import java.nio.channels.SelectionKey;
import java.util.HashSet;
/**
* 类SelectionKeyHolder.java的实现描述:TODO 类实现描述
*
* @author macun 2015年11月30日 下午12:21:26
*/
public class SelectionKeyHolder {
private static HashSet keySet = new HashSet();
private static Object lock = new Object();
public static void put(SelectionKey key) {
synchronized (lock) {
keySet.add(key);
}
}
public static boolean isContainKey(SelectionKey key) {
return keySet.contains(key);
}
public static void putIfAbsent(SelectionKey key) {
if (keySet.contains(key)) {
return;
}
put(key);
}
public static void remove(SelectionKey key) {
synchronized (lock) {
keySet.remove(key);
}
}
}
Handler类改造
public void run() {
System.out.println("handler:"+key);
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
try {
int n = client.read(buffer);
String temp = null;
if (n > 0) {
temp = new String(buffer.array());
System.out.println(temp);
buffer.clear();
buffer.put(temp.getBytes());
buffer.flip();
client.write(buffer);
buffer.clear();
}
SelectionKeyHolder.remove(key);
key.cancel();
// client.close();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (IOException e) {
try {
client.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
System.out.println(e.getMessage());
}
}
通过以上几个重构点,就可以解决以上nio服务端多线程full gc的问题。但是对于使用HashSet保持SelectionKey并不是最优的,可以替换成CurrentHashMap。