JAVA中的线程池

2019-10-14  本文已影响0人  刻骨铭心_17d7

一、为什么要使用线程池

构建服务器应用的简单模型是:每当一个请求到达就创建一个新线程,在新线程中请求服务。在原型开发这种方法工作得很好如果部署以这种方式运行的服务器应用程序,这种方法存在严重不足之一是:服务器应用程序中单任务处理的时间短,请求数大,而每当有一个新请求就为其创建一个新线程,请求处理结束后还要负责销毁线程,这大大增加了系统在创建和销毁线程上时间的花费,还消耗了系统资源,往往比处理用户实际请求的时间和资源更多。
线程池为线程生命开销问题和资源不足问题提供了解决方案。多个任务重用线程,线程创建的开销被分配到了多个任务上。当请求到达时线程已经存在,消除了线程创建带来的延迟。而且通过适当调整线程池中线程数量,当请求超过阈值时,强制其他新到的请求等待,直到获得一个线程来处理,防止资源不足

二、线程池的种类及区别

ThreadPoolExecutor类属性

public ThreadPoolExecutor(int var1, int var2, long var3, TimeUnit var5, BlockingQueue<Runnable> var6, ThreadFactory var7, RejectedExecutionHandler var8) {
        this.ctl = new AtomicInteger(ctlOf(-536870912, 0));
        this.mainLock = new ReentrantLock();
        this.workers = new HashSet();
        this.termination = this.mainLock.newCondition();
        if (var1 >= 0 && var2 > 0 && var2 >= var1 && var3 >= 0L) {
            if (var6 != null && var7 != null && var8 != null) {
                this.acc = System.getSecurityManager() == null ? null : AccessController.getContext();
                this.corePoolSize = var1;
                this.maximumPoolSize = var2;
                this.workQueue = var6;
                this.keepAliveTime = var5.toNanos(var3);
                this.threadFactory = var7;
                this.handler = var8;
            } else {
                throw new NullPointerException();
            }
        } else {
            throw new IllegalArgumentException();
        }
    }

corePoolSize:核心线程数;
maximumPoolSize:最大线程数,线程中中允许存在的最大线程数;
keepAliveTime:线程存活时间,超过核心线程数的线程,当线程处理空闲状态下,且维持时间达到keepAliveTime时,线程被销毁;
unit:keepAliveTime的时间单位
workQuene:工作队列,用于存放待执行的线程任务
threadFactory:创建线程的工厂,用于标记区分不同线程池创建出来的线程;
handler:当达到线程数上限或工作队列已满时的处理逻辑;

线程执行策略

如果运行线程少于corePoolSize,则会创建一个新线程处理请求,不将其添加到队列中
如果线程数达到corePoolSize则将新的请求放入队列中,若请求无法加入队列,且线程数未达到maximumPoolSize则创建新线程,否则任务将被拒绝。

BlockingQueue类型

无界队列

队列大小无限制,常用的为无界的LinkedBlockingQueue,使用该队列做为阻塞队列时要尤其当心,当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM。最近工作中就遇到因为采用LinkedBlockingQueue作为阻塞队列,部分任务耗时80s+且不停有新任务进来,导致cpu和内存飙升服务器挂掉。

有界队列

常用的有两类,一类是遵循FIFO原则的队列如ArrayBlockingQueue与有界的LinkedBlockingQueue,另一类是优先级队列如PriorityBlockingQueue。PriorityBlockingQueue中的优先级由任务的Comparator决定。
使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量。

同步移交

如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。

1.固定大小线程池newFixedThreadPool

public class Main {

    public static void main(String[] args) {

        ExecutorService pool = Executors.newFixedThreadPool(2);

        Thread t1 = new Thread(new MyThread(1));
        Thread t2 = new Thread(new MyThread(2));
        Thread t3 = new Thread(new MyThread(3));
        Thread t4 = new Thread(new MyThread(4));

        pool.execute(t1);
        pool.execute(t2);
        pool.execute(t3);
        pool.execute(t4);

        pool.shutdown();
    }

}
class MyThread implements Runnable{

    int count;

    MyThread(int i) {
        this.count = i;
    }

    public void run() {

        while (true) {
            try {
                Thread.sleep(10);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("ThreadName = " + count + "----");
        }

    }

}

执行结果:


image.png

将Executors.newFixedThreadPool(2)改成Executors.newFixedThreadPool(5)后执行结果:


image.png
可以看出该方法指定运行线程最大数目,超过这个数目线程加进去不会执行,且贤臣运行顺序不受加入顺序影响。

2.单任务线程池,newSingleThreadExecutor

仅仅是把上述代码中的ExecutorService pool = Executors.newFixedThreadPool(2)改为ExecutorService pool = Executors.newSingleThreadExecutor();
输出结果:


image.png

可以看出该线程池按线程加入顺序执行,哪怕当前线程休眠也不会跳到下一个线程执行

3.可变尺寸线程池,newCachedThreadPool

与上面的类似,只是改动下pool的创建方式:ExecutorService pool = Executors.newCachedThreadPool();
执行结果:


image.png

可根据需要创建新线程,当线程阻塞时会跳到其他线程执行

三、线程池使用注意

谨慎使用Executors创建线程

Executors是java并发包提供的,用于快速创建不同类型线程池。
Executors包中部分源码:

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue());
    }

    public static ExecutorService newCachedThreadPool(ThreadFactory var0) {
        return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), var0);
    }

使用Executors创建线程池时代码:

ExecutorService pool = Executors.newCachedThreadPool();

        Thread t1 = new Thread(new MyThread(1));
        Thread t2 = new Thread(new MyThread(2));
        Thread t3 = new Thread(new MyThread(3));
        Thread t4 = new Thread(new MyThread(4));

        pool.execute(t1);
        pool.execute(t2);
        pool.execute(t3);
        pool.execute(t4);

        pool.shutdown();

这种方法在开发个人或临时项目时速度很快,几行代码就解决,但是在大型项目中是禁止使用的。
通过上面的源码可以直观地看到,它是自动地为ThreadPoolExecutor指定参数,Executors创建线程池时,使用的是无边界队列SynchronousQueue,不断加入任务会出现内存溢出问题。

参考资料

Java 四种线程池的用法分析
JAVA 线程池的正确打开方式

上一篇下一篇

猜你喜欢

热点阅读