java socket用法(三)

5.3使用JDK类库提供的线程池

java.util.concurrent包提供了现成的线程池的实现

健壮,而且功能也更强大。如图3-4所示是线程池的类框图。
图3-4 JDK类库中的线程池的类框图
Executor 接口表示线程池,它的execute(Runnable task)方法用来执行Runnable 类
型的任务。Executor 的子接口ExecutorService 中声明了管理线程池的一些方法,比如
用于关闭线程池的shutdown()方法等。Executors 类中包含一些静态方法,它们负责生
成各种类型的线程池ExecutorService实例

package multithread3;
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
public class EchoServer {
private int port=8000;
private ServerSocket serverSocket;
private ExecutorService executorService; //线程池
private final int POOL_SIZE=4; //单个CPU时线程池中工作线程的数目
public EchoServer() throws IOException {
serverSocket = new ServerSocket(port);
//创建线程池
//Runtime 的availableProcessors()方法返回当前系统的CPU的数目
//系统的CPU越多,线程池中工作线程的数目也越多
executorService= Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * POOL_SIZE);
System.out.println("服务器启动");
}
public void service() {
while (true) {
Socket socket=null;
try {
socket = serverSocket.accept();
executorService.execute(new Handler(socket));
}catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String args[])throws IOException {
new EchoServer().service();
}
}
/** 负责与单个客户通信的任务,代码与3.6.1 节的例程3-5 的Handler类相同 */
class Handler implements Runnable{…}

 

在EchoServer 的构造方法中,调用Executors.newFixedThreadPool()创建了具有固
定工作线程数目的线程池。在EchoServer 的service()方法中,通过调用executor-
Service.execute()方法,把与客户通信的任务交给了ExecutorService线程池来执行。

 

使用线程池的注意事项

虽然线程池能大大提高服务器的并发性能,但使用它也会存在一定风险。与所有
多线程应用程序一样,用线程池构建的应用程序容易产生各种并发问题,如对共享资
源的竞争和死锁。此外,如果线程池本身的实现不健壮,或者没有合理地使用线程池,
还容易导致与线程池有关的死锁、系统资源不足和线程泄漏等问题。
1.死锁
任何多线程应用程序都有死锁风险。造成死锁的最简单的情形是,线程A持有对
象X 的锁,并且在等待对象Y 的锁,而线程B 持有对象Y 的锁,并且在等待对象X
的锁。线程A与线程B都不释放自己持有的锁,并且等待对方的锁,这就导致两个线
程永远等待下去,死锁就这样产生了。
虽然任何多线程程序都有死锁的风险,但线程池还会导致另外一种死锁。在这种情
形下,假定线程池中的所有工作线程都在执行各自任务时被阻塞,它们都在等待某个任
务A 的执行结果。而任务A 依然在工作队列中,由于没有空闲线程,使得任务A 一直
不能被执行。这使得线程池中的所有工作线程都永远阻塞下去,死锁就这样产生了。
2.系统资源不足
如果线程池中的线程数目非常多,这些线程会消耗包括内存和其他系统资源在内

的大量资源,从而严重影响系统性能。
3.并发错误
线程池的工作队列依靠wait()和notify()方法来使工作线程及时取得任务,但这两
个方法都难于使用。如果编码不正确,可能会丢失通知,导致工作线程一直保持空闲
状态,无视工作队列中需要处理的任务。因此使用这些方法时,必须格外小心,即便
是专家也可能在这方面出错。最好使用现有的、比较成熟的线程池。例如,直接使用
java.util.concurrent包中的线程池类。
4.线程泄漏
使用线程池的一个严重风险是线程泄漏。对于工作线程数目固定的线程池,如果
工作线程在执行任务时抛出 RuntimeException 或Error,并且这些异常或错误没有被
捕获,那么这个工作线程就会异常终止,使得线程池永久失去了一个工作线程。如果
所有的工作线程都异常终止,线程池就最终变为空,没有任何可用的工作线程来处理
任务。
导致线程泄漏的另一种情形是,工作线程在执行一个任务时被阻塞,如等待用户
的输入数据,但是由于用户一直不输入数据(可能是因为用户走开了),导致这个工作
线程一直被阻塞。这样的工作线程名存实亡,它实际上不执行任何任务了。假如线程
池中所有的工作线程都处于这样的阻塞状态,那么线程池就无法处理新加入的任务了。
5.任务过载

 

当工作队列中有大量排队等候执行的任务时,这些任务本身可能会消耗太多的系
统资源而引起系统资源缺乏。
综上所述,线程池可能会带来种种风险,为了尽可能避免它们,使用线程池时需
要遵循以下原则。
(1)如果任务A 在执行过程中需要同步等待任务B的执行结果,那么任务A 不
适合加入到线程池的工作队列中。如果把像任务A 一样的需要等待其他任务执行结果
的任务加入到工作队列中,可能会导致线程池的死锁。
(2)如果执行某个任务时可能会阻塞,并且是长时间的阻塞,则应该设定超时
时间,避免工作线程永久的阻塞下去而导致线程泄漏。在服务器程序中,当线程等待
客户连接,或者等待客户发送的数据时,都可能会阻塞。可以通过以下方式设定超时
时间:
l 调用ServerSocket的setSoTimeout(int timeout)方法,设定等待客户连接的超时
时间,参见本章3.5.1 节(SO_TIMEOUT选项);
l 对于每个与客户连接的Socket,调用该Socket的setSoTimeout(int timeout)方
法,设定等待客户发送数据的超时时间,参见本书第2 章的2.5.3 节
(SO_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)避免任务过载。服务器应根据系统的承载能力,限制客户并发连接的数目。
当客户并发连接的数目超过了限制值,服务器可以拒绝连接请求,并友好地告知客户:
服务器正忙,请稍后再试。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值