深度解析多线程的创建方式和正确启动多线程

Posted javazhizhe

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度解析多线程的创建方式和正确启动多线程相关的知识,希望对你有一定的参考价值。

一、创建多线程

1. 实现多线程

java 实现多线程的方式准确来说有两种(oracle官方文档说的):

(1)实现 Runnable 接口, 重写run()函数,运行start()方法

代码演示:

/**
 * 用Runnable方式创建线程
 */
public class RunnableStyle implements Runnable 
    @Override
    public void run() 
        System.out.println("我是一个Runnable");
    

    public static void main(String[] args) 
        new Thread(new RunnableStyle()).start();
    

(2)继承 Thread 类,重写run()函数,运行start()方法

/**
 * 用Thread方式创建线程
 * 每个线程只能操作当前线程类的对象变量,耦合性太强
 */
public class ThreadStyle extends Thread 

    @Override
    public void run() 
        System.out.println("我是一个 Thread");
    

    public static void main(String[] args) 
        new ThreadStyle().start();
    

2. 两种实现多线程方法的对比

方法1(实现Runnable接口)更好的三个优势:

(1)解耦性好,run方法业务与线程建立逻辑分离

  • 表现1,这样创建不同的线程可以共享Runnable实例中的变量和方法,即多个线程可以操作同一资源,而Thread只能操作当前对象的业务,即每新建一个线程类,当前资源类变量都会初始化

  • 代码实现区别

    • 实现Runnable接口
    /**
     * 用Runnable方式创建线程
     * 解耦,不同线程可以操作同一资源对象的变量,run业务与线程建立分离
     */
    public class RunnableStyle implements Runnable 
    
        private int count = 10;
    
        @Override
        public void run() 
            count--;
            System.out.println(Thread.currentThread().getName() + " : count=" + count);
        
    
        public static void main(String[] args) throws InterruptedException 
            RunnableStyle runnableStyle = new RunnableStyle();
            new Thread(runnableStyle,"Runnable1").start();
            Thread.sleep(1000);
            new Thread(runnableStyle, "Runnable2").start();
        
    
    
    Runnable1 : count=9
    Runnable2 : count=8
    
    • 继承Thread类
    /**
     * 用Thread方式创建线程
     * 每个线程只能操共享当前线程类的对象变量,耦合性太强
     */
    public class ThreadStyle extends Thread 
    
        private int count = 10;
    
        @Override
        public void run() 
            count--;
            System.out.println(Thread.currentThread().getName() + " : count=" + count);
        
    
        public static void main(String[] args) throws InterruptedException 
            ThreadStyle thread1 = new ThreadStyle();
            thread1.setName("Thread1");
            thread1.start();
            Thread.sleep(1000);
            ThreadStyle thread2 = new ThreadStyle();
            thread2.setName("Thread2");
            thread2.start();
        
    
    
    Thread1 : count=9
    Thread2 : count=9
    
#### (2)节约资源,每次新建任务无需每次都新建线程,例如线程池类

使用继承Thread的方式的话,那么每次想新建一个任务,只能新建一个独立的线程,而这样做的损耗会比较大(比如重头开始创建一个线程、执行完毕以后再销毁等。如果线程的实际工作内容,也就是run()函数里只是简单的打印一行文字的话,那么可能线程的实际工作内容还不如损耗来的大)。如果使用Runnable的方式,只需要更换Runnable实例,不需要新建Thread类,就可以实现线程的重复利用,线程池就是基于这个原理,从而大大减小损耗。

#### (3)可扩展性好,可用实现多继承接口,不能实现多继承类

继承Thread类以后,由于Java语言不支持双继承,这样就无法再继承其他的类,限制了可扩展性。

### 3. 两种方法的本质对比

方法一和方法二,也就是“实现Runnable接口并传入Thread类”和“继承Thread类然后重写run()”在实现多线程的本质上,并没有区别,都是最终调用了start()方法来新建线程。这两个方法的最主要区别在于run()方法的内容来源:

```java
...
public class Thread implements Runnable 
    private Runnable target;
...
    @Override
    public void run() 
        if (target != null) 
            target.run();
        
    
...

方法一:最终调用Runnable实例的run()方法(target.run());

方法二:直接将Thread类的里整个 run() 方法重写

4. 创建线程时同时使用Runnable、Thread两种方法会发生什么?

如果 同时使用Runnable、Thread两种方法,那么只会执行 Thread 中重写的 run()方法,因为将父类中的run方法覆盖了:

...
public class Thread implements Runnable 
    private Runnable target;
...
    // 该 run 方法直接被重写了,便不再会执行 target.run()方法了
    @Override
    public void run() 
        if (target != null) 
            target.run();
        
    
...

同时执行两种方式的测试:

/**
 * 同时执行两种方式的测试
 * 匿名内部类
 */
public class DoubleStyle 
    public static void main(String[] args) 
        new Thread(() -> 
            System.out.println("我是 Runnable");
        ) 
            @Override
            public void run() 
                System.out.println("我是 Thread");
            
        .start();
    

执行结果:

我是 Thread 的 run方法

准确的讲,创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元有两种方式:一是实现Runnable接口的run方法,并把Runnable实例传给Thread类,二是重写Thread的run方法(继承Thread类)

5. 其它创建线程的方式

其它创建线程的方式也有很多,但都是在代码的写法上的千变万化,本质上还是都基于 Thread 类和 Runnable 类的重写run()方法。比如以下几种方式:

(1)线程池创建线程

  • 线程池本质上,是创建有限个线程排队处理多于线程数量的业务,为了节省创建和销毁线程的时间
  • 本质上还是实现Runnable接口创建线程类
  • 源码片段:

(2)通过Callable和FutureTask创建线程

  • Callable和FutureTask类同时继承了Future接口和Runnnable接口,本质还是实现Runnnable接口中的run方法

(3)定时器也可以创建线程

演示代码:

/**
 * 描述:     定时器创建线程
 */
public class DemoTimmerTask 

    public static void main(String[] args) 
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() 
            @Override
            public void run() 
                System.out.println(Thread.currentThread().getName());
            
        , 1000, 1000);
    

其中 TimerTask 又是实现了 Runnable 接口 的类。

所以,本质上,创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元有两种方式:一是实现Runnable接口的run方法,并把Runnable实例传给Thread类,二是重写Thread的run方法(继承Thread类)

二、启动多线程

1. start()和run()对比

代码测试:

public class StartAndRunMethod 

    public static void main(String[] args) 
        Runnable runnable = () -> 
            System.out.println(Thread.currentThread().getName());
        ;
        runnable.run();

        new Thread(runnable).start();
    

测试结果:

main
Thread-0

可见 run() 只是在主线程中调用了 run 方法,没有启动子线程,这显然不符合我们开启新线程的预期;而 start() 启动了一个子线程,并调用了线程中的run方法。

下面就这两种方法进行解析:

2. start()方法

执行start方法是请求 JVM 开辟一个子线程,但线程具体什么时候开始执行,取决于CPU的调度,有可能立即执行,也有可能因为繁忙稍后执行或不执行。

start方法的执行流程:

  • 检查线程状态,只有NEW状态下的线程才能继续,否则会抛出IllegalThreadStateException(在运行中或者已结束的线程,都不能再次启动)
  • 加入线程组
  • 调用 start0() 方法启动线程,这是一个 native 方法,用 c++ 写的。

注意点:

  • start方法是被synchronized修饰的方法,可以保证线程安全;
  • start()方法不能执行两次,因为会有“检查线程状态”这一步,见上图;
  • 由JVM创建的main方法线程和system组线程,并不会通过start来启动。

3. run()方法

源码:

run()方法重写的两种方式:

  • 实现Runnable接口,重写Runnable接口的run()方法,注入到Thread类,执行target.run()。
  • 继承Thread类,重写Thread类中的run(),这时原run()中的if(target != null) target.run(); ,这部分就会被覆盖重写。

文章来源:深度解析多线程的创建方式和正确启动多线程


微信公众号名称:Java知者

微信公众号id:JavaZhiZhe

欢迎关注,谢谢!


多线程--线程池的正确打开方式

概述

线程可认为是操作系统可调度的最小的程序执行序列,一般作为进程的组成部分,同一进程中多个线程可共享该进程的资源(如内存等)。JVM线程跟内核轻量级进程有一对一的映射关系,所以JVM中的线程是很宝贵的。

一般在工程上多线程的实现是基于线程池的。因为相比自己创建线程,多线程具有以下优点

  • 线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
  • 可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。

Executors存在什么问题

看阿里巴巴开发手册并发编程这块有一条:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。

技术图片

Executors为什么存在缺陷

1. 线程池工作原理

技术图片

当一个任务通过execute(Runnable)方法欲添加到线程池时:

  • 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  • 如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
  • 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  • 那么通过 handler所指定的策略来处理此任务。也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
  • 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

2.newFixedThreadPool分析

技术图片

Java中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueue 和 LinkedBlockingQueue

ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。

LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE

这里的问题就出在:不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。也就是说,如果我们不设置LinkedBlockingQueue的容量的话,其默认容量将会是Integer.MAX_VALUE

newFixedThreadPool中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。

3. newCachedThreadPool分析

技术图片

结合上述流程图,核心线程数=0,最大线程无限大,由于SynchronousQueue是一个缓存值为1的阻塞队列。当有大量任务请求时,线程池会创建大量线程,造成OOM。

线程池参数详解

1. 构造方法

/**
 * @param corePoolSize  核心线程数
 * @param maximumPoolSize 最大线程数
 * @param keepAliveTime 线程所允许的空闲时间
 * @param unit 线程所允许的空闲时间的单位
 * @param workQueue  线程池所使用的缓冲队列
 * @param handler 线程池对拒绝任务的处理策略
 */
ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler)

2. 线程池拒绝策略

RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。。以下是JDK1.5提供的四种策略。

  • AbortPolicy:直接抛出异常
  • CallerRunsPolicy:只用调用者所在线程来运行任务。
  • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
  • DiscardPolicy:不处理,丢弃掉。
  • 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

线程池正确打开方式

1. 创建线程池

避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。

        ThreadPoolExecutor executorService = new ThreadPoolExecutor(8,
                16,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(10));

2. 向线程池提交任务

我们可以使用execute提交的任务,但是execute方法没有返回值,所以无法判断任务知否被线程池执行成功。通过以下代码可知execute方法输入的任务是一个Runnable类的实例。

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(60));

        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程池无返回结果");
            }
        });

我们也可以使用submit 方法来提交任务,它会返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(60));

        Future<String> future = threadPoolExecutor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "ok";
            }
        });
        System.out.println("线程池返回结果:" + future.get());

3. 关闭线程池

shutdown关闭线程池

方法定义:public void shutdown()

(1)线程池的状态变成SHUTDOWN状态,此时不能再往线程池中添加新的任务,否则会抛出RejectedExecutionException异常。

(2)线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。

注意这个函数不会等待提交的任务执行完成,要想等待全部任务完成,可以调用:

public boolean awaitTermination(longtimeout, TimeUnit unit)

shutdownNow关闭线程池并中断任务

方法定义:public List shutdownNow()

(1)线程池的状态立刻变成STOP状态,此时不能再往线程池中添加新的任务。

(2)终止等待执行的线程,并返回它们的列表;

(3)试图停止所有正在执行的线程,试图终止的方法是调用Thread.interrupt(),但是大家知道,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。

4. 如何配置线程池大小

CPU密集型任务

该任务需要大量的运算,并且没有阻塞,CPU一直全速运行,CPU密集任务只有在真正的多核CPU上才可能通过多线程加速 CPU密集型任务配置尽可能少的线程数量:

CPU核数+1个线程的线程池

例如: CPU 16核,内存32G。线程数=16

IO密集型任务

IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2

某大厂设置策略:IO密集型时,大部分线程都阻塞,故需要多配置线程数:

CPU核数/(1-阻塞系数)

例如: CPU 16核, 阻塞系数 0.9 ------------->16/(1-0.9) = 160 个线程数。

此时非阻塞线程=16

以上是关于深度解析多线程的创建方式和正确启动多线程的主要内容,如果未能解决你的问题,请参考以下文章

回炉再造-多线程的创建

多线程--线程池的正确打开方式

Java多线程带你用不一样的思维看创建线程的两种方式

Java多线程带你用不一样的思维看创建线程的两种方式

java核心学习(二十一) 多线程---创建启动线程的三种方式

多线程