一、线程池的用途

线程池主要是为了解决以下几个问题:

1、为多线程执行琐碎的任务提供支持

2、避免线程创建销毁过程的资源消耗

3、控制资源的使用,根据实际情况合理高效利用资源

4、

二、java提供的线程池结构

java jdk中提供了线程池的创建接口,已经可以很方便对线程的管理和使用了。

Java 线程池限流 java线程池threadpool_java

上图是线程池和调度线程的UML图

1、创建线程就是用ThreadPoolExecutor中实现的,下面是构造方法

ThreadPoolExecutor(int corePoolSize,
                   int maximumPoolSize,
                   long keepAliveTime,
                   TimeUnit unit,
                   BlockingQueue<Runnable> workQueue,
                   ThreadFactory threadFactory,
                   RejectedExecutionHandler handler)

最后两个参数是可选的,都有默认值。

ThreadFactory参数建议带上,可以设置线程的名字等信息,已经有很多开源的实现的可以直接用,Apache的common-lang3和Google的guava两个都是非常可靠地开源库。

RejectedExecutionHandler 参数是用来处理线程添加异常的,可以自己处理异常信息。

BlockingQueue是用来放线程的阻塞队列,阻塞队列有很多种实现方式,ArrayBlockingQueue、LinkedBlockingQueue、DelayQueue、SynchronousQueue等,一般使用前两个就可以满足要求了。

创建完线程之后就可以将自己的线程交给线程池执行了,最好用callable的方式运行线程,可以有返回值,实际线程里面也是将runnable转成callable执行的。


2、工厂方法创建线程池

jdk中提供了一个Executors的类可以快速创建线程池,里面已经提供了四种类型的线程池可以选择。

1)newFixedThreadPool

创建固定size的线程池,同样用上面的测试程序,只是用这个方法创建线程池

public class ThreadTest {
    public static void main(String[] args) {
        ExecutorService fixed = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int index = i;
            fixed.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(index * 1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println(index+"当前线程ID"+Thread.currentThread().getId());
                }
            });
        }
    }
}

输出结果:

0当前线程pool-2-thread-1
1当前线程pool-2-thread-2
2当前线程pool-2-thread-3
3当前线程pool-2-thread-1
4当前线程pool-2-thread-2
5当前线程pool-2-thread-3
6当前线程pool-2-thread-1
7当前线程pool-2-thread-2
8当前线程pool-2-thread-3
9当前线程pool-2-thread-1

但是在每次输出结果中间都有时间间隔。

2)newCachedThreadPool

创建缓存线程池,这个是用SynchronousQueue来做缓冲队列的,这个队列可以叫同步队列里面最多只能有一个元素,缓存线程池就是每次都会从队列中取线程,如果没有了就会新创建一个,最大是int最大值,所以使用这个方式是有风险的,有可能会出现线程数非常大的问题。

public class ThreadTest {
    public static void main(String[] args) {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            cachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(index * 100);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println(index+"当前线程ID"+Thread.currentThread().getId());
                }
            });
        }
    }
}

上面的测试程序每次都会创建一个新线程,因为上一个还在sleep中呢,所以运行结果是:

0当前线程pool-1-thread-1
1当前线程pool-1-thread-2
2当前线程pool-1-thread-3
3当前线程pool-1-thread-4
4当前线程pool-1-thread-5
5当前线程pool-1-thread-6
6当前线程pool-1-thread-7
7当前线程pool-1-thread-8
8当前线程pool-1-thread-9
9当前线程pool-1-thread-10

3)newSingleThreadExecutor

这个从名字上应该就能看出来是单线程,单线程为什么还是线程池呢?因为这个单线程可以保证同一时间只有一个线程在跑,但是如果线程挂了会再起一个去执行任务,所以这个其实跟串行执行任务似的,只是他会保证不会因为一个任务影响其他任务的执行。

下面代码中有一个错误会引起exception,可以看到执行日志。

public static void main(String[] args) {
        ExecutorService singleService = Executors.newSingleThreadExecutor();

        try {
            for (int i = 0; i < 10; i++) {
                final int index = i;
                singleService.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(index * 1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(index+"当前线程ID"+Thread.currentThread().getId());
                        String str = null;
                        str.length();
                    }
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

0当前线程ID13
Exception in thread "pool-3-thread-1" java.lang.NullPointerException
	at com.watson.mytest.thread.ThreadTest$1.run(ThreadTest.java:37)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
1当前线程ID15
Exception in thread "pool-3-thread-2" java.lang.NullPointerException
	at com.watson.mytest.thread.ThreadTest$1.run(ThreadTest.java:37)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "pool-3-thread-3" java.lang.NullPointerException
2当前线程ID16
	at com.watson.mytest.thread.ThreadTest$1.run(ThreadTest.java:37)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
3当前线程ID17
Exception in thread "pool-3-thread-4" java.lang.NullPointerException
	at com.watson.mytest.thread.ThreadTest$1.run(ThreadTest.java:37)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "pool-3-thread-5" java.lang.NullPointerException
.
.
.
.

上面执行结果每次都有一个新的线程ID,如果把代码中错误去掉,日志中的ID就不会变了,可以自己试一下。

4)newScheduledThreadPool

这个是周期执行线程池,或者是创建一个延迟执行的线程池。他有四种执行方式:

//延迟执行Runnable类型线程
    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);
//延迟执行Callable类型线程
    public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay, TimeUnit unit);
//第一次执行延迟initialDelay,每次从开始执行开始计时间隔period再执行下一次,如果一次执行时间比period还长,当上一次执行完立即执行下一次,不会并行执行。
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);
//第一次执行延迟initialDelay,每次从执行结束计时间隔period再执行下一次。
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

 上一个例子,可以做个测试。

public static void main(String[] args) {
        ScheduledExecutorService scheduledService = Executors.newScheduledThreadPool(2);

        try {
            for (int i = 0; i < 10; i++) {
                final int index = i;
                scheduledService.schedule(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(index * 1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(index+"当前线程ID"+Thread.currentThread().getId());
                        String str = null;
                        str.length();
                    }
                }, 1, TimeUnit.SECONDS);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

三、优劣比较

最后在集中比较一下这四种快速创建线程池的优劣。

1、newFixedThreadPool和newSingleThreadExecutor这两个可能会出现阻塞队列中有大量线程等待被执行,甚至可能会OOM

2、newCachedThreadPool和newScheduledThreadPool这两个最大能创建的线程数是int最大值,有可能会创建非常多线程,甚至OOM。

在实际使用中这四种方式都有弊端,最好自己根据实际场景控制队列最大长度和线程池最大值,如果超过这个值说明系统存在问题需要优化了,而不至于让系统直接挂掉。自己实现线程池时必须要学习的就是java的阻塞队列,这又是一个新的知识以后慢慢学吧。

定长线程池可以根绝系统资源设置线程数,根据以前写c驱动的经验是设置成比机器资源少两个的值(如果cpu核少于4个就不用太在意了),因为系统自己的运行比如网络磁盘等设备的管理需要留一个,另一个是线程的切换需要留一个,因为多线程的线程切换也是需要资源的。Runtime.getRuntime().availableProcessors()

多线程是增加任务复杂度的怪兽,但也是挺高系统性能的精灵,怎么hold住这个怪物小精灵还是需要更多的学习和实践。

 

参考:

JDK1.8源码

commons-lang3-3.8.1