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

相关文章
|
1天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
3天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
3天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
3天前
|
监控 Java API
探索Java NIO:究竟在哪些领域能大显身手?揭秘原理、应用场景与官方示例代码
Java NIO(New IO)自Java SE 1.4引入,提供比传统IO更高效、灵活的操作,支持非阻塞IO和选择器特性,适用于高并发、高吞吐量场景。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),能实现多路复用和异步操作。其应用场景涵盖网络通信、文件操作、进程间通信及数据库操作等。NIO的优势在于提高并发性和性能,简化编程;但学习成本较高,且与传统IO存在不兼容性。尽管如此,NIO在构建高性能框架如Netty、Mina和Jetty中仍广泛应用。
14 3
|
3天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
18 3
|
3天前
|
安全 算法 Java
Java CAS原理和应用场景大揭秘:你掌握了吗?
CAS(Compare and Swap)是一种乐观锁机制,通过硬件指令实现原子操作,确保多线程环境下对共享变量的安全访问。它避免了传统互斥锁的性能开销和线程阻塞问题。CAS操作包含三个步骤:获取期望值、比较当前值与期望值是否相等、若相等则更新为新值。CAS广泛应用于高并发场景,如数据库事务、分布式锁、无锁数据结构等,但需注意ABA问题。Java中常用`java.util.concurrent.atomic`包下的类支持CAS操作。
21 2
|
3天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
37 2
|
11天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
43 6
|
7月前
|
存储 安全 Java
深入理解Java并发编程:线程安全与锁机制
【5月更文挑战第31天】在Java并发编程中,线程安全和锁机制是两个核心概念。本文将深入探讨这两个概念,包括它们的定义、实现方式以及在实际开发中的应用。通过对线程安全和锁机制的深入理解,可以帮助我们更好地解决并发编程中的问题,提高程序的性能和稳定性。
|
4月前
|
存储 安全 Java
解锁Java并发编程奥秘:深入剖析Synchronized关键字的同步机制与实现原理,让多线程安全如磐石般稳固!
【8月更文挑战第4天】Java并发编程中,Synchronized关键字是确保多线程环境下数据一致性与线程安全的基础机制。它可通过修饰实例方法、静态方法或代码块来控制对共享资源的独占访问。Synchronized基于Java对象头中的监视器锁实现,通过MonitorEnter/MonitorExit指令管理锁的获取与释放。示例展示了如何使用Synchronized修饰方法以实现线程间的同步,避免数据竞争。掌握其原理对编写高效安全的多线程程序极为关键。
71 1