Java并发编程笔记(六):并发执行任务
大多数并发应用程序都是围绕“任务执行”来构造了,每一个HTTP请求,数据库CRUD等都可以算是一个“任务”。通常将一个大任务拆分成多个小任务,使用并发技术来提高小任务的执行效率,从而提高大任务的整体效率。执行任务的手段有很多,单线程串行执行,多线程并发执行,使用线程池提高线程复用率等等。
一、单线程串行执行
其实单线程是多线程的一种特殊情况,即线程只有一个。所以串行的执行任务也是可以的,而且还不用担心线程安全等常见的并发问题。但是这种方式的效率很低,往往是一个线程包办一切。例如在socket编程中,仅仅使用单线程的话,那么主线程接收到请求后,需要去处理这个请求,在处理请求的这段时间,主线程不能接受任何任务,使得系统的吞吐量非常低。Demo如下:
public class EasySocket {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8000);
while (true) {
Socket socket = serverSocket.accept();
doSomething(socket);
}
}
private static void doSomething(Socket socket) {
}
}
二、多线程并发执行任务
使用多线程处理任务比使用单线程处理任务的效率会高很多,多线程的最简单使用如下Demo所示:
public class EasySocket {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8000);
while (true) {
Socket socket = serverSocket.accept();
System.out.println("收到连接");
new Thread(() -> doSomething(socket));
}
}
private static void doSomething(Socket socket) {
}
}
比上述单线程仅仅多了一行代码,即
new Thread(() -> doSomething(socket));
但是吞吐量比单线程高了很多,但是这样简单粗暴的多线程也是有很严重的问题的,因为线程数是受到JVM和操作系统的控制的,不可能无限创建,而且如果并发量很大的时候会导致线程的频繁创建,创建线程的代价虽然比创建进程的代价低,但还是不小的开销的,开销不仅仅体现在内存上,还体现在时间上。故这种模式不适合高并发的环境下。
三、使用线程池提高线程复用率
Java5提供了Executor框架,这个框架最显著的特点就是线程池化,且是基于生产者-消费者模型实现的,提交任务的一方相当于生产者,处理任务的一方相当于消费者,所以Executor也具有程序解耦的特性,将任务提交和任务处理逻辑分离,各司其职。
Executor将生产者提交的任务添加到内部的wokeQueue阻塞队列,消费者(处理任务的线程)从阻塞队列里拿到任务并创建或者复用空闲worker(简单理解就是线程池中的线程)去处理任务。Worker这个类负责控制线程的状态,从而根据状态决定线程该做的事,workers负责持有这些work的引用,防止GC。
上面对Executor的描述很粗略,详细的还是得看看源码。
Executor的使用也非常简单,我们可以实现Executor接口,重写void execute(Runnable command)方法即可。用户可以在方法自己实现需要的逻辑。不过一般情况下,使用Java类库自带的一些实现类是不错的实践。下图是Executors里提供的一些工厂方法:
![](https://img.haomeiwen.com/i4880496/aaf4aea92189f451.png)
从名字不难看出其功能,不再赘述了。下面是一个简单的demo:
public class EasySocket {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8000);
ExecutorService service = Executors.newFixedThreadPool(4);
while (true) {
Socket socket = serverSocket.accept();
System.out.println("收到连接");
service.submit(() -> doSomething(socket));
}
}
private static void doSomething(Socket socket) {
}
}
Executor的生命周期
如果仅仅简单的创建一个ExecutorService ,提交任务,无论任务是否执行完毕,Java程序都不会停止(当然,断电等强制使进程关闭肯定可以停止程序)。Executor框架提供了平滑停止服务的方式。ExecutorService扩展了Executor接口,添加了控制Executor生命周期方法,例如shutdown(),isTerminated()等,所有方法如下图所示:
![](https://img.haomeiwen.com/i4880496/31bf78ed3e3c67bb.png)
需要注意的是shutdown()和shutdownNow()是不一样的,执行shutdown()会停止接受任务,后续到达的任务会有RejectedExecutionHandler来执行拒绝逻辑(拒绝逻辑在框架中也有几个实现,具体的参看源码即可),并等待所有任务都执行完毕后才关闭,shutdownNow()会粗暴的停止当前正在执行的任务,并取消还没有执行的任务,最后停止服务。
一般为了保证服务一定会停止,会在shutdown()后面调用awaitTermination()来保证即使shutdown()出问题了,程序也会在超时时间到的时候停止。
小结
为了高效的完成任务,多线程比单线程串行的程序效率和性能会高很多,线程池的技术提高了线程的复用率,使得创建和销毁线程的总开销大大降低,提升系统的整体性能,为了保证系统的健壮性,Executor框架提供了平滑关闭程序的功能。