线程并发相关面试题
1. 线程的生命周期?线程有几种状态
线程的生命周期线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
Java线程具有五种基本状态
新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable):当调用线程对象的start()方法t.start();,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
2. sleep()、wait()、join()、yield()的区别
sleep() 方法是属于Thread类中的,sleep() 过程中线程不会释放锁,只会阻塞线程,让出cpu给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态,可中断,sleep() 给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会
wait() 方法是属于 Object 类中的,wait() 过程中线程会释放对象锁,只有当其他线程调用 notify() 才能唤醒此线程。wait() 使用时必须先获取对象锁,即必须在 synchronized 修饰的代码块中使用,那么相应的 notify() 方法同样必须在 synchronized 修饰的代码块中使用,如果没有在synchronized 修饰的代码块中使用时运行时会抛出IllegalMonitorStateException的异常
等待调用join()方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。例如:主线程创建并启动了子线程,如果子线程中要进行大量耗时运算计算某个数据值,而主线程要取得这个数据值才能运行,这时就要用到 join() 方法了
yield()和 sleep() 一样都是 Thread 类的方法,都是暂停当前正在执行的线程对象,不会释放资源锁,和 sleep() 不同的是 yield()方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。还有一点和 sleep() 不同的是 yield() 方法只能使同优先级或更高优先级的线程有执行的机会
3. 对线程安全的理解
一个比较恰当的定义:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。”。
线程安全的类型
1、不可变
Java 语言中,如果共享数据是一个基本数据类型,那么只要在定义时使用 final 关键字修饰它就可以保证它是不可变的。如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行,如果你不明白这句话,不妨想一想 java.lang.String 类的对象,它是一个典型的不可变对象,我们调用它的 substring()、replace() 和 concat() 这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。
2、绝对线程安全
一个类要达到 “不管运行环境如何,调用者都不需要任何额外的同步措施” 通常需要付出很大的,甚至有时候是不切实际的代价。jdk api中标注自己是线程安全的类中的大多数,都不是绝对安全的,而是相对安全。
3、相对线程安全
相对安全指的是对某个对象的单个操作是线程安全的,但对于某些特定顺序的组合操作,就可能需要在调用端使用额外的同步手段来保证调用的正确性。java中大部分的所谓线程安全类都是这个类型的,例如Vector,HashTable等。可通过再加一层互斥锁来实现线程安全。
4、线程兼容
线程兼容指的是对象本身不是线程安全的,但可以通过在调用端正确的使用同步手段来保证在并发环境中可以安全的使用。我们平时说的某个类是线程不安全的,绝大多数时候就是说的这种情况,例如ArrayList和HashMap等。
5、线程对立
线程对立指的是无论调用端是否采取了同步措施,都无法在多线程环境中保证数据的安全。java中这种代码是很少的,而且通常是有害的,应尽量避免。一个例子就是Thread类的suspend()跟resume()方法。两个线程同时持有一个对象,一个尝试去中断线程,一个尝试去恢复线程,无论调用时是否进行了同步,目标线程都存在死锁的风险。也正是这个原因,这俩方法已经被jdk废弃了。
4. Thread、Runnable的区别
并没有本质区别,但最好实现Runnable接口,毕竟只能继承一个类
实现Runnable接口比继承Thread类所具有的优势:
1):更适合多个相同的程序代码的线程去处理同一个资源:从Thread源码中也可以看到,当以Thread方式去实现资源共享时,实际上源码内部是将thread向下转型为了Runnable,实际上内部依然是以Runnable形式去实现的资源共享

2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
5. 对守护线程的理解
当 JVM 中不存在任何一个正在运行的非守护线程时,则 JVM 进程即会退出。
故若存在一个非守护线程一直不退出的情况,即使主线程退出,JVM进程依旧不能退出。但若将该线程设为守护线程,则可正常退出。即添加一句话:thread.setDaemon(true);
守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。
JVM 中的垃圾回收线程就是典型的守护线程,如果说不具备该特性,会发生什么呢?
当 JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬了!!!由此可见,守护线程的重要性了。
通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。
6. ThreadLocal的原理和使用场景
直接定位到 ThreadLocal 的源码,可以看到源码注释中有很清楚的解释:它是线程的局部变量,这些变量只能在这个线程内被读写,在其他线程内是无法访问的。 ThreadLocal 定义的通常是与线程关联的私有静态字段(例如,用户ID或事务ID)。
原理:
首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。
因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个 ThreadLocalMap 类对应的 get()、set() 方法。例如下面的 set 方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
调用 ThreadLocal 的 set 方法时,首先获取到了当前线程,然后获取当前线程维护的 ThreadLocalMap 对象,最后在ThreadLocalMap 实例中添加上。如果 ThreadLocalMap 实例不存在则初始化并赋初始值。
这里看到 set 方法的第一个参数是 this ,this即指的是当前的 ThreadLocal 对象,回看代码就是指的 mLocal 这个对象。而在 ThreadLocalMap 的 set 方法中会根据当前 ThreadLocal 对象实例,做一些操作和判断,最终实现赋值操作(具体参考源码)。
所以说,最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是一个中间工具,传递了变量值。
使用场景:
1、比如线程中处理一个非常复杂的业务,可能方法有很多,那么,使用 ThreadLocal 可以代替一些参数的显式传递;
2、比如用来存储用户 Session。Session 的特性很适合 ThreadLocal ,因为 Session 之前当前会话周期内有效,会话结束便销毁。但是例如 tomcat 这类的服务器软件都是采用了线程池技术的,并不是严格意义上的一个会话对应一个线程。并不是说这种情况就不适合 ThreadLocal 了,而是要在每次请求进来时先清理掉之前的 Session ,一般可以用拦截器、过滤器来实现。
3、在一些多线程的情况下,如果用线程同步的方式,当并发比较高的时候会影响性能,可以改为 ThreadLocal 的方式,例如高性能序列化框架 Kyro 就要用 ThreadLocal 来保证高性能和线程安全;
4、还有像线程内上线文管理器、数据库连接等可以用到 ThreadLocal;
7. ThreadLocal内存泄露原因,如何避免
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap 中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。
8. 并发、并行、串行的区别
并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
并行:指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
串行:指在同一时刻,只有一条指令在执行,待其执行完后才能进行下一条指令。
9. 并发的三大特性
原子性、可见性、有序性
1. 原子性
-
含义
一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。
在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作。
-
重要
不采取任何的原子性保障措施的自增操作并不是原子性的。
- 如何保证原子性
- 通过 synchronized 关键字定义同步代码块或者同步方法保障原子性。
- 通过 Lock 接口保障原子性。
- 通过 Atomic 类型保障原子性。
2. 可见性
-
含义:
当一个线程修改了共享变量的值,其他线程能够看到修改的值。
Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。
-
volatile 变量和普通变量区别:
普通变量与 volatile 变量的区别是 volatile 的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新,因此我们可以说 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
- 如何保证可见性
- 通过 volatile 关键字标记内存屏障保证可见性。
- 通过 synchronized 关键字定义同步代码块或者同步方法保障可见性。
- 通过 Lock 接口保障可见性。
- 通过 Atomic 类型保障可见性。
- 通过 final 关键字保障可见性
3. 有序性
-
含义
即程序执行的顺序按照代码的先后顺序执行。
JVM 存在指令重排,所以存在有序性问题。
如何保证有序性
- 通过 synchronized关键字 定义同步代码块或者同步方法保障有序性。
- 通过 Lock接口 保障有序性。
10. volatile
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
-
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
-
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
保证可见性,不保证原子性,一定程度上保证有序性。
-
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
11. 为什么用线程池?解释下线程池参数?
一、为什么用线程池
-
降低资源消耗:提高线程利用率,降低创建和销毁线程的消耗。
-
提高响应速度:任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
-
提高线程的可管理性:线程是稀缺资源
二、线程池参数解释
-
corePoolSize:代表核心线程数,也就是正常情况下创建工作的线程数,这些线程数创建后并不会消除,而是一种常驻线程。
-
maxinumPoolSize:代表的最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用过了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数。
-
keepAliveTime、unit:表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除。我们可以通过setKeepAliveTime来设置空闲时间。
-
workQueue:用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则会全部放入队列,直到整个队列被放满但任务还在持续进入则会开始创建新的线程。
-
ThreadFactory:实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来制定不同的线程工厂。
-
Handle:任务拒绝策略,有两种情况,第一种是当我们调用shutdown等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续向线程池提交任务就会遭到拒绝。另外一种情况就是当达到最大线程数,线程池已经没有能力处理新提交的任务时,也会拒绝。
12. 简述线程池处理流程

13. 线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?
-
一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
阻塞队列自带阻塞和唤醒功能,不需要做额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一直占用CPU资源。
-
在创建新线程的时候,是要获取全局锁的,这个时候其他的就需要阻塞,影响了整体效率。
就好比一个企业里面有十个(core)正式工的名额,最多招十个正式工(核心线程),要是任务超过正式人数(task>core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这十个人,但是任务可以稍积压一下。即先放到
队列中去(代价低)。十个正式工慢慢干,迟早会干完的,如果任务还在持续增加,超过正式工的加班忍耐极限了(队列满了),就招外包(非核心线程)帮忙了,还是正式工加外包还不能完成任务,那么新来的任务就会被领导拒绝(线程池拒绝策略)。
14. 线程池中线程复用原理
线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前Thread创建线程时一个线程必须对应一个任务的限制。
在线程池中,同一个线程可以从阻塞队列中不断的获取新任务执行,其核心原理在于线程池对Thread进行了封装,并不是每一次调用线程都会调用Thread.start()来创建新线程,而是让每个线程去执行循环任务,在这个循环任务中不停检查是否有任务需要被执行,如果有,直接执行,也就是调用任务中的run方法,将run方法当成一个普通方法执行,通过这种方式只使用固定的线程就将所有任务run方法串联起来了。

浙公网安备 33010602011771号