Java在做服务器的时候,我们司空见惯的就是阻塞式的方式,也就是每当发生一次连接,就new 一个Thread出来,假如线程在读写,连接发生问题,线程则会一直阻塞,但是并不会消亡。所以随着线程数的增加,CPU的利用率会随之降低,因此我们应当采用非阻塞式的方式,能更好的解决问题。
看了一本书《Java网络编程精解》最近才大致的通读了一遍,这本书上面讲解的很详细,简单的将前面的非阻塞与阻塞的结合部分,代码做了一定的修改。并自己进行了一些抽象,测试通过没有提。
用一个AcceptThread采用阻塞的方式,来处理连接。
用一个RWThread采用非阻塞的方式,来处理读写问题。
这样既充分利用了多核心CPU的特性,两个线程做两个事情,也解决了开始提到的问题。
public class Application { //存在死锁风险,再认真考虑一下,虽然概率低一些。读写和接收线程问题!!!! public static void main(String[] args){ Server server = Server.getInstance(); //启动接收线程 new AcceptThread(server).start(); //启动读写线程 new RWThread(server).start(); } }
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; //使用单例模式,创建唯一的Selector public class Server { private static Server server; //选择器 public Selector selector; //创建serverSocket public ServerSocketChannel serverSocket; //端口号 private int port = 8086; private Server(){ try { //初始化Selector selector = Selector.open(); //初始化serverSocket serverSocket = ServerSocketChannel.open(); serverSocket.configureBlocking(false); //可以绑定到相同端口 serverSocket.socket().setReuseAddress(true); //绑定到某个地址上 serverSocket.socket().bind(new InetSocketAddress(port)); serverSocket.register(selector, SelectionKey.OP_ACCEPT); } catch (IOException e) { e.printStackTrace(); } } public static Server getInstance(){ if(server == null){ server = new Server(); } return server; } }
我们使用单例模式,创建ServerSocketChannel,获取唯一的Selector,因为Selector当中会将ServerSocketChannel,SocketChannel的key存储在Selector当中。(注:正因为两个线程分别一个接受要往Selector当中添加,另一个读写线程要从Selector当中获取SocketChannel,所以要防止死锁情况的出现)与此同时,我们还给ServerSocket注册了Accept事件
import java.io.IOException; 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; public class AcceptThread extends Thread{ private Selector selector; public AcceptThread(Server server){ selector = server.selector; } public void run() { try { while(true){ selector.select(); //阻塞 Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while(it.hasNext()){ SelectionKey keys = it.next(); //接收状态下的操作 if(keys.isAcceptable()){ //获取当前链接的SocketChannel ServerSocketChannel ssc = (ServerSocketChannel)keys.channel(); SocketChannel sc = ssc.accept(); if(sc!=null){ //非阻塞 sc.configureBlocking(false); synchronized(selector){ System.out.println("连接成功"); sc.register(selector,SelectionKey.OP_READ|SelectionKey.OP_WRITE); } } } } } } catch (IOException e) { e.printStackTrace(); } } }
我们通过便利Selector对象来获取Accept事件状态,从而获取SocketChannel,并且注册Write/Read事件我们会将SocketChannel注册到Selector当中,因此在注册事件的同时,我们要加锁。
刚开始,我们采用的是select()方法,这个方法是阻塞的,如果不发生连接此方法将一直阻塞下去。
import java.io.IOException; 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; public class RWThread extends Thread { //key集合类 private Selector selector; //BufferByte private ByteBuffer byteBuffer; public RWThread(Server server){ selector = server.selector; } @Override public void run() { while(true){ synchronized(selector){ try{ if(selector.selectNow()>0){ //非阻塞 System.out.println(selector.selectedKeys().size()); Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while(it.hasNext()){ SelectionKey keys = it.next(); //可读状态下的操作 if(keys.isReadable()){ } //可写状态下的操作 if(keys.isWritable()){ } try { Thread.sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } catch (IOException e) { e.printStackTrace(); } } } } }
这次读写selectNow()是非阻塞的,所以将会一直执行下去,我们为了不让CPU运行太高,因此加入了一个睡眠线程,从而降低CPU的使用。
后来想到的:
这样的效率并不是最好的,因为我们每一次it.next(),我们都会讲Selector当中注册时事件全部遍历下来,从而才会获取到相应的事件,并作出处理。因此将读写的Selector与接收的Accept事件分开,可能会好一些。也就是Accept当中注册读写事件的时候,将事件注册到一个新的Selector当中。单独遍历不同的Selector,这样也避免了死锁的存在问题才是。