许多实际应用要求服务器具有同时为多个客户提供服务的能力。HTTP服务器就是最明显的例子。任何时刻,HTTP服务器都可能接收到大量的客户请求,每个客户都希望能快速得到HTTP服务器的响应。如果长时间让客户等待,会使网站失去信誉,从而降低访问量。

可以用并发性能来衡量一个服务器同时响应多个客户的能力。一个具有好的并发性能的服务器,必须符合两个条件:

能同时接收并处理多个客户连接;

对于每个客户,都会迅速给予响应。

用多个线程来同时为多个客户提供服务,这是提高服务器的并发性能的最常用的手段。

以下将按照3中方式来实现EchoServer,它们都使用多线程。

为每个客户分配一个工作线程。

创建一个线程池,由其中的工作线程来为客户服务。

利用JDK的Java类库中现成的线程池,由它的工作线程来为客户服务。

2.1、 为每个客户分配一个线程

服务器的主线程负责接收客户的连接,每次接收到一个客户连接,就会创建一个工作线程,由它负责与客户的通信。

代码示例一:

public static voidstart(){try{
ServerSocket serverSocket= newServerSocket(PORT);
System.out.println("server listen on port:" +PORT);while (true){try{
Socket client=serverSocket.accept();
System.out.println("receive client connect, localPort=" +client.getPort());new Thread(newEchoServer.HandlerServer(client)).start();
}catch(Exception e){
System.out.println("client exception,e=" +e.getMessage());
}
}
}catch(Exception e){
System.out.println("server exception,e=" +e.getMessage());
}
}

以上工作线程执行HandlerServer的run()方法,其负责与单个客户端通信,通信完毕后断开连接,线程自然终止。

代码示例二:

当Server每接受到一个Client连接请求之后,都把处理流程放到一个独立的线程里去运行,然后等待下一个Client连接请求,这样就不会阻塞Server端接收请求了。每个独立运行的程序在使用完Socket对象之后要将其关闭。这样就实现了多线程socket通信。

防止阻塞如果不用线程,你在主线程中侦听端口,那么你会发现,你如果不连接上的话,就会一直在等待,什么也做不了。

服务端:

package com.asd.reserve.utils.socket.demo2;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;public classServerLogin {public static voidmain(String[] args) throws IOException, ClassNotFoundException {
System.out.println("====服务器开启====");//【1】创建ServerSocket对象
ServerSocket ss=new ServerSocket(9999);while(true){//while实现服务器的一直开启//【2】监听是否有客户端发送请求
Socket client=ss.accept();/**System.out.println(client.getInetAddress().getHostAddress()+"的客户端请求登录");
//System.out.println(client.get"");
//【3】获取客户端发送过来的User对象
ObjectInputStream ois=new ObjectInputStream(client.getInputStream());
User user=(User)ois.readObject();
//【4】验证用户名是否正确
String result=user.getName().equals("zs")&&user.getPwp().equals("z123s")?"登录成功":"失败";
//【5】将结果相应给客户端
DataOutputStream dos=new DataOutputStream(client.getOutputStream());
dos.writeUTF(result);
//【6】关闭流
dos.close();
ois.close();
client.close();*/
//【3】创建线程类对象
ServerRunnable sr=newServerRunnable(client);//【4】启动线程
newThread(sr).start();
}
}
}
package com.asd.reserve.utils.socket.demo2;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;public classServerRunnable implements Runnable{privateSocket client;publicServerRunnable(Socket client) {this.client =client;
}
@Overridepublic voidrun() {
ObjectInputStream ois=null;
DataOutputStream dos=null;//TODO Auto-generated method stub
System.out.println(client.getInetAddress().getHostAddress()+"的客户端请求登录");try{
ois=newObjectInputStream(client.getInputStream());
User user=(User)ois.readObject();
String result=user.getName().equals("zs")&&user.getPwp().equals("z123s")?"登录成功":"失败";
dos=newDataOutputStream(client.getOutputStream());
dos.writeUTF(result);
}catch(ClassNotFoundException e) {//TODO Auto-generated catch block
e.printStackTrace();
}catch(IOException e) {//TODO Auto-generated catch block
e.printStackTrace();
}finally{try{
dos.close();
ois.close();
client.close();//不关闭ss是为了实现多线程
} catch(IOException e) {//TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
package com.asd.reserve.utils.socket.demo2;
import java.io.Serializable;public classUser implements Serializable{privateString name;privateString pwp;private static final long serialVersionUID = 1L;publicString getName() {returnname;
}public voidsetName(String name) {this.name =name;
}publicString getPwp() {returnpwp;
}public voidsetPwp(String pwp) {this.pwp =pwp;
}publicUser(String name, String pwp) {
super();this.name =name;this.pwp =pwp;
}publicUser() {
super();
}
}

客户端:

packagecom.asd.reserve.utils.socket.demo2;importjava.io.DataInputStream;importjava.io.IOException;importjava.io.ObjectOutputStream;importjava.net.Socket;importjava.util.Scanner;public classClient {public static void main(String[] args) throwsIOException {
Scanner sc=newScanner(System.in);//【1】输入对象信息
System.out.println("请输入用户名:");
String name=sc.next();
System.out.println("请输入密码:");
String pwp=sc.next();//【2】封装成对象
User u=newUser(name,pwp);//【3】向服务器端发送登录的请求
Socket client=new Socket("10.129.206.99",9999);//服务器端的IP地址,服务器的接收端口//获取输出流
ObjectOutputStream oos=newObjectOutputStream(client.getOutputStream());
oos.writeObject(u);//写入对象,转成Object类型//【4】接收服务器端的相应结果
DataInputStream dis=newDataInputStream(client.getInputStream());
System.out.println(dis.readUTF());//关闭流
dis.close();
oos.close();
client.close();
}
}packagecom.asd.reserve.utils.socket.demo2;importjava.io.Serializable;public class User implementsSerializable{privateString name;privateString pwp;private static final long serialVersionUID = 1L;publicString getName() {returnname;
}public voidsetName(String name) {this.name =name;
}publicString getPwp() {returnpwp;
}public voidsetPwp(String pwp) {this.pwp =pwp;
}publicUser(String name, String pwp) {super();this.name =name;this.pwp =pwp;
}publicUser() {super();
}
}

2.2、创建线程池

对每个客户都分配一个新的工作线程。当工作线程与客户通信结束,这个线程就被销毁。这种实现方式有以下不足之处:

服务器创建和销毁工作线程的开销(包括所花费的时间和系统资源)很大。如果服务器需要与许多客户通信,并且与每个客户的通信时间都很短,那么有可能服务器为客户创建新线程的开销比实际与客户通信的开销还要大。

除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。每个线程本身都会占用一定的内存(每个线程需要大约1M内存),如果同时有大量客户连接服务器,就必须创建大量工作线程,它们消耗了大量内存,可能会导致系统的内存空间不足。

如果线程数目固定,并且每个线程都有很长的生命周期,那么线程切换也是相对固定的。不同操作系统有不同的切换周期,一般在20毫秒左右。这里所说的线程切换是指在Java虚拟机,以及底层操作系统的调度下,线程之间转让CPU的使用权。如果频繁创建和销毁线程,那么将导致频繁地切换线程,因为一个线程被销毁后,必然要把CPU转让给另一个已经就绪的线程,使该线程获得运行机会。在这种情况下,线程之间的切换不再遵循系统的固定切换周期,切换线程的开销甚至比创建及销毁线程的开销还大。

线程池为线程生命周期开销问题和系统资源不足问题提供了解决方案。线程池中预先创建了一些工作线程,它们不断从工作队列中取出任务,然后执行该任务。当工作线程执行完一个任务时,就会继续执行工作队列中的下一个任务。线程池具有以下优点

减少了创建和销毁线程的次数,每个工作线程都可以一直被重用,能执行多个任务。

可以根据系统的承载能力,方便地调整线程池中线程的数目,防止因为消耗过量系统资源而导致系统崩溃。

2.3、使用JDK类库提供的线程池

java.util.concurrent包提供了现成的线程池的实现,其比自己实现的线程池更加健壮,且功能也更加强大。

java高并发socket服务 java socket并发_java socket实现交互

Executor接口继承图.png

Executor接口表示线程池,它的execute(Runnable task)方法用来执行Runnable类型的任务。Executor的子接口ExecutorService中声明了管理线程池的一些方法,比如用于关闭线程池的shutdown()方法等。Executors类中包含一些静态方法,它们负责生成各种类型的线程池ExecutorService实例。

java高并发socket服务 java socket并发_服务器_02

Executors类的静态方法.png

2.4、使用线程池注意事项

虽然线程池能大大提高服务器的并发性能,但使用它也会存在一定风险。与所有多线程应用程序一样,用线程池构建的应用程序容易产生各种并发问题,如对共享资源的竞争和死锁。此外,如果线程池本身的实现不健壮,或者没有合理地使用线程池,还容易导致与线程池有关的死锁、系统资源不足和线程泄漏等问题。

2.4.1、死锁

任何多线程应用程序都有死锁风险。造成死锁的最简单的情形是,线程A持有对象X的锁,并且在等待对象Y的锁,而线程B持有对象Y的锁,并且在等待对象X的锁。线程A与线程B都不释放自己持有的锁,并且等待对方的锁,这就导致两个线程永远等待下去,死锁就这样产生了。

虽然任何多线程程序都有死锁的风险,但线程池还会导致另外一种死锁。在这种情形下,假定线程池中的所有工作线程都在执行各自任务时被阻塞,它们都在等待某个任务A的执行结果。而任务A依然在工作队列中,由于没有空闲线程,使得任务A一直不能被执行。这使得线程池中的所有工作线程都永远阻塞下去,死锁就这样产生了。

2.4.2、系统资源不足

如果线程池中的线程数目非常多,这些线程会消耗包括内存和其他系统资源在内的大量资源,从而严重影响系统性能。

2.4.3.并发错误

线程池的工作队列依靠wait()和notify()方法来使工作线程及时取得任务,但这两个方法都难于使用。

如果编码不正确,可能会丢失通知,导致工作线程一直保持空闲状态,无视工作队列中需要处理的任务。因此使用这些方法时,必须格外小心,即便是专家也可能在这方面出错。最好使用现有的、比较成熟的线程池。例如,直接使用java.util.concurrent包中的线程池类。

2.4.4.线程泄漏

使用线程池的一个严重风险是线程泄漏。对于工作线程数目固定的线程池,如果工作线程在执行任务时抛出RuntimeException 或Error,并且这些异常或错误没有被捕获,那么这个工作线程就会异常终止,使得线程池永久失去了一个工作线程。如果所有的工作线程都异常终止,线程池就最终变为空,没有任何可用的工作线程来处理任务。

导致线程泄漏的另一种情形是,工作线程在执行一个任务时被阻塞,如等待用户的输入数据,但是由于用户一直不输入数据(可能是因为用户走开了),导致这个工作线程一直被阻塞。这样的工作线程名存实亡,它实际上不执行任何任务了。假如线程池中所有的工作线程都处于这样的阻塞状态,那么线程池就无法处理新加入的任务了。

2.4.5.任务过载

当工作队列中有大量排队等候执行的任务时,这些任务本身可能会消耗太多的系统资源而引起系统资源缺乏。

综上所述,线程池可能会带来种种风险,为了尽可能避免它们,使用线程池时需要遵循以下原则。

(1)如果任务A在执行过程中需要同步等待任务B的执行结果,那么任务A不适合加入到线程池的工作队列中。如果把像任务A一样的需要等待其他任务执行结果的任务加入到工作队列中,可能会导致线程池的死锁。

(2)如果执行某个任务时可能会阻塞,并且是长时间的阻塞,则应该设定超时时间,避免工作线程永久的阻塞下去而导致线程泄漏。在服务器程序中,当线程等待客户连接,或者等待客户发送的数据时,都可能会阻塞。可以通过以下方式设定超时时间:

◆调用ServerSocket的setSoTimeout(int timeout)方法,设定等待客户连接的超时时间;

◆对于每个与客户连接的Socket,调用该Socket的setSoTimeout(int timeout)方法,设定等待客户发送数据的超时时间。

(3)了解任务的特点,分析任务是执行经常会阻塞的I/O操作,还是执行一直不会阻塞的运算操作。前者时断时续地占用CPU,而后者对CPU具有更高的利用率。预计完成任务大概需要多长时间?是短时间任务还是长时间任务?

根据任务的特点,对任务进行分类,然后把不同类型的任务分别加入到不同线程池的工作队列中,这样可以根据任务的特点,分别调整每个线程池。

(4)调整线程池的大小。线程池的最佳大小主要取决于系统的可用CPU的数目,以及工作队列中任务的特点。假如在一个具有 N 个CPU的系统上只有一个工作队列,并且其中全部是运算性质(不会阻塞)的任务,那么当线程池具有 N 或 N+1 个工作线程时,一般会获得最大的 CPU 利用率。

如果工作队列中包含会执行I/O操作并常常阻塞的任务,则要让线程池的大小超过可用CPU的数目,因为并不是所有工作线程都一直在工作。选择一个典型的任务,然后估计在执行这个任务的过程中,等待时间(WT)与实际占用CPU进行运算的时间(ST)之间的比例WT/ST。对于一个具有N个CPU的系统,需要设置大约N×(1+WT/ST)个线程来保证CPU得到充分利用。

当然,CPU利用率不是调整线程池大小过程中唯一要考虑的事项。随着线程池中工作线程数目的增长,还会碰到内存或者其他系统资源的限制,如套接字、打开的文件句柄或数据库连接数目等。要保证多线程消耗的系统资源在系统的承载范围之内。

(5)避免任务过载。服务器应根据系统的承载能力,限制客户并发连接的数目。当客户并发连接的数目超过了限制值,服务器可以拒绝连接请求,并友好地告知客户:服务器正忙,请稍后再试。