线程池的原理和使用(一)

为什么需要线程池呢

java线程的创建非常昂贵,需要JVM和OS(操作系统)配合完成大量的工作.

1:需要为线程堆栈分配和初始化大量的内存块,其中至少1MB栈内存.

2:需要进行系统调用,以便在OS(操作系统)中创建和注册本地线程.

如果频繁的创建和销毁线程会带来性能上的消耗,并且不被编程规范允许.所以线程池主要解决问题如下:

1:提升性能:线程池可以独立的负责线程的创建和维护分配,在执行大量异步任务的时候,不需要手动创建线程,线程池会进行调度,最大限度对已创建的线程复用.

2:线程管理:每个java线程池会保持一些线程池的基本统计信息.执行任务个数,时间等.可以对线程高效的管理,可以对异步任务高效调度.

JUC线程池架构

Executor:

提供了execute接口来执行提交的Runnable执行目标实例.作为执行者的角色,其目的是任务的提交者与任务执行者分离开的.

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}
ExecutorService:

继承Executor,是异步目标任务的执行者服务接口,对外提供异步任务的接收服务,提供了接收异步任务并转交给执行者的方法.

//提交单个任务.
<T> Future<T> submit(Callable<T> task);

//批量提交任务.
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
AbstractExecutorService:

这是一个抽象类,是对ExecutorService方法的默认实现.

ThreadPoolExecutor:

继承于AbstractExecutorService抽象类.是一个线程池的实现类.为每个线程提供了和维护了一些基础的数据统计,方便线程的管理和监控.

ScheduledExecutorService:

是一个接口继承于ExecutorService,是一个可以完成延时和周期性任务的线程调度接口,功能和Timer/TimerTask类似.

ScheduledThreadPoolExecutor:

继承于ThreadPoolExecutor并且实现了ScheduledExecutorService接口,提供了延时和周期性任务的默认实现.

Executors创建线程的方式:

Executors.newSingleThreadExecutor创建单例化线程池:

该快捷方式创建一个单例化线程池,也就是一个线程来执行所有的任务.

public class SingleThreadPoolTest {

    public static final int SLEEP_GAP = 500;

    static class TargetTask implements Runnable {

        static AtomicInteger taskNo = new AtomicInteger(1);

        private String taskName;

        public TargetTask() {
            taskName = "task-" + taskNo.get();
            taskNo.incrementAndGet();
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() 
                    + "任务:" + taskName + "doing");
            //线程睡眠一会.
            try {
                Thread.sleep(SLEEP_GAP);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() 
                    + taskName + "运行结束");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService singleThreadExecutor = 
         Executors.newSingleThreadExecutor();
        for (int i = 0; i < 5; i++) {
            singleThreadExecutor.execute(new TargetTask());
            singleThreadExecutor.execute(new TargetTask());
        }
        Thread.sleep(SLEEP_GAP);
        singleThreadExecutor.shutdown();
    }
}

从这个例子可以看出这个线程池的特点.

1:单线程化的线程池中的任务,是按照提交的次序执行的.

2:池中的线程存活时间是无限的.

3:当池中的唯一线程繁忙时,新提交的任务会进入阻塞队列,并且阻塞队列是无界的.

Executors.newFixedThreadPool()创建固定数量的线程池:

该快捷方式创建一个固定数量的线程池,唯一的参数就是用来设置线程数的大小.

public class FixedThreadPoolTest {

    public static final int SLEEP_GAP = 500;

    static class TargetTask implements Runnable {

        static AtomicInteger taskNo = new AtomicInteger(1);

        private String taskName;

        public TargetTask() {
            taskName = "task-" + taskNo.get();
            taskNo.incrementAndGet();
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()
                    + "任务:" + taskName + "doing");
            //线程睡眠一会.
            try {
                Thread.sleep(SLEEP_GAP);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + taskName + "运行结束");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService singleThreadExecutor = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++) {
            singleThreadExecutor.execute(new SingleThreadPoolTest.TargetTask());
            singleThreadExecutor.submit(new SingleThreadPoolTest.TargetTask());
        }
        Thread.sleep(SLEEP_GAP);
        singleThreadExecutor.shutdown();
    }
}

从这个例子可以看出这个线程池的特点.

1:如果线程数没有达到固的数量,每提交一个任务就会创建一个线程,直到达到固的数量.

2:线程一旦达到固的数量就会保持不变,如果某个线程因为异常而结束,那么线程池会在补充一个线程.

3:当池中所有线程繁忙时,新提交的任务会进入阻塞队列,并且阻塞队列是无界的.

适用场景:

需要任务长期执行的场景,固的线程池的线程数可以稳定在一个一个数.避免了频繁的回收和创建,所以适用于CPU密集型的任务.在CPU长期使用的情况下,减少线程的切换.

缺点:

内部使用无界队列来放任务,当大量任务需要处理时,队列无限增大,使资源迅速耗尽.

Executors.newCachedThreadPool创建可缓存线程池

该快捷方式创建一个创建可缓存线程池,如果池内的某些线程无事可干成为空闲线程,可灵活回收.

public class CachedThreadPoolTest {
    public static final int SLEEP_GAP = 500;

    static class TargetTask implements Runnable {

        static AtomicInteger taskNo = new AtomicInteger(1);

        private String taskName;

        public TargetTask() {
            taskName = "task-" + taskNo.get();
            taskNo.incrementAndGet();
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()
                    + "任务:" + taskName + "doing");
            //线程睡眠一会.
            try {
                Thread.sleep(SLEEP_GAP);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + taskName + "运行结束");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService singleThreadExecutor = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            singleThreadExecutor.execute(new SingleThreadPoolTest.TargetTask());
            singleThreadExecutor.submit(new SingleThreadPoolTest.TargetTask());
        }
        Thread.sleep(SLEEP_GAP);
        singleThreadExecutor.shutdown();
    }
}

从这个例子可以看出这个线程池的特点.

1:在接收新的异步任务target执行目标实例时,如果线程池内所有线程繁忙,就会创建新的线程处理任务.

2:不会限制线程池的大小,线程大小完全依赖于操作系统(或者JVM)所能创建的线程大小.

3:如果部分线程空闲,存活的线程大于任务数量(默认为60s)就会被回收.

适用场景:

快速处理突发性强,耗时较短的任务场景,如netty的NIO处理场景Rest API接口的瞬时削峰场景.可缓存线程池的线程数量不固定,只要有空闲线程就会被回收.接收到新的异步任务,没有可用线程就直接创建一个.

缺点:

线程池没有最大数量的限制,如果大量异步任务执行目标实例同时提交,会因为线程过多而导致资源耗尽.

Executors.newScheduledThreadPool创建一个定时线程池

该快捷方式创建一个可以调度线程池,提供一个延时和周期性任务的线程池

public class ScheduledThreadPoolTest {

    public static final int SLEEP_GAP = 500;

    static class TargetTask implements Runnable {

        static AtomicInteger taskNo = new AtomicInteger(1);

        private String taskName;

        public TargetTask() {
            taskName = "task-" + taskNo.get();
            taskNo.incrementAndGet();
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()
                    + "任务:" + taskName + "doing");
            //线程睡眠一会.
            try {
                Thread.sleep(SLEEP_GAP);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + taskName + "运行结束");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService scheduledExecutorPool = 
                Executors.newScheduledThreadPool(2);
        for (int i = 0; i < 2; i++) {
            //0代表首次执行任务的延迟时间.500代表每次任务的间隔时间.
            scheduledExecutorPool.scheduleAtFixedRate(new TargetTask(),
                    0,500, TimeUnit.MILLISECONDS);
        }
        Thread.sleep(SLEEP_GAP);
//        scheduledExecutorPool.shutdown();
    }
}
.适用场景:

 周期性执行的任务.

public ScheduledFuture<?> scheduleAtFixedRate(
//异步任务
Runnable command,
 //首次执行延时                                            
long initialDelay,
//两次开始执行最小间隔时间
long period,
//时间计时单位
TimeUnit unit);
 public ScheduledFuture<?> scheduleWithFixedDelay(
//执行任务实例.
Runnable command,
 //首次执行延时.
long initialDelay,
//两次执行最小间隔时间.
 long delay,
 //时间单位
 TimeUnit unit);
 

 线程池的标准创建方式:

ThreadPoolExecutor构造方法有很多重载版本,其中比较重要的的构造器如下:

 public ThreadPoolExecutor(
//核心线程数,即使空闲也不会回收.
int corePoolSize,
//线程数的上限
int maximumPoolSize,
//最大空闲时间.
long keepAliveTime,
//存活时间单位                              
TimeUnit unit,
//阻塞队列
ThreadFactory threadFactory,
//任务拒绝策略
RejectedExecutionHandler handler)
1:核心和最大线程数量 

corePoolSize参数用于设置核心线程数量,maximumPoolSize用于设置最大线程数量,线程池会根据这两个参数自动维护线程池中的工作线程,大致规则为:

1:当线程池接收到新的任务,并且当前工作线程少于核心线程数时.即使其他工作线程处于空闲状态,也会创建一个线程来处理新的请求,直到线程数达到核心线程数.

2:如果当前工作线程数大于核心线程数并且小于最大线程数,只有当队列任务满了,才会创建一个新的线程.通过设置核心线程数和最大线程数相等,可以创建一个固定大小的线程池.

3:当最大线程数设置为无界值.线程池可以接收任意数量的并发任务.

4:核心线程数和最大线程数不仅可以再构造方法中设置,还可以通过set方法设置.

 public void setCorePoolSize(int corePoolSize){}

 public void setMaximumPoolSize(int maximumPoolSize) {}
BlockingQueue:

BlockingQueue(阻塞队列)实例用于暂时接收异步任务,如果线程池的核心线程池都在忙,所接收的异步任务都会放在阻塞队列中.

keepAliveTime和TimeUnit unit:

空闲线程的存活时间和时间单位.

ThreadFactory threadFactory:

线程工厂,线程池通过这个工厂类创建线程.可以继承对应的接口,根据业务实现对应的线程.

RejectedExecutionHandler handler:

异步任务的拒绝策略,除了默认的实现以外,还可以通过实现接口RejectedExecutionHandler自定义拒绝策略.

线程提交任务的两种方式:

方式一:

Executor接口中的方法.

  public void execute(Runnable command) {}

方式二:

ExecutorService接口中的方法.

<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

Future<?> submit(Runnable task);

 这两种提交方式的区别:

1:二者接收的方法参数类型不一样

execute方法只能接受Runnable类型的参数.submit方法能够接受Runnable类型的参数也能接受Callable类型参数.Runnable类型的不可以返回结果,不允许抛出异常.Callable类型可以返回结果.允许抛出异常.

2:submit提交任务会有返回值,execute提交任务没有返回值,

execute主要用于任务的提交执行,任务执行的结果和异常调用者并不关心.submit方法也可以用于启动任务的执行,启动以后会返回一个Future对象.代表一个异步执行的实例,可以获取执行结果.

3:submit方法方便Exception处理.

execute方法在启动任务执行后,对执行过程中产生的异常并不关心.,通过submit方法提交可以通过返回的Future实例捕获异常.

通过submit()方法返回的Future对象获取结果

public class CreateThreadPoolDemo {
    public static void main(String[] args) {
        ScheduledExecutorService threadPool = 
        Executors.newScheduledThreadPool(2);
        Future<Integer> future = threadPool.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                Random random = new Random();
                return random.nextInt(10);
            }
        });

        try {
            Integer integer = future.get();
            System.out.println("异步执行的结果" + integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
     threadPool.shutdown();
}  

通过submit()方法返回的Future对象捕获异常

public class ErrorThreadPoolDemo {

    public static final int SLEEP_GAP = 500;

    static class TargetTask implements Runnable {

        static AtomicInteger taskNo = new AtomicInteger(1);

        private String taskName;

        public TargetTask() {
            taskName = "task-" + taskNo.get();
            taskNo.incrementAndGet();
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()
                    + "任务:" + taskName + "doing");
            //线程睡眠一会.
            try {
                Thread.sleep(SLEEP_GAP);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + taskName + "运行结束");
        }
    }

    static class ErrorTask extends TargetTask {
        @Override
        public void run() {
            super.run();
            throw new RuntimeException("我抛出了异常.");
        }
    }

    public static void main(String[] args) {
        ScheduledExecutorService threadPool =
        Executors.newScheduledThreadPool(2);
        ErrorTask errorTask = new ErrorTask();
        Future<?> future = threadPool.submit(errorTask);

        try {
            if (future.get() == null) {
                System.out.println("任务完成");
            }
        } catch (Exception e) {
            System.out.println(e.getCause().getMessage());
        }
        threadPool.shutdown();
    }
}

坚持的意义在哪里呢,其实我自己的感觉就是,没有告诉你应该怎么做,所以我们只能通过学习不断地去摸索去探索,只要在路上,我们终会成功.

如果大家喜欢我的分享的话.可以关注一下微信公众号

心有九月星辰.

 

 
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值