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与其原理相同。

相关文章
|
7天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
27 9
|
10天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
7天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
9天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
10天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
21 1
|
6月前
|
Java 调度
Java并发编程:深入理解线程池的原理与实践
【4月更文挑战第6天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将从线程池的基本原理入手,逐步解析其工作过程,以及如何在实际开发中合理使用线程池以提高程序性能。同时,我们还将关注线程池的一些高级特性,如自定义线程工厂、拒绝策略等,以帮助读者更好地掌握线程池的使用技巧。
|
3月前
|
安全 Java 数据库
一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这篇文章是关于Java面试题的笔记,涵盖了线程池复用原理、Spring框架基础、AOP和IOC概念、Bean生命周期和作用域、单例Bean的线程安全性、Spring中使用的设计模式、以及Spring事务的实现方式和隔离级别等知识点。
|
2月前
|
存储 缓存 Java
JAVA并发编程系列(11)线程池底层原理架构剖析
本文详细解析了Java线程池的核心参数及其意义,包括核心线程数量(corePoolSize)、最大线程数量(maximumPoolSize)、线程空闲时间(keepAliveTime)、任务存储队列(workQueue)、线程工厂(threadFactory)及拒绝策略(handler)。此外,还介绍了四种常见的线程池:可缓存线程池(newCachedThreadPool)、定时调度线程池(newScheduledThreadPool)、单线程池(newSingleThreadExecutor)及固定长度线程池(newFixedThreadPool)。
|
4月前
|
监控 Java 开发者
深入理解Java并发编程:线程池的原理与实践
【5月更文挑战第85天】 在现代Java应用开发中,高效地处理并发任务是提升性能和响应能力的关键。线程池作为一种管理线程的机制,其合理使用能够显著减少资源消耗并优化系统吞吐量。本文将详细探讨线程池的核心原理,包括其内部工作机制、优势以及如何在Java中正确实现和使用线程池。通过理论分析和实例演示,我们将揭示线程池对提升Java应用性能的重要性,并给出实践中的最佳策略。
|
4月前
|
设计模式 存储 安全
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
62 1