Java 并发编程

Java并发编程笔记(六):并发执行任务

2018-07-02  本文已影响5人  yeonon

大多数并发应用程序都是围绕“任务执行”来构造了,每一个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里提供的一些工厂方法:


Executors工厂方法

从名字不难看出其功能,不再赘述了。下面是一个简单的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()等,所有方法如下图所示:

ExecutorService接口的提供的方法

需要注意的是shutdown()和shutdownNow()是不一样的,执行shutdown()会停止接受任务,后续到达的任务会有RejectedExecutionHandler来执行拒绝逻辑(拒绝逻辑在框架中也有几个实现,具体的参看源码即可),并等待所有任务都执行完毕后才关闭,shutdownNow()会粗暴的停止当前正在执行的任务,并取消还没有执行的任务,最后停止服务。

一般为了保证服务一定会停止,会在shutdown()后面调用awaitTermination()来保证即使shutdown()出问题了,程序也会在超时时间到的时候停止。

小结

为了高效的完成任务,多线程比单线程串行的程序效率和性能会高很多,线程池的技术提高了线程的复用率,使得创建和销毁线程的总开销大大降低,提升系统的整体性能,为了保证系统的健壮性,Executor框架提供了平滑关闭程序的功能。

上一篇 下一篇

猜你喜欢

热点阅读