线程池专题(下)

11.线程池线程数如何确定
一般多线程执行的任务类型可以分为 CPU 密集型和 I/O 密集型,根据不同的任务类型,我们计算线程数的方法也不一样。

CPU 密集型任务:这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

当线程数量太小,同一时间大量请求将被阻塞在线程队列中排队等待执行线程,此时 CPU 没有得到充分利用;当线程数量太大,被创建的执行线程同时在争取 CPU 资源,又会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。

I/O 密集型任务:这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N

12.常用的线程池工作队列

1、ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue
一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
3、SynchronousQueue(同步队列)
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
4、PriorityBlockingQueue
一个具有优先级的无限阻塞队列。
5.DelayQueue 延时队列

ArrayBlockingQueue

特点:

1.初始化一定容量的数组 使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥
2.是有界设计,如果容量满无法继续添加元素直至有元素被移除
3. 使用时开辟一段连续的内存,如果初始化容量过大容易造成资源浪费,过小易添加失败

LinkedBlockingQueue
特点:

1.内部使用节点关联,会产生多一点内存占用
2.使用两个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待
3.有边界的,在默认构造方法中容量是Integer.MAX_VALUE
4. 非连续性内存空间

DelayQueue
特点:

1.无边界设计 添加(put)不阻塞,移除阻塞
2.元素都有一个过期时间 取元素只有过期的才会被取出

SynchronousQueue
特点:

1.内部容量是0 每次删除操作都要等待插入操作 每次插入操作都要等待删除操作
2.一个元素,一旦有了插入线程和移除线程,那么很快由插入线程移交给移除线程
3.这个容器相当于通道,本身不存储元素
4.在多任务队列,是最快的处理任务方式

PriorityBlockingQueue
特点:

无边界设计,但容量实际是依靠系统资源影响 添加元素,如果超过1,则进入优先级排序

13.线程创建方式?
Java可以用四种方式来创建线程,如下所示:

1)继承Thread类创建线程

2)实现Runnable接口创建线程

3)使用Callable和Future创建线程

4)使用线程池例如用Executor框架

1.通过继承Thread类来创建并启动多线程的一般步骤如下

1】定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。

2】创建Thread子类的实例,也就是创建了线程对象

3】启动线程,即调用线程的start()方法

代码实例

public class MyThread extends Thread{//继承Thread类
 
  public void run(){
  //重写run方法
 
  }
 
}
 
public class Main {
  public static void main(String[] args){
    new MyThread().start();//创建并启动线程
 
  }
 
}

2.实现Runnable接口创建线程
通过实现Runnable接口创建并启动线程一般步骤如下:

1】定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体

2】创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象

3】第三部依然是通过调用线程对象的start()方法来启动线程

public class MyThread2 implements Runnable {//实现Runnable接口
 
  public void run(){
  //重写run方法
 
  }
 
}
 
public class Main {
  public static void main(String[] args){
    //创建并启动线程
 
    MyThread2 myThread=new MyThread2();
 
    Thread thread=new Thread(myThread);
 
    thread().start();
 
    //或者    new Thread(new MyThread2()).start();
 
  }
 
}

3.使用Callable和Future创建线程
和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。

1.call()方法可以有返回值

2.call()方法可以声明抛出异常

Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。

创建并启动有返回值的线程的步骤如下:

1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。

2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值

3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)

4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

4.使用线程池例如用Executor框架

Executor框架的内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作

在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象用Executor在构造器中。

14.创建线程池的方式?

线程池的创建⽅法总共有 7 种,但总体来说可分为 2 类:

线程池的创建⽅式总共包含以下 7 种(其中 6 种是通过 Executors 创建的, 1 种是通过
ThreadPoolExecutor 创建的):

  1. Executors.newFixedThreadPool:创建⼀个固定⼤⼩的线程池,可控制并发的线程数,超出的线程会在队列中等待;
  2. Executors.newCachedThreadPool:创建⼀个可缓存的线程池,若线程数超过处理所需,缓存⼀段时间后会回收,若线程数不够,则新建线程;
  3. Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执⾏顺序;
  4. Executors.newScheduledThreadPool:创建⼀个可以执⾏延迟任务的线程池;
  5. Executors.newSingleThreadScheduledExecutor:创建⼀个单线程的可以执⾏延迟任务的线程池;
  6. Executors.newWorkStealingPool:创建⼀个抢占式执⾏的线程池(任务执⾏顺序不确定)【JDK1.8 添加】。
  7. ThreadPoolExecutor:最原始的创建线程池的方式

单线程的线程池又什么意义?

    1. 复用线程。
    2. 单线程的线程池提供了任务队列和拒绝策略(当任务队列满了之后(Integer.MAX_VALUE),新来的任务就会拒绝策略)

15.如果队列中核心线程池满了,工作队列没满,有新的任务,会怎么做?

会加入到阻塞队列中,原因如下:
因为线程池的执行过程,可以分为三个主要步骤:

1.提交任务后会首先进行当前工作线程数与核心线程数的比较,如果当前工作线程数小于核心线程数,则直接调用 addWorker() 方法创建一个核心线程去执行任务;

2.如果工作线程数大于核心线程数,即线程池核心线程数已满,则新任务会被添加到阻塞队列中等待执行,当然,添加队列之前也会进行队列是否为空的判断;

3.如果线程池里面存活的线程数已经等于核心线程数了,且阻塞队列已经满了,再会去判断当前线程数是否已经达到最大线程数 maximumPoolSize,如果没有达到,则会调用 addWorker() 方法创建一个非核心线程去执行任务;

4.如果当前线程的数量已经达到了最大线程数时,当有新的任务提交过来时,会执行拒绝策略

总结来说就是优先核心线程、阻塞队列次之,最后非核心线程。

16.线程池启动过程?拒绝策略?CallerRunsPolicy 什么场景适合?如果我们不想丢掉任何任务怎么办?
启动过程:

1)当线程池刚创建时,线程池内是无线程的状态,任务队列是以参数的形式传入线程池的,线程池不会立即执行队列中的任务。
2)线程池在启动任务时会先做以下判断: a、如果正在运行的线程数量小于corePoolSize,则立马创建相关任务。
b、如果正在运行的线程数量大于或等于corePoolSize,则将任务放入任务队列等待。
c、如果队列已满,且正在运行的线程数量小于maximumPoolSize,则创建线程执行任务。
d、如果队列已满,且正在运行的线程数量大于或等于maximumPoolSize,则抛异常。
3)当一个线程完成任务后,会到队列中取任务继续执行。
4)当队列为空时,线程的存活时间超过keepAliveTime,如果当前运行的线程数大于corePoolSize则这个线程会被销毁,故而线程池内所有的任务都完成后,线程的数量会缩到corePoolSize的大小。

拒绝策略

1.AbortPolicy - 抛出异常,中止任务。抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行
2.CallerRunsPolicy - 不进入线程池执行,任务将由调用者线程(提交任务的线程)去执行。由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
(用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。)
3.DiscardPolicy - 直接丢弃
4.DiscardOldestPolicy - 丢弃队列最老任务,添加新任务。当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入

CallerRunsPolicy 什么场景适合?

用于被拒绝任务的处理程序

如果我们不想丢掉任何任务怎么办?

1.加快消费速度减少任务排队,降低丢失风险;
2.如果并发量不大的话,可以考虑在拒绝策略中交由主线程完成,主线程会因此降低任务提交速度,进而降低线程池负担,形成反馈环
3.如果并发量大的话,可以重写拒绝策略,将被拒绝的任务以某种形式进行持久化,随后再次处理,这也是最稳健的方案,
4.可以把这些被拒绝执行的任务收集起来:可以考虑使用缓存容器,并且是并发安全的缓存容器,所以可以使用juc包下的java.util.concurrent.LonkedBlockingQueue

17.线程池的创建,java 自带的 4 种,threadFactory 能干啥?线程池设置守护线程从这里设置吗

java自带的四种线程池?

newSingleThreadExexcutor:单线程数的线程池(核心线程数=最大线程数=1)
newFixedThreadPool:固定线程数的线程池(核心线程数=最大线程数=自定义)
newCacheThreadPool:可缓存的线程池(核心线程数=0,最大线程数=Integer.MAX_VALUE)
newScheduledThreadPool:支持定时或周期任务的线程池(核心线程数=自定义,最大线程数=Integer.MAX_VALUE)

threadFactory 能干啥?

1.设置线程名称
2.为线程设置优先级
ThreadFactory是一个接口,在接口中定义了一个newThread(Runnable r)的方法,用来创建一个线程。
其他线程实现ThreadFactory这个接口,从而为这个线程命名,并且能够设置线程的优先级
代码:

public interface ThreadFactory {
    Thread newThread(Runnable r);
}

线程池设置守护线程从这里设置吗?

可以从线程池设置,也可以用户在编写程序时自己设置守护线程
守护线程:

JAVA中的线程主要分为两类:用户线程(User Thread)和守护线程(Daemon Thread)。
JAVA语言中无论是线程还是线程池,默认都是用户线程,因此用户线程也被称为普通线程。
守护线程被称之为后台线程,它是为用户线程服务的,当线程中的用户线程都执行结束后,守护线程也会跟随结束,并且守护线程具有自动结束生命周期的特性,而非守护线程则不具备该特性。
注意事项:

  1. setDaemon(true) 必须写在start方法前面;
    setDaemon(true) 如果设置在 start() 之后,不但程序的执行会报错,而且设置的守护线程也不会生效。
    2.不能把正在运行中的线程设置为守护线程;

18.线程池的原理?阻塞队列的实现?线程池的好处?线程池的源码?

19线程池状态以及状态是如何变化的(ctl高三位)?线程的状态以及如何变化的?java的线程池怎么设计?为什么要区分核心和最大线程?

20.线程六大状态?线程有 running 这个状态吗 ?
怎么样变成等待状态?创建线程的方式?为什么推荐使用线程池创建?为什么不推荐使用 Executors 的静态方法去创建 ?这个问的其实就是为什么不推荐去创建单
例线程池、缓存线程池等那四种,但是当时没听懂 Executors 的静态方法是什么意思

1.六大状态:

  1. New 新建状态(线程刚被创建,start方法之前的状态)
  2. Runnable 运行状态(得到时间片运行中状态)(Ready就绪,未得到时间片就绪状态)
  3. Blocked 阻塞状态(如果遇到锁,线程就会变为阻塞状态等待另一个线程释放锁)
  4. Waiting 等待状态(无限期等待)
  5. Time_Waiting 超时等待状态(有明确结束时间的等待状态)
  6. Terminated 终止状态(当线程结束完成之后就会变成此状态)
    如图:
    在这里插入图片描述
    详解:
  1. 初始状态(NEW)

实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

2.1. 就绪状态(RUNNABLE之READY)

  1. 就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。

2.调用线程的start()方法,此线程进入就绪状态。
3 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
4.当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
5. 锁池里的线程拿到对象锁后,进入就绪状态。

2.2. 运行中状态(RUNNABLE之RUNNING)

线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。

  1. 阻塞状态(BLOCKED)

阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态

  1. 等待(WAITING)

处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

  1. 超时等待(TIMED_WAITING)

处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

  1. 终止状态(TERMINATED)

1.当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。

2.线程一旦终止了,就不能复生。 在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

2.线程有 running 这个状态吗 ?

没有,java是runnable

3.怎么样变成等待状态?

1.Object.wait()
当一个线程拥有一个对象的监听器时,我们可以暂停它的执行,直到另一个线程完成工作并使用 notify() 方法将其唤醒。
2.Tread.join()

Join :一个同步方法,该方法阻止调用线程 (即调用方法的线程) ,直到 Join 调用方法的线程完成。 使用此方法可以确保线程已终止。
如果线程未终止,调用方将无限期阻止

我们可以用来暂停线程执行的另一种方法是通过 join() 调用。 当我们的主线程需要等待工作线程首先完成时,我们可以从主线程调用工作线程实例上的 join() 方法将主线程暂停,主线程将进入 WAITING 状态
3.LockSupport.park()

还有LockSupport 类的 park() 静态方法可以将线程设置为 WAITING 状态, 调用 park() 将停止当前线程的执行并将其置于 WAITING 状态

4.创建线程的方式?

方式1:通过继承Thread类创建线程
方式2:通过实现Runnable接口创建线程
方式3:使用Callable和Future来创建线程
方法4:通过线程池来创建线程

5.为什么推荐使用线程池创建?为什么不推荐使用 Executors 的静态方法去创建 ?这个问的其实就是为什么不推荐去创建单
例线程池、缓存线程池等那四种,但是当时没听懂 Executors 的静态方法是什么意思
**

这几个构造方法的底层都是直接用了ThreadPoolExecutor,只不过是不同实现的时候对ThreadPoolExecutor的构造参数做了特殊处理,如下所示。

newFixedThreadPool:
        构造方式为new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())
        问题:设置了corePoolSize=maxPoolSize,keepAliveTime=0(此时该参数没作用),无界队列,任务可以无限放入,当请求过多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致占用过多内存或直接导致OOM异常

newSingleThreadExector:
       构造方式为new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), var0)
       问题:基本同newFixedThreadPool,但是将线程数设置为了1,单线程,弊端和newFixedThreadPool一致

newCachedThreadPool:
        构造方式为new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue())
        问题:corePoolSize=0,maxPoolSize为很大的数,同步移交队列,也就是说不维护常驻线程(核心线程),每次来请求直接创建新线程来处理任务,也不使用队列缓冲,会自动回收多余线程,由于将maxPoolSize设置成Integer.MAX_VALUE,当请求很多时就可能创建过多的线程,导致资源耗尽OOM

不推荐直接使用Executors创建线程池的4种方法(如newFixedThreadPool),而是直接使用ThreadPoolExecutor来创建线程池,目的就是为了让开发者根据业务实际情况来创建合适的线程池,避免默认线程池的各种各样问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值