多线程 | 多线程实现方式和差异

1、为什么要使用多线程呢?

  1. 从计算机底层来说: 线程可以⽐作是轻量级的进程,是程序执⾏的最⼩单位,线程间的切换和调度的成本远远⼩于进程。

  1. 现在的系统动不动就要求百万级甚⾄千万级的并发量,⽽多线程并发编程正是开发⾼并发系统的基础,利⽤好多线程机制可以⼤⼤提⾼系统整体的并发能⼒以及性能。

  1. 提高CPU的利用率 目前大多数CPU都是多核的 可以都利用起来。

  1. 耗时的操作使用多线程,可以异步执行提高应用程序响应。

常用场景:客户端请求后可以异步执行不用返回给客户端的数据处理、后台定时任务中的异步分批执行任务、优化复杂查询(FutureTask)等等。

2、线程的生命周期和状态?

3、使用多线程可能带来什么问题?

并发编程的⽬的就是为了能提⾼程序的执⾏效率提⾼程序运⾏速度,但是并发编程并不总是能提⾼程序运⾏速度的,⽽且并发编程可能会遇到很多问题,⽐如:内存泄漏、上下⽂切换、死锁还有受限于硬件和软件的资源闲置问题。

4、线程创建方式?

  1. 继承 Thread

 private static void thread() {
        Thread thread = new Thread(() -> {
            try {
                // 业务代码 。。。
                //加sleep 试一下执行顺序
                Thread.sleep(100);
                System.out.println("Thread方式执行 新 线程:" + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        //调用 start 才会执行线程中的 run()方法中的业务代码
        thread.start();
        System.out.println("Thread方式执行 主 线程:"+Thread.currentThread().getName());
    }

通过实现Thread类型创建线程(实际开发中用到的不多,直接用线程池,只是为了单开一个线程可以使用)

执行结果:可以看出没有按照顺序执行,异步的。主线程比新开线程先执行。

  1. 实现 Runnable

private static void runnable() {
        Runnable runnable = ()-> {
            // 业务代码 。。。
            try {
                //加sleep 试一下执行顺序
                Thread.sleep(100);
                System.out.println("Runnable 方式执行 新 线程:" + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        //调用 start 才会执行线程中的 run()方法中的业务代码
        new Thread(runnable).start();
        System.out.println("Runnable 方式执行 主 线程:"+Thread.currentThread().getName());
    }

通过实现Runnable和Thread的结果是一样的。

3、实现Callable

 private static void callable() throws ExecutionException, InterruptedException {
        Callable callable = ()-> {
            // 业务代码 。。。
            //加sleep 试一下执行顺序
            Thread.sleep(100);
            System.out.println("Callable 方式执行 新 线程:" + Thread.currentThread().getName());
            return 1;
        };
        //要使用Callable还不能直接像 Runnable 一样,而是要借助 FutureTask
        FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);
        new Thread(futureTask).start();
        //获取返回值
        System.out.println(futureTask.get() );
        System.out.println("Callable 方式执行 主 线程:"+Thread.currentThread().getName());
    }

通过实现Runnable ,使用callable还不能直接像 runnable 一样,而是要借助 FutureTask。可以得到新线程的结果,且异常可以抛到主线程种(这里也有个坑,就是如果新新线程种出现异常抛到主线程,可能不是很容易定位)

4、线程池创建方式

线程池的4种创建方式

Executors.newCachedThreadPool() 创建一个可缓存的线程池

  • 这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

  • ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue());

Executors.newFixedThreadPool(3) 创建固定大小的线程池

  • 每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

  • LinkedBlockingQueue 核心线程和最大线程是相同的

Executors.newSingleThreadExecutor() 创建一个单线程化的线程池

  • 这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

  • 核心线程和最大都是一个 LinkedBlockingQueue

Executors.newScheduledThreadPool(5) 创建一个定长线程池。此线程池支持定时以及周期性执行任务的需求。— 延迟执行

ThreadPoolExecutor的方式程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写更加明确线程池的运行规则,规避资源耗尽的风险。

基于ThreadPoolExecutor线程池工具类

public class ThreadPoolUtils {
    //可用处理器的Java虚拟机的数量
    private static int CORE_SIZE  =  Runtime.getRuntime().availableProcessors();
    private static int CORE_MAX_SIZE =  Runtime.getRuntime().availableProcessors()*2;
    // volatile 保证线程的存可见性
    private static volatile ExecutorService executorService ;
    private static ExecutorService getInstance(){
        synchronized (ThreadPoolUtils.class){
            if(executorService == null){
                synchronized (ThreadPoolUtils.class){
                    return executorService = new ThreadPoolExecutor(CORE_SIZE, CORE_MAX_SIZE , 10, TimeUnit.SECONDS,
                            new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(),
                            new ThreadPoolExecutor.AbortPolicy());
                }
            }
            return executorService;
        }
    }

    public static void submit(Runnable runnable) {
        getInstance().submit(runnable);
    }

    public static <T> Future<T> submitCall(Callable<T> callable) {
        return getInstance().submit(callable);
    }

    public static void synCall(Runnable... runnableSet) {
        for (Runnable runnable : runnableSet) {
            getInstance().submit(runnable);
        }
    }
}

5、SpringBoot 中的多线程开启

  • 在SpringBootApplication启动类上开启这个异步注解@EnableAsync。

  • 配置 Configuration 配置线程池的参数

  • 在需要开启异步的方法上面加注解 @Async 【异步方法不要和主线程在同一个类,不然可能会不生效哦

 @Bean("scheduledTaskExecutor")
    public Executor asyncSendCouponsScheduledTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //配置核心线程数
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
        //配置最大线程数
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
        //配置队列大小
        executor.setQueueCapacity(500000);
        //配置线程池中的线程的名称前缀
        executor.setThreadNamePrefix("scheduled-task-executor-");
        // rejection-policy:当pool已经达到max size的时候,如何处理新任务
        // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //执行初始化
        executor.initialize();
        return executor;
    }

5、为什么要⽤线程池?

降低资源消耗:通过重复利用已创建的线程减少线程的创建和销毁造成的消耗。

提高响应速度:当任务到达时,可以不用等待线程创建就可以直接执行。

提⾼线程的可管理性:线程时稀缺资源,不断地无限制创建,不仅会消耗系统的资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优、监管。

总结

1、Thread和Runnable的异同:

  • Thread 和 Runnable本质上没有什么区别,看源码可以看见Thread是 类,Runnable是 接口,Thread实现了Runnable接口(只有一个run()方法)

Thread作为实现类扩展了很多实现方法

  • 在使用上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现Runnable 。

2、Runnable与Callable的异同

相同点

  • 都是接口

  • 都可以编写多线程程序

  • 都采用Thread.start()启动线程

不同点

  • Runnable没有返回值;Callable可以返回执行结果

  • Callable接口的call()允许抛出异常;Runnable的run()不能抛出

  • 要使用Callable还不能直接像 Runnable 一样,而是要借助 FutureTask

3、相比new Thread,Java提供的四种线程池的好处在于:

  • 每次new Thread新建对象性能差。

  • 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。

  • 缺乏更多功能,如定时执行、定期执行、线程中断。

  1. 线程池重用存在的线程,减少对象创建、消亡的开销,性能佳。

  1. 线程池可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

  1. 线程池提供定时执行、定期执行、单线程、并发数控制等功能。