Java中多线程创建方式对比与线程池相关原理概述汇总(超详细)

简介: Java中多线程创建方式对比与线程池相关原理概述汇总(超详细)准备加入阿里开发者社区,同时也是很久没有写文章了,一时也不知道从何写起,那就先从多线程来吧,这次想把多线程与高并发的相关知识进行一下梳理,从多线程的几种创建方式开始,逐步到线程池原理分析,再到java中常用锁的使用场景与原理分析,再到高并发的处理方案,以及后面分布式锁等知识点,分成批次来进行梳理,这次先分析一下多线程的创建方式的异同点与线程池的执行原理。

1、线程的创建

线程的创建无非就是以下几种方式:

  1. 采用直接集成Thread类,并重写run方法
  2. 实现Runnable接口,并重写run方法
  3. 实现有返回值的Callable接口,重写call方法
  4. 采用线程池 (不建议使用Executors来创)


前三种方式不建议在程序中直接使用,这就是为什么我们要采用线程池的问题?那为啥呢?

穿插问题一、为什么要采用线程池?

1. 减:  首先就是减少开销,频繁的创建线程会占用内存,增加内存的开销,每次请求都要创建和销毁一个线程的话会占用系统很多的资源,同时也会增大系统处理时间,使得响应时间变慢。

2. 控:  合理的控制线程数,通过线程池的核心线程数、最大线程数、队列线程数等来控制多线程的数量,可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,使服务器崩溃。

3. 管:  方便管理线程,线程池就就是线程的托管中心,里面包含了定义的线程数量,当任务到达时,任务可以不需要的等到线程创建就能立即执行。

上面提到, 为什么不建议使用Executors来创建线程呢? 这个问题我们留在后面谈到线程池的时候再进行详细的阐述。

1.1 直接继承Thread类


```java

/**

* @author :zjc

* @ProjectName: execises

* @Package: com.execises.zjc.controller.thread

* @ClassName: ThreadOpByThread

* @date :Created in 2021/7/16 15:03

* @description:创建多线程之继承Thread类

* @modified By:

* @version: v1.0.0$

*/

public class ThreadOpByThread extends Thread {

   @Override

   public void run() {

       super.run();

       for (int i = 0; i < 3; i++) {

           System.out.println(Thread.currentThread().getName() + i);

       }

   }

   static class testThread {

       public static void main(String[] args) {

           ThreadOpByThread threadOpByThread = new ThreadOpByThread();

           threadOpByThread.start();

       }

   }

}

1.2 实现Runnable接口


```java

/**

* @author :zjc

* @ProjectName: execises

* @Package: com.execises.zjc.controller.thread

* @ClassName: ThreadOpByThread

* @date :Created in 2021/7/16 15:03

* @description:创建多线程之继承Thread类

* @modified By:

* @version: v1.0.0$

*/

public class ThreadOpByRunnable implements Runnable {

   @Override

   public void run() {

       for (int i = 0; i < 3; i++) {

           System.out.println(Thread.currentThread().getName() + i);

       }

   }

   static class testThread {

       public static void main(String[] args) {

           ThreadOpByRunnable threadOpByThread = new ThreadOpByRunnable();

           Thread thread = new Thread(threadOpByThread);

           thread.start();

       }

   }

}

1.3 实现Callable接口

在这里我们采用两种方式,一个是采用FutureTask来接收返回值,第二个就是采用线程池来接收返回值,首先我们采用第一种方式。


```java

/**

* @author :zjc

* @ProjectName: execises

* @Package: com.execises.zjc.controller.thread

* @ClassName: ThreadOpByThread

* @date :Created in 2021/7/16 15:03

* @description:创建多线程之继承Thread类

* @modified By:

* @version: v1.0.0$

*/

public class ThreadOpByCallable implements Callable<Integer> {

   @Override

   public Integer call() {

       int sum = 0;

       for (int i = 0; i < 3; i++) {

           System.out.println(Thread.currentThread().getName() + i);

           sum += i;

       }

       return sum;

   }

   static class TestThread {

       public static void main(String[] args) throws ExecutionException, InterruptedException {

           ThreadOpByCallable threadOpByThread = new ThreadOpByCallable();

           FutureTask<Integer> ft= new   FutureTask<Integer>(threadOpByThread);

           Thread thread = new Thread(ft);

           thread.start();

           int sum = ft.get();

           System.out.println("最终值:"+sum);

       }

   }


第二种就是采用线程池的方法,这里先采用newCachedThreadPool线程池,后面我会讲解这几个线程池有啥区别,自定义线程池为啥就那么好,这里先卖个关子。


```java

public static void main(String[] args) throws ExecutionException, InterruptedException {

           ExecutorService poolExecutor = Executors.newCachedThreadPool();

//            ExecutorService poolExecutor = Executors.newFixedThreadPool(10);

//            ExecutorService poolExecutor = Executors.newSingleThreadExecutor();

//            ExecutorService poolExecutor = Executors.newScheduledThreadPool(10);

//            ExecutorService poolExecutor = Executors.newWorkStealingPool();

           //创建一个有返回值的实例

           Callable c = new ThreadOpByCallable();

           //提交线程 获取Future对象

           Future future = poolExecutor.submit(c);

           System.out.println("运行结果" + future.get().toString());

           //关闭线程池

           poolExecutor.shutdown();

       }

这里我们就用到了Executors这个类,那么阿里规约里面,是不建议使用这个类的,使用这个类时就会弹出以下提示:

20210717091828532.png

那么我们就要想,为什么会这样提示呢?

穿插问题二、为什么不建议使用Executors来创建线程池?

经过查看阿里开发手册,上面是这样写的: 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 

Executor提供的四个常见的静态方法创建线程池,但是阿里规约却并不建议使用它,这四个线程池也就是我上面注释掉的

  1. newCachedThreadPool
  2. newFixedThreadPool
  3. newSingleThreadExecutor
  4. newScheduledThreadPool

我这边又多加了一个newWorkStealingPool,接下来我就用一个例子来阐述他们有什么样的关系和区别。

首先在进入线程池章节之前,我们先思考一下这个问题,为什么我已经有了Thread还要用Runnable来创建线程呢,这俩有啥不同呢?

穿插问题三、有了Thread还要用Runnable来创建线程

1、首先我们要明白这两者的区别,Thread是类,只能通过单一继承的方式来实现,Runnable是接口,在这里可以去详细的学习一下接口和抽象类的区别,后面我也会总结一下,放到这里。

2、在使用Runnable定义的子类中没有start()方法,只有run()方法,只有Thread类中才有start()方法。


一、对于类继承,java只允许单继承模式,如果你需要设计一个类既继承业务类拥有某个业务类功能,又继承线程对象Thread类具有多线程的功能,那么Thread类就无法满足你的要求。这时候就要选择Runnable。

二、在使用Runnable定义的子类中没有start()方法,只有Thread类中才有。此时观察Thread类,有一个构造方法:public Thread(Runnable targer)此构造方法接受Runnable的子类实例,也就是说可以通过Thread类来启动Runnable实现的多线程。(start()可以协调系统的资源)

在程序开发中只要是多线程肯定永远以实现Runnable接口为主,因为实现Runnable接口相比继承Thread类有如下好处:

避免点继承的局限,一个类可以继承多个接口。 适合于资源的共享


那么start()方法和run()方法有什么区别呢?

穿插问题四、start()方法和run()方法有什么区别?

1、当java虚拟机(也就是经常说的 JVM)执行mian方法的时候,他会找操作系统(也就是os)开辟一条main方法通向cpu的路径,这个路径就是main线程,也就是所说的主线程。

2、接着cpu通过这个线程,也就是这个路径,就可以执行main方法了。

3、当我们new了一个Thread对象,这个时候又开辟了一条通向cpu的路径,而这条路径是用来执行run方法的。

4、而现在对于cpu而言,他就有了很多条执行的路径,cpu就有了选择的权利,cpu他喜欢谁,就会执行哪条路径,我们是控制不了cpu的,所有就有了出现随机打印的结果。

反过来说,两个线程,一个main线程,一个新线程,他们一起抢夺cpu的执行权(即 cpu的执行时间),谁抢到了,谁先执行。

继续前面的问题,此时我们观察Thread类,有一个构造方法:


```java

public Thread(Runnable target) {

       init(null, target, "Thread-" + nextThreadNum(), 0);

   }

public Thread(Runnable target)此构造方法接受Runnable的子类实例,也就是说可以通过Thread类来启动Runnable实现的多线程。(start()可以协调系统的资源)

在程序开发中只要是多线程肯定永远以实现Runnable接口为主,因为实现Runnable接口相比继承Thread类有如下好处:


1、避免点继承的局限,一个类可以继承多个接口。 

2、适合于资源的共享 

详细了解一下关于资源共享方式和方法,网上有很多,这边就举个例子,可以去看下面的链接。


2.线程池


文章一开始就阐述了采用线程池的优点,如果我们使用线程的时候就去创建一个线程,虽然简单,但是存在很大的问题。如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。线程池通过复用可以大大减少线程频繁创建与销毁带来的性能上的损耗。


在前面我们讲过线程池中不建议采用Executors去创建,那我们就来对比一下,Executors中的四种方法和自定义线程池有啥不同,我在这里还是采用代码的形式,根据响应时间来进行一个对比:


```java

/**

* @author :zjc

* @ProjectName: execises

* @Package: com.execises.zjc.controller.thread.threadPool

* @ClassName: ThreadExecutorsNewCachedThreadPool

* @date :Created in 2021/7/18 9:40

* @description:线程池

* @modified By:

* @version: v1.0.0$

*/

public class ThreadExecutors {

   static class MyThread implements Runnable {

       private String name;

       public MyThread(String name) {

           this.name = name;

       }

       @Override

       public void run() {

           System.out.println(Thread.currentThread().getName() + "----------" + name);

           try {

               Thread.sleep(1000L);

           } catch (InterruptedException e) {

               e.printStackTrace();

           }

       }

   }

   public static void main(String[] args) {

       ExecutorService poolExecutor = Executors.newCachedThreadPool(); //时间间隔为1s 最快

       //ExecutorService poolExecutor = Executors.newFixedThreadPool(10);//时间间隔为1s 慢

       //ExecutorService poolExecutor = Executors.newSingleThreadExecutor();// 时间间隔为1s 超级慢 一个一个执行

       //ExecutorService poolExecutor = Executors.newScheduledThreadPool(10); //时间间隔为1s 一般

       //ExecutorService poolExecutor = Executors.newWorkStealingPool(); //时间间隔为1s 一般

       try {

           for (int i = 0; i < 100; i++) {

               poolExecutor.execute(new MyThread(i + ""));

           }

       } catch (Exception e) {

           e.printStackTrace();

       } finally {

           //关闭线程池

           poolExecutor.shutdown();

       }

   }

}

```

这里就逐一介绍一下Executors去创建线程池的四种方式:

2.1、Executors的常用线程池

Executors提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。

在讲解这几个方法式,我们先了解一下线程池中的几个参数,以及主要的线程池对象ThreadPoolExecutor。


```java

   public ThreadPoolExecutor(int corePoolSize,//核心线程数

                             int maximumPoolSize,//最大线程池大小,也就是线程池总的大小

                             long keepAliveTime,//线程最大空闲时间

                             TimeUnit unit,//时间单位

                             BlockingQueue<Runnable> workQueue, //线程等待队列

                             ThreadFactory threadFactory, //线程创建工厂

                             RejectedExecutionHandler handler //拒绝策略

                             )

```

上面代码就是ThreadPoolExecutor中主要的参数,在这里我用一个生动形象的比喻来整体上阐述他们的关系。

2.2、外包模式分析线程池执行逻辑

目前很多互联网大厂都是采用外包的形式,这里也采用这种形式,能够通俗易懂的了解相关的逻辑原理。


corePoolSize核心线程数就是大厂公司的正式员工


maximumPoolSize减去corePoolSize就是外包过来的员工也就是非核心员工,该部分也就是线程池总的大小减去核心线程数。


workQueue是队列,用来存放每一个线程所要执行的任务,也就是公司的任务列表,类似于需求评审报告的性质。



2.2.1、newCachedThreadPool()

创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。有多少任务我就创建多少员工。

其实他的底层也就是直接新建了一个ThreadPoolExecutor对象,我们查看其源码如下:


```java

 public static ExecutorService newCachedThreadPool() {

       return new ThreadPoolExecutor(0, Integer.MAX_VALUE,

                                     60L, TimeUnit.SECONDS,

                                     new SynchronousQueue<Runnable>());

   }

```


```java

corePoolSize = 0


maximumPoolSize = Integer.MAX_VALUE,即线程数量几乎无限制;

@Native public static final int   MAX_VALUE = 0x7fffffff;


keepAliveTime = 60s,线程空闲60s后自动结束。

```


workQueue 为 SynchronousQueue 同步队列,这个队列类似于一个接力棒,入队出队必须同时传递,因为CachedThreadPool线程创建无限制,不会有队列等待,所以使用SynchronousQueue;


首先会先到核心线程数也就是核心员工去找,看有没有核心员工,没有的话就放入到队列当中,因为是同步队列,只存在一个节点,只要有任务过来就立马创建消费流程,所以就会在非核心员工处去执行任务。因为这个部分有很多的非核心线程数,所以每一个任务都能被执行,所以速度会很快。


2.2.2、newFixedThreadPool(int nThreads)

```java

   public static ExecutorService newFixedThreadPool(int nThreads) {

       return new ThreadPoolExecutor(nThreads, nThreads,

                                     0L, TimeUnit.MILLISECONDS,

                                     new LinkedBlockingQueue<Runnable>());

   }

```

线程大小为10个10个一组,创建固定数目线程的线程池。newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程。


LinkedBlockingQueue为无界任务队列(等待队列的大小是无界,理论上大小取决于内存大小)查看其底层源码,该队列的大小为Integer的最大值:2的31次方-1。


```java

public LinkedBlockingQueue() {

       this(Integer.MAX_VALUE);

   }

```

该队列的大小为2的31次方-1


我们在程序中固定的线程数为10,所以核心员工和最大员工数量都是10,10-10为非核心员工数,所以只有核心员工10人,所以整个线程运行中只存在10个员工,首先执行0-9序号的10个任务,其余90个任务在队列里等待,执行完毕后,再去执行10-19序号的10个任务,其余80个在队列里等待,重复整个操作,直到所有任务都被执行完毕。

所以在这里是他的线程数量决定了它的运行快慢,如果将其改为100,他执行效率和newCachedThreadPool效率大别不是很大,几乎相同。


2.2.3、SingleThreadExecutor()

单例线程,任意时间池中只能有一个线程用的是和cache池和fixed池相同的底层池。


```java

   public static ExecutorService newSingleThreadExecutor() {

       return new FinalizableDelegatedExecutorService

           (new ThreadPoolExecutor(1, 1,

                                   0L, TimeUnit.MILLISECONDS,

                                   new LinkedBlockingQueue<Runnable>()));

   }


```

他和newFixedThreadPool底层是相同的,只不过每次最大线程数和核心线程数都是1,所以每次执行都是单一进程在执行,线程名称均为thread-1,执行完一个任务再去执行下一个任务,循环往复。

从整体上看,以上几种创建方式都可以结合业务去调整,都能够满足我们日常开发中的需要,但是我们为什么不建议使用以上几种线程池的创建方式呢?因为Executors 中的线程池对象会产生OOM异常。

穿插问题:Executors 中的线程池对象为什么会产生OOM?

Executors 返回线程池对象的弊端如下 :   

FixedThreadPool 和 SingleThreadExecutor  :允许请求的队列长度为   Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。

CachedThreadPool 和 ScheduledThreadPool :允许创建的线程数量为   Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM


首先我们先明白,为啥会发生OOM,一般在大型企业互联网公司,大促或者秒杀的时候,都会产生大量的请求过来,并发会超级高,任务会越来越多。

假如阿里采用newCachedThreadPool()有多少个任务就会创建多少个线程,比如有1千万个请求任务,就会创建1千万个线程,这些线程都会在电脑当中创建,所以CPU就会100%,程序就会卡死不动了。

因为创建的是线程,需要CPU分配最小时间单位去执行线程,所以newCachedThreadPool()并不一定会发生OOM操作,会直接卡死,占用大量CPU。


FixedThreadPool()我们上面已经分析过,他是一个可以固定线程数量的线程池,我们设定为10,所以只能执行10个任务,并不会一直新创建线程,剩余的都会放在队列里,又因为其队列大小为Integer的最大值,无限增长队列,因为队列存在于内存当中,通过内存装载数据,直到装满到最大值,产生内存溢出。SingleThreadExecutor与其原理相同。

相关文章
|
23小时前
|
缓存 Java
Java并发编程:深入理解线程池
【4月更文挑战第26天】在Java中,线程池是一种重要的并发工具,它可以有效地管理和控制线程的执行。本文将深入探讨线程池的工作原理,以及如何使用Java的Executor框架来创建和管理线程池。我们将看到线程池如何提高性能,减少资源消耗,并提供更好的线程管理。
|
1天前
|
消息中间件 缓存 NoSQL
Java多线程实战-CompletableFuture异步编程优化查询接口响应速度
Java多线程实战-CompletableFuture异步编程优化查询接口响应速度
|
1天前
|
数据采集 存储 Java
高德地图爬虫实践:Java多线程并发处理策略
高德地图爬虫实践:Java多线程并发处理策略
|
2天前
|
安全 算法 Java
JavaSE&多线程&线程池
JavaSE&多线程&线程池
16 7
|
2天前
|
缓存 Java
【Java基础】简说多线程(上)
【Java基础】简说多线程(上)
6 0
|
3天前
|
并行计算 算法 安全
Java从入门到精通:2.1.3深入学习Java核心技术——掌握Java多线程编程
Java从入门到精通:2.1.3深入学习Java核心技术——掌握Java多线程编程
|
3天前
|
安全 Java 编译器
是时候来唠一唠synchronized关键字了,Java多线程的必问考点!
本文简要介绍了Java中的`synchronized`关键字,它是用于保证多线程环境下的同步,解决原子性、可见性和顺序性问题。从JDK1.6开始,synchronized进行了优化,性能得到提升,现在仍可在项目中使用。synchronized有三种用法:修饰实例方法、静态方法和代码块。文章还讨论了synchronized修饰代码块的锁对象、静态与非静态方法调用的互斥性,以及构造方法不能被同步修饰。此外,通过反汇编展示了`synchronized`在方法和代码块上的底层实现,涉及ObjectMonitor和monitorenter/monitorexit指令。
16 0
|
17天前
|
存储 Java 数据库连接
java多线程之线程通信
java多线程之线程通信
|
28天前
|
存储 缓存 NoSQL
Redis单线程已经很快了6.0引入多线程
Redis单线程已经很快了6.0引入多线程
31 3
|
1月前
|
消息中间件 安全 Linux
线程同步与IPC:单进程多线程环境下的选择与权衡
线程同步与IPC:单进程多线程环境下的选择与权衡
58 0