线程和线程池

一、知识结构及面试题目分析

多线程几乎也是 Java 高阶必考内容,内容比较发散,涉及到多线程、线程池、ThreadLocal、线程等等内容,这里选择若干高频问题进行讲解,更多的内容还可以参见扩展阅读部分内容。

二、典型面试例题及思路分析

问题 1:java 中线程有哪些状态?

共 6 种状态:

  • 初始状态 (NEW) :尚未启动的线程处于此状态。通常是新创建了线程,但还没有调用 start () 方法;
  • 运行状态 (RUNNABLE):Java 线程中将就绪(ready)和运行中(running)两种状态笼统的称为 “运行中”。比如说线程可运行线程池中,等待被调度选中,获取 CPU 的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得 CPU 时间片后变为运行中状态(running)。
  • 阻塞状态 (BLOCKED):表示线程阻塞于锁;
  • 等待状态 (WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断);
  • 超时等待状态 (TIMED_WAITING):进入该状态的线程需要等待其他线程在指定时间内做出一些特定动作(通知或中断),可以在指定的时间自行返回;
  • 终止状态 (TERMINATED):表示该线程已经执行完毕,已退出的线程处理此状态。
追问 1:线程状态 RUNNABLE 如何变为 BLOCKED?

线程状态变为阻塞有多种原因:可能调用 wait () 方法进入等待池,可能执行同步方法 / 同步代码块进入等锁池,可能调用了 sleep ()/join () 等待休眠或其他线程结束,可能发生了 I/O 中断等。

点评:

Java 线程状态转化如下所示:
图片描述

线程创建之后它将处于 NEW 状态,调用 start () 方法后开始运行,线程这时候处于 READY 状态。可运行状态的线程获得了 CPU 时间片后就处于 RUNNING 状态。

线程执行 wait () 方法后,进入 WAITING 状态,此时需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep()或 wait()可以将线程置于 TIME_WAITING 状态。当超时时间到达又会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED 状态。线程在执行 Runnable 的 run () 方法之后将会进入到 TERMINATED 状态。

问题 2:为什么要使用线程池?如何使用线程池?线程池有哪些核心参数?初始化线程池的大小的如何算?shutdown 和 shutdownNow 有什么区别?
  1. 为什么要使用线程池?

    (1) 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

    (2) 提高响应速度。 当任务到达时,任务可以不需要等到线程创建就能立即执行。

    (3) 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池可以认为是更普遍的 “池化资源” 技术的一个示例,其产生的原因是,由于创建 / 销毁对象需要对内存资源或者其他相关资源进行相对操作,所以某些消耗资源的对象的创建和销毁是比较费事的(java 虚拟机还需要管理每一个对象的生命周期),所以资源优化的一个方向就是减少创建和销毁对象的次数,尽可能地复用。比如说数据库连接池;

  1. 如何使用线程池?
    工具类 Executors 提供了静态工厂方法以生成常用的线程池:

    (1) newSingleThreadExecutor:创建一个单线程的线程池。如果该线程因为异常而结束,那么会有一个新的线程来替代它。

    (2) newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大值,一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

    (3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(默认 60 秒不执行任务)的线程。当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。

    (4) newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

  2. java 线程池的核心参数

    Executors 也提供自定义的线程池构造方法,里面包括七个参数:

public ThreadPoolExecutor(  int corePoolSize,   //常驻线程数,即使空闲时仍保留在池中的线程数  
 int maximumPoolSize, //线程池中允许的最大线程数   
 long keepAliveTime,   // 存活时间。线程数比corePoolSize多且处于闲置状态的情况下,这些闲置的线程能存活的最大时间,为0表示会立即回收;  
 TimeUnit unit,  //keepAliveTime的单位  
 BlockingQueue<Runnable> workQueue, //被提交尚未被执行的任务阻塞队   
 ThreadFactory threadFactory, // 创建线程的工厂  
 RejectedExecutionHandler handler  // 饱和拒绝策略,当队列满了并且线程个数达到maximunPoolSize后采取的策略。目前支付四种:AbortPolicy(抛出异常),CallerRunsPolicy(调用者线程处理),DiscardOldestPolicy(直接丢弃任务,不予处理也不抛出异常),DiscardPolicy(默默丢弃,不抛出异常)  
 ) 
  1. 如何设置初始化线程池的大小?

    可根据线程池中的线程处理任务的不同进行分别估计:

    (1) CPU 密集型任务,这类任务需要大量的运算,通常 CPU 利用率很高,无阻塞,因此应配置尽可能少的线程数量,可设置为 CPU 核数 + 1;

    (2) IO 密集型任务,这类任务有大量 IO 操作,伴随着大量线程被阻塞,可配置更多的线程数,通常可设置 CPU 核心数 * 2;

这个问题其实并非要候选人提出一个天衣无缝的初始线程数,这在实际工程中既无必要(可以逐渐优化调整),也很难实施(服务器上不止一个 java 应用),而是考察候选人结构化的思维能力,针对一个相对陌生或者相对复杂的问题,如何去分析思考。因此回答这类题,需要做到有据可循、言之有物。简单地说,就是能圆回来。针对这类问题有一个简单的技巧,就是先分类,然后针对不同的分类分别回答,这样结构会更清晰,考虑也更全面,而且一般这种偏开放式的问题,本身也很难说有一个答案可以覆盖所有场景。

  1. shutdown 和 shutdownNow 有什么区别?
    shutdown,有序停止。将线程池状态设置为 SHUTDOWN

    (1) 停止接收外部提交的任务;

    (2) 先前提交的任务务会执行(但不保证完成执行);

    shutdownNow,尝试立即停止,将线程池状态设置为 STOP,相对来说是一种更优雅的关闭方法。

    (1) 停止接收外部提交的任务;

    (2) 不再处理队列里等待的任务;

    (3) 忽略队列里等待的任务;

    (4) 返回正在等待执行的任务列表;

shutdownNow 试图取消线程的方法是通过调用 Thread.interrupt () 方法来实现的,非强制的,如果线程中没有 sleep/wait 等应用,interrupt () 方法是无法中断当前的线程的。所以,ShutdownNow 并不代表线程池就一定立即就能退出,它也可能必须要等待所有正在执行的任务都执行完成了才能退出,但是大多数时候是能立即退出的
点评:

这是典型的线程池面试题,采用一环扣一环 / 层层递进的追问问法,目的在于有效评测候选者的知识深度,有时候会追问到候选者答不上为止。对应的方式就是要熟练掌握相关类(特别是 Executors 类)的用法和代码,最好平常有使用经验。常见的其他考察还有任务执行、线程池容量动态调整等内容,可以参见扩展阅读部分内容。

问题 3:ThreadLocal 的作用是什么?有什么风险?
  1. ThreadLocal 的作用是什么?
    提供每个线程存储自身专属的局部变量。

    实现原理:
    每个 Thread 维护着一个 ThreadLocalMap 的引用,ThreadLocalMap 是 ThreadLocal 的内部类,用 Entry 来进行存储。
    调用 ThreadLocal 的 set () 方法时,实际上就是往 ThreadLocalMap 设置值,key 是 ThreadLocal 对象,值是传递进来的对象;调用 ThreadLocal 的 get () 方法时,实际上就是往 ThreadLocalMap 获取值,key 是 ThreadLocal 对象 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。因为这个原理,所以 ThreadLocal 能够实现 “数据隔离”,获取当前线程的局部变量值,不受其他线程影响。

  2. 有什么风险?
    内存泄漏。ThreadLocal 被 ThreadLocalMap 中的 entry 的 key 弱引用,如果 ThreadLocal 没有被强引用, 那么 GC 时 Entry 的 key 就会被回收,但是对应的 value 却不会回收。就会造成内存泄漏。 ​ 解决方案,每次使用完 ThreadLocal,都调用它的 remove () 方法,清除数据。
    图片描述

三、总结

同为高阶必考面试题,多线程和线程池面试题比 JVM 更灵活一些,本身知识点比较多,理解起来也更困难,而且想在实践中验证也更困难。不过万变不离其宗,对内要修练 “内功”,深入掌握 JDK 源码(见下图),灵活理解; 对外则多在项目中实践。
图片描述

四、扩展阅读及思考题

问:什么是进程?

是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。

问:什么是线程?

线程是操作系统能够进行运算调度的最小单位。

它被包含在进程之中,是进程中的实际运作单位。

一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

问:进程和线程的区别?

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响。

线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。

但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

问:多线程和单线程有什么区别?

单线程程序:程序执行过程中只有一个有效操作的序列,不同操作之间都有明确的执行先后顺序,容易出现代码阻塞

多线程程序:有多个线程,线程间独立运行,能有效地避免代码阻塞,并且提高程序的运行性能

问:为什么要使用多线程?

1、使用多线程可以减少程序的响应时间。 在单线程的情况下,如果某个程序很耗时或者陷入长时间等待(如等待网络响应),此时程序将不会相应鼠标和键盘等操作,使用多线程后,可以把这个耗时的线程分配到一个单独的线程去执行,从而是程序具备了更好的交互性。

2、与进程相比,线程的创建和切换开销更小。 由于启动一个新的线程必须给这个线程分配独立的地址空间,建立许多数据结构来维护线程代码段、数据段等信息,而运行于同一个进程内的线程共享代码段、数据段,线程的启动或切换的开销就比进程要少很多。同时多线程在数据共享方面效率非常高。

3、多CPU或多核心计算机本身就具有执行多线程的能力。 如果使用单个线程,将无法重复利用计算机资源,造成资源的巨大浪费。因此在多CPU计算机上使用多线程能提高CPU的利用率。

4、使用多线程能简化程序的结构,使用程序便于理解和维护。 一个非常复杂的进程可以分成多个线程来执行。

问:什么是线程安全?

当多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替运行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获取正确的结果,那这个对象是线程安全的。

问:为何要使用线程同步?

Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突。

因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

问:如何确保线程安全?

1、对非安全的代码进行加锁控制;

2、使用线程安全的类;

3、多线程并发情况下,线程共享的变量改为方法级的局部变量

问:线程安全的级别?

1、不可变。不可变的对象一定是线程安全的,并且永远也不需要额外的同步。

Java类库中大多数基本数值类如Integer、String和BigInteger都是不可变的。

2、无条件的线程安全。由类的规格说明所规定的约束在对象被多个线程访问时仍然有效,不管运行时环境如何排列,线程都不需要任何额外的同步。

如 Random 、ConcurrentHashMap、Concurrent集合、atomic

3、有条件的线程安全。有条件的线程安全类对于单独的操作可以是线程安全的,但是某些操作序列可能需要外部同步。

有条件线程安全的最常见的例子是遍历由 Hashtable 或者 Vector 或者返回的迭代器

4、非线程安全(线程兼容)。线程兼容类不是线程安全的,但是可以通过正确使用同步而在并发环境中安全地使用。

如ArrayList HashMap

5、线程对立。线程对立是那些不管是否采用了同步措施,都不能在多线程环境中并发使用的代码。

如如System.setOut()、System.runFinalizersOnExit()

问:为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

new 一个 Thread,线程进入了新建状态(NEW);调用 start() 方法,会启动一个线程并使线程进入了就绪状态(READY),当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

问:使用多线程可能带来什么问题?

多线程的目的就是为了能提高程序的执行效率提高程序运行速度,但也可能会遇到很多问题:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。

问:线程池有哪些状态?

线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。

  1. RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务;
  2. SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 3 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。;
  3. STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态;
  4. TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。
  5. TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。

线程流转如下:
在这里插入图片描述

注意线程池的状态和线程的状态

问:线程池中 execute()和submit()方法有什么区别?

1、execute方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;

2、submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

问:实现Runnable接口和Callable接口的区别?

两者的区别在于 Runnable 接口不会返回结果但是 Callable 接口可以返回结果。

备注: 工具类Executors可以实现Runnable对象和Callable对象之间的相互转换。(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。

问: 创建线程有哪些方式?

1、继承 Thread 类创建线程类;

2、通过 Runnable 接口创建线程类。

3、通过 Callable 和 Future 创建线程。

4、线程池

问:线程池的拒绝策略有哪些?

1、AbortPolicy:默认的拒绝策略,直接抛出 RejectedExecutionException 异常;

2、CallerRunsPolicy:既不会抛弃任务,也不会抛出异常,由调用线程处理该任务;

3、DiscardPolicy: 直接丢弃任务,不予处理也不抛出异常。如果允许任务丢失,是最好的处理策略。

4、DiscardOldestPolicy: 抛弃队列中等待最久的任务,然后把当前任务加入队列尝试再次提交(可能会再次失败,导致重复)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值