自己整理的一些多线程面试题,持续更新
多线程编程的好处
程序中启用多个线程并发执行以提高程序的效率,多个线程共享heap memory,创建多个线程充分利用CPU资源,创建多个线程执行任务比创建多个进程要好
用户线程和守护线程
用户线程是用户在java程序中创建的线程,称为用户线程;
守护线程是程序在后台执行且并不会阻止JVM终止的线程,当没有用户线程运行的时候,JVM关闭程序并且推出,但守护线程仍然继续执行;守护线程创建的子线程依然是守护线程
守护线程简介
- 守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程;如果用户线程已经全部退出运行了,只剩下守护线程存在,虚拟机也就退出了
- 在线程启动之前,通过setDaemon(true)方法来将线程设置为守护线程:
daemonThread.setDaemon(true);
- 守护线程的优先级很低,不能安排重要的任务
- 当全部用户线程结束时,虚拟机推出,守护线程也自动结束
简述线程生命周期
- java线程生命周期包括新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态
- 5个生命周期描述:
1. 新建(New):当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,JVM为其分配内存并初始化成员变量
2. 就绪(Runnable):调用线程的start()方法后,线程就处于就绪状态,JVM会为其创建方法调用栈和程序计数器,等待调度运行
3. 运行(Running):就绪中的线程获得了CPU时间片,开始执行run()方法的线程体
4. 阻塞(Blocked):当发生如下情况时,线程将会进入阻塞状态;从阻塞状态只能进入Runnable状态,无法直接进入Running状态
```
1 线程调用sleep()方法主动放弃所占用的处理器资源
2 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
3 线程试图获得一个同步监视器(即对象的同步锁),但该同步监视器正被其他线程所持有
4 线程中运行的对象调用了wait()方法,线程进入了等待队列,在等待某个通知(notify or notifyALl())
5 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法
```
5. 死亡(Dead):线程会以如下3种方式结束,结束后就处于死亡状态:
```
1 run()或call()方法执行完成,线程正常结束
2 线程抛出一个未捕获的Exception或Error
3 直接调用该线程stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用
```
线程生命周期示意图:
简单描述这三个方法wait(), notify()和notifyAll()三个方法
- java线程并没有适用于所有对象的锁和同步器,因此需要Object类自己拥有这样的方法来保证线程间通信
- 当前线程调用对象的wait()方法,释放该对象的同步锁,进入该对象的wait queue;该方法必须在对象的同步方法和同步块中被调用;当其他线程调用notify()或notifyAll()时,wait queue中的某个线程或者全部线程被唤醒,进入对象的锁池;唤醒后的线程何时能够获得对象锁,完全凭运气
什么是上下文切换(context-switching)
存储和恢复CPU状态的过程,使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。
yield()方法简介
- 当前线程调用此方法,放弃获取的CPU时间片,由Running状态变回Runnable状态,让系统再次选择线程
- 实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中
- yield()方法不会导致线程Blocked
- 该方法是Thread的静态方法,当前线程调用Thread.yield()使其让出CPU时间片
sleep()方法简介
- 当前线程调用Thread.sleep(long millis)方法强制该线程休眠,进入Blocked状态
- 线程进入休眠后,不会释放之前已经获得的同步锁
- 休眠一定时间后线程回到Runnable状态
简单介绍Thread对象的join()方法
- 使用场景:如果需要让子线程subThread执行完再结束主线程,那么可以在main方法中直接调用子线程subThread.join()方法,让主线程进入wait状态,让subThread执行完,再重新执行主线程
- 原理:API中叙述如下:
public final void join() throws InterruptedException Waits for this thread to die. Throws: InterruptedException - if any thread has interrupted the current thread. The interrupted status of the current thread is cleared when this exception is thrown.
意思就是说,join()可以让调用这个方法的线程进入wait状态直到子线程结束
从join()的JDK源码角度来理解:
//这个方法是个同步方法,就是说调用子线程的父线程必须拿到子线程对象的锁才能使join()起作用
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis(); //调用开始时间
long now = 0;
if (millis < 0) {
//join方法中传入的毫秒值不能为负,否则报错
throw new IllegalArgumentException("timeout value is negative");
}
//我们可以看到这里使用了while循环做判断的,然后调用wait方法的,所以说join方法的执行是完全通过wait方法实现的
//等待时间为0的时候,就是无限等待,直到线程死亡了(即线程执行完了)
if (millis == 0) {
//join()传入的毫秒值默认为0
while (isAlive()) {
//只有当子线程为就绪、运行或阻塞状态时返回ture,新建但未start或者死亡状态返回false
//调用该线程的join方法的父线程拿到锁之后,进入等待队列(wait queue),直到子线程执行结束(即子线程的isAlive()方法返回false)
wait(0);
}
} else {
//如果父线程调用join()方法时传入了特定的毫秒值
while (isAlive()) {
//同样是子线程状态为就绪、运行或阻塞状态时返回ture
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
//父线程在等待队列中先等待delay时间,等delay时间过了就恢复(前提是子线程还没结束)
}
}
}
综上所述,父线程调用子线程的join方法目的就是让父线程暂停执行,待子线程结束后再恢复;或者制定join某个时间,当到时间后,不管子线程有没有执行网,父线程都会恢复
如何保证线程安全
- 同步锁
- 使用原子类(atomic concurrent class):该类位于java.util.concurrent.atomic包中,这些类保证在多线程环境下,当某个线程执行atomic的方法时不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个线程执行。Atomic类在软件层面上是非阻塞的,它的原子性其实是在硬件层面上借助相关的指令来保证的。使用Atomic类时不再需要人为添加synchronized关键字来保证同步性
- 实现并发锁
- 使用volatile关键字
- 使用不变类
- 使用线程安全类
同步方法和同步块,哪个是更好的选择?简述两种同步方式的区别
同步块是更好的选择,它可以指定线程需要获取哪个对象的同步锁才能执行对应的方法,不局限于某个具体的同步方法,灵活性较高
- 同步方法:通过将某个类的方法声明为同步方法,来保证同一时间只有一个线程能够获得该类实例对象的同步锁,来执行这个同步方法,与此同时,其他线程都无法访问这个同步方法以及同步块,但可以访问这个对象中的非同步部分;同步方法的锁是当前对象的锁
- 同步块:当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁,其他原理跟同步方法一致
- 两者的差异:
1. 灵活性:同步块不但可以指定获取当前对象的锁才能访问同步代码(synchronized(this)),还可以指定需要获取其他对象的锁才能访问(synchronized(otherObject));
2. 效率:同步的范围越多,越影响程序执行效率,因此用同步代码块可以尽量缩小影响范围,有时候只需要将一个方法中该同步的地方同步了就行了,比如运算
什么是ThreadLocal?
- 为了保证对象的全局变量的线程安全,当不想使用同步时,可以选择使用ThreadLocal类
- 单个线程使用ThreadLocal对象的get()set()方法去获取他们的默认值或者在线程内部改变他们的值
- 实现方式:当多个线程需要访问同一个共享变量时,将该变量保存到ThreadLocal对象的ThreadLocalMap映射表中保存为副本,key为保存该变量的线程实例本身,这样就能实现每个线程在使用该共享变量时不受其他线程影响
什么是Thread Group?
ThreadGroup API提供了两个功能:
- 获取线程组中处于活跃状态线程的列表
- 为线程设置未捕获异常处理器Uncaught exception handler(此功能在JDK1.5后已经被Thread类的setUncaughtExceptionHandler(UncaughtExceptionHandler eh)方法取代)
说说UncaughtExceptionHandler接口
- 当单线程的程序发生一个未捕获的异常时我们可以采用try....catch进行异常的捕获,但是在多线程环境中,线程抛出的异常是不能用try....catch捕获的,这样就有可能导致一些问题的出现,比如异常的时候无法回收一些系统资源,或者没有关闭的链接
- 使用方法:实现UncaughtExceptionHandler接口,重写uncaughtException方法,定制异常捕获后需要进行的操作
class ExceptionHandler implements UncaughtExceptionHandler
{
@Override
public void uncaughtException(Thread t, Throwable e)
{
System.out.println("==Exception: "+e.getMessage());
}
}
- 启用该实现:
Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler());
Thread thread = new Thread(new Task());
thread.start();
什么是Java线程转储(Thread Dump),如何得到它?
- 线程转储是一个JVM活动线程的列表,它对于分析系统瓶颈和死锁非常有用。有很多方法可以获取线程转储——使用Profiler,Kill -3命令,jstack工具等等
什么是死锁(DeadLock)?如何分析和避免死锁?写一个简单的demo描述什么是死锁
两个线程A和B,如果线程A持有锁L并且想获得锁M,线程B持有锁M并且想获得锁L,那么这两个线程将永远等待下去,这种情况就是最简单的死锁形式。JVM中,当一组JAVA线程发生死锁时,这两个线程就永远无法使用了。
例子:
- x对象和y对象是线程t1和t2的共享资源,t1需要获取y对象的同步锁执行完y的同步方法后才能释放x对象的同步锁,而t2需要获取x对象的同步锁执行完x的同步方法才能释放y对象的同步锁,因此造成了死锁:
class X{
public synchronized void doFirst(Y y){
System.out.println("当前运行:"+Thread.currentThread.getName()+"的doFirst()方法");
Thread.sleep(1000);
y.doSecond();
}
public synchronized void doSecond(){
System.out.println("当前运行:"+Thread.currentThread.getName()+"的doSecond()方法");
}
}
class Y{
public synchronized void doFirst(X x){
System.out.println("当前运行:"+Thread.currentThread.getName()+"的doFirst()方法");
Thread.sleep(1000);
x.doSecond();
}
public synchronized void doSecond(){
System.out.println("当前运行:"+Thread.currentThread.getName()+"的doSecond()方法");
}
}
public class Run implements Runnable{
public int flag;
static X x = new X(), Y y = new Y();
Run run1 = new Run();
Run run2 = new Run();
run1.flag = 1;
run2.flag = 0;
Thread t1 = new Thread(run1);
Thread t2 = new Thread(run2);
t1.start();
t2.start();
public void run(){
if(flag == 1){
x.doFirst(y);
}
if(flag == 0){
y.doFirst(x);
}
}
}
- 如何分析死锁:分析死锁需要查看Java应用程序的线程转储,找出那些状态为Blocked的线程和他们等待的资源。每个资源都有一个唯一的id,用这个id我们可以找出哪些线程已经拥有了它的对象锁:
1. 控制台输入jps获得当前JVM进程的pid
2. 输入jstack以及进程pid,打印当前进程堆栈,就可以发现哪些线程处于死锁状态及其等待的同步锁对象id
- 避免死锁:
1. 尽量让线程每次至多获得一个锁
2. 设计程序时尽量减小嵌套加锁的情况
3. 利用Lock功能代替synchronized来获取锁:object.lock.tryLock(),当获取到object对象的锁后才返回true,以此执行同步操作,最后使用object.lock.unLock()方法来手动释放同步锁
什么是线程池?如何创建一个Java线程池?
- 线程池管理了一组工作线程,便于对线程进行统一分配、调优和监控,同时还包括了一个用于放置等待执行的任务队列
- 使用线程池有如下好处:
1. 降低资源消耗:重复利用已经创建的线程降低线程创建和销毁造成的消耗
2. 提高相应速度:任务到达时不需要等到线程创建就能立即执行
3. 提高线程的可管理性
- 使用使用JDK自带的ExecutorService接口及其实现来进行线程池操作:
1. 通过ThreadPoolExecutor来创建线程池
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, threadFactory,handler);
2. 通过execute()方法或者submit()方法来执行任务,均传入Runnable对象,不同之处在于submit()可返回任务执行的结果
3. 通过shutdown()和shutdownNow()来关闭线程池
简述volatile关键字的作用及其原理
- 相比于synchronized,volatile是一个轻量级锁,使用volatile不会引起上下文切换和调度,提高了程序执行效率
- 通过volatile关键字修饰某个变量,可以保证所有使用这个变量的线程看到的变量值都是一致的,如果某线程对该变量进行了修改,那么其他线程可以立马看到修改结果,保证了线程可见性
- 实现原理简介:
并发编程有三个基本概念:原子性、可见性、有序性
1. 原子性:类似于数据库事务操作的原子性,某项操作执行的过程中要么成功要么失败,中途不能被其他因素(其他线程)打扰,volatile无法保证程序执行的原子性,仅能通过synchronized等同步方式来实现
2. 可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值,volatile就能保证可见性,对应到操作系统内存模型中,当一个线程修改共享变量后他会立即被更新到主内存,其他线程读取该共享变量时会直接读取主内存中的最新数据
3. 有序性:程序执行的顺序按照代码的先后顺序进行执行;JVM内存模型中,为了提高程序执行效率会对程序进行重排序,涉及volatile修饰变量的操作将不会进行重排序,以此保证了程序执行的有序性,让每个线程获取的变量都是正确的值
JVM底层的volatile机制是采用内存屏障来实现的