【JUC】——深入浅出搞懂线程池

        线程池一个并不陌生的概念,印象中的线程池经常使用,但是却不怎么了解原理。本文主要从线程出发,讲解线程池的使用,以及线程池的底层原理。

线程

创建线程的方式

  • 继承Thread类

  • 实现Runnable接口

  • 实现Callable接口通过FutureTask包装器来创建Thread线程

但是从本质上来讲,java中创建线程的方式只有一种,就是实现Runable接口,即所以的线程都实现了run()方法。

下面可以通过java中的UML图进行证明

先看个线程的使用demo

public class ThreadTest {

    public static class ThreadDemo extends Thread {
        private String name;

        public ThreadDemo(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println("调用方式:" + this.name + ",线程id:" + Thread.currentThread().getId() + ",线程name:" + Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        System.out.println("main线程id:" + Thread.currentThread().getId());
        //线程级别
        new ThreadDemo("start").start();
        //方法级别
        new ThreadDemo("run").run();
    }
}

执行结果

通过执行结果可以看出,调用start()和 run()方法执行的线程并不一致,start()是新创建的线程,而run()则和main()共用一个线程。它们之间线程的关系,如下图

1、线程有生命周期

2、调用start(),执行run() --》多线程

3、start()是线程级别,而单独调用run()是方法级别,等同于普通调用

线程池

public static void main(String[] args) throws InterruptedException {
        Long start = System.currentTimeMillis();
        final Random random = new Random();
        final List<Integer> list = new ArrayList<Integer>();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 100000; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    list.add(random.nextInt());
                }
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.DAYS);
        System.out.println("使用Executors.newSingleThreadExecutor 花费时间:"+(System.currentTimeMillis() - start));
        System.out.println("大小:"+list.size());

        System.out.println("==================");

        list.clear();
        for (int i = 0; i < 100000; i++) {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    list.add(random.nextInt());

                }
            };
            thread.start();
            thread.join();
        }
        System.out.println("使用thread时间:"+(System.currentTimeMillis() - start));
        System.out.println("大小:"+list.size());

    }

执行结果

 为什么两种方式执行结果相差这么大?

因为方式一使用的是线程池的方式,那么为何线程池?

        线程池 就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

两种处理方式时间相差较大的原因,本质上就是省去线程的创建时间,线程的复用,节省了大量的时间消耗。

几种常见的线程池使用

public class ThreadPoolDemo {
    public static void main(String[] args) {
        //固定大小的线程池,跟回收型线程池类似,只是可以限制同时运行的线程数量 执行慢
        ExecutorService executorService1 = Executors.newFixedThreadPool(10);
        //回收型线程池,可以重复利用之前创建过的线程,运行线程最大数是Integer.MAX_VALUE,快
        ExecutorService executorService2 = Executors.newCachedThreadPool();
        //单线程池,同时只有一个线程在跑,最慢
        ExecutorService executorService3 = Executors.newSingleThreadExecutor();
        //提供延迟和定时任务的线程池
        Executors.newScheduledThreadPool(10);
        for (int i = 0; i < 100; i++) {
            executorService1.execute(new MyTask(i));
        }
    }
}

class MyTask implements Runnable {
    int i = 0;

    public MyTask(int i) {
        this.i = i;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "--" + i);
        try {
            Thread.sleep(1000L);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果已经标注到注释中,就不放图单独展示了。

那么为什么每种线程池的执行效率又各不相同呢?

在ThreadPoolExecutor的创建过程涉及到以下几个属性值

int corePoolSize,核心线程
int maximumPoolSize,非核心线程
long keepAliveTime,时间
TimeUnit unit,时间单位
BlockingQueue<Runnable> workQueue,队列
ThreadFactory threadFactory,线程工厂
RejectedExecutionHandler handler 拒绝策略

先看线程池组成

核心线程在整个程序运行过程会一直存在,即使处于空闲。

非核心线程等于maximumPoolSize减corePoolSize,非核心线程在空闲状态中的存活时间取决于keepAliveTime单位秒,如果keepAliveTime为0,即无待执行任务后,非核心线程立刻结束生命周期。

队列用于存放尚未处理的任务

RejectedExecutionHandler拒绝策略,当核心线程,队列,和非核心线程都没有空间存放待处理任务时,则任务会被拒绝执行。ps:拒绝策略非本文核心,只做简单展示

下面结合demo来展示线程池中核心线程,非核心线程,队列的使用

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(2));
        for (int i = 0; i < 6; i++) {
            threadPoolExecutor.execute(new MyTask(i));
        }
    }
}

class MyTask implements Runnable {
    int i = 0;

    public MyTask(int i) {
        this.i = i;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "--" + i);
        try {
            Thread.sleep(1000L);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果

为什么任务2,3比任务4,5执行的更晚?

原因在线程池中任务的提交顺序和任务的执行顺序并不相同

提交顺序

对应到java代码中java.util.concurrent.ThreadPoolExecutor#execute

因为这个提交顺序,所以在java提供的线程池FixedThreadPool没有非核心线程,因为使用队列为LinkedBlockingQueue:是一个基于链表结构的阻塞队列并无边界,此时即使有非核心线程也不会发挥作用。同样CachedThreadPool,使用的是SynchronousQueue一个不存储元素的阻塞队列,所以CachedThreadPool执行效率特别高,因为任务不会堆积,会一直创建线程来执行新的任务。

但是在日常开发过程上述几种线程池均不推荐使用,因为LinkedBlockingQueue无边界,如果任务过多,会发生内存耗尽的情况。而CachedThreadPool当任务过多的时候,会创建大量线程,进行任务处理,同样会占用过多的CPU资源阻碍其他进程。

执行顺序

从执行结果其实就可以看出执行顺序为核心线程-》非核心线程-》队列

线程的本质都有run()->runWorker()

java.util.concurrent.ThreadPoolExecutor#runWorker

为什么任务0,1和任务2,3执行的线程是相同的?

这个问题的本质,线程是如何做到复用的

线程复用和线程执行流程图:

总结 

       线程有三种创建方式,但是本质上只有一种即实现Runable接口,所以线程均有run()。想要搞懂线程池的底层实现原理,必须先明白一个概念,start()是创建线程,而run()则只是普通方法。在线程复用的时候,则调用的是方法级别的run()。

        线程池的有核心线程,非核心线程,队列三个属性值。在向线程池提交任务和执行任务的时候顺序并不相同,提交任务顺序为核心-》队列-》非核心,而执行任务则为核心-》非核心-》队列。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mandy_i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值