基础线程面试题总结
- 有几种实现线程的方式?
- 实现Runnable接口和继承Thread类哪种更好?
- 一个线程两次调用start()方法会出现什么情况?为什么?
- 既然start()方法会调用run()方法,为什么我们选择调用start()方法,而不是直接调用run()方法呢?
- 如何停止线程
- 如何处理不可中断的阻塞
- 线程有哪几种状态?生命周期是什么?
- 生产者与消费者设计模式(wait、notify)
- 两个线程交替打印0~100的奇偶数(wait、notify)
- 为什么wait()需要在同步代码块内使用,而sleep()方法不需要
- 为什么线程通信的方法wait()、notify()和notifyAll()被定义在Object类中?而sleep定义在Thread类里?
- wait方法是属于Object对象的,那调用Thread.wait会怎么样?
- 如何选择用notify还是nofityAll ?
- notifyAll之 后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?
- 用suspend(和resume()来阻塞线程可以吗?为什么?
- wait/notify、sleep异同 (方法属于哪个对象?线程状态怎么切换? )
- 在join期间,线程处于哪种线程状态?
- 守护线程和普通线程的区别
- 我们是否需要给线程设置为守护线程?
- java异常体系
- 如何全局处理异常?为什么要全局处理?不处理行不行?
- run方法是否可以抛出异常?如果抛出异常,线程的状态会怎么样?
- 一共有哪几类线程安全问题?
- 哪些场景需要额外注意的线程安全问题?
- 什么是多线程的上下文切换?
- JMM应用的实例:单例模式8种写法、单例和并发的关系
- 饿汉式的缺点、懒汉式的缺点,为什么要用double-check?为什么双重检查模式要用valatile?
- 什么是Java内存模型?
- volatile 和 synchronized的异同?
- 什么是原子操作?java中有哪些原子操作?生成对象的过程是不是原子操作?
- 内存可见性?
- 64位的double 和 long 写入的时候是原子的吗
- 写一个必然死锁的例子,生产中什么场景下会发生死锁?
- 发生死锁的必要条件
- 如何定位死锁?
- 有哪些解决死锁问题的策略
- 哲学家就餐问题
- 实际工程中如何避免死锁?
- 什么是活跃性问题?
只是思路的整理,具体回答请百度
有几种实现线程的方式?
- 从不同的角度看,会有不同的答案。
- 典型答案是两种
- 我们看原理,两种本质都是一样的
- 具体展开说其他方式
- 结论
实现Runnable接口和继承Thread类哪种更好?
- 从代码架构角度
- 新建线程的损耗
- Java不支持双继承
一个线程两次调用start()方法会出现什么情况?为什么?
- 会报错,非法的线程状态,线程六个状态的转换问题
- start()源码解析:
启动新线程检查线程的状态:threadStatus(线程状态是否为0)
加入线程group组、调用start0()本地方法
既然start()方法会调用run()方法,为什么我们选择调用start()方法,而不是直接调用run()方法呢?
- start()方法的作用
- 直接调用run()方法相当于是一个接口中的普通方法
如何停止线程
- 原理:用interrupt来请求、好处
- 想停止线程,要请求方、被停止方、子方法被调用方相互配合
- 最后再说错误的方法: stop/suspend已废弃, volatile的boolean无法处理长时间阻塞的情况
如何处理不可中断的阻塞
- 针对特定的情况使用特定的方法区处理
线程有哪几种状态?生命周期是什么?
- 线程间的状态转化
生产者与消费者设计模式(wait、notify)
package Thread;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 用 wait/notify来实现
* @author 陈觉如
*
*/
public class ProducerConsumerModel {
public static void main(String[] args) {
EventStorage eventStorage = new EventStorage();
Producer producer = new Producer(eventStorage);
Consumer consumer =new Consumer(eventStorage);
new Thread(producer).start();
new Thread(consumer).start();
}
}
class Producer implements Runnable{
private EventStorage storage;
public Producer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.put();
}
}
}
class Consumer implements Runnable{
private EventStorage storage;
public Consumer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.take();
}
}
}
class EventStorage{
private int maxSize;
private List<Date> storage;
public EventStorage(){
maxSize = 10;
storage = new ArrayList<Date>();
}
public synchronized void put() {
if (storage.size() == maxSize) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(new Date());
System.out.println("仓库里有了"+storage.size()+"个产品。");
notify();
}
public synchronized void take() {
if (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("拿到了"+storage.get(0)+",现在仓库还剩下"+storage.size());
storage.remove(0);
notify();
}
}
两个线程交替打印0~100的奇偶数(wait、notify)
package Thread;
/**
* 两个线程交替打印0~100的奇偶数
* 用synchronized关键字实现
* @author 陈觉如
*/
public class WaitNotifyPrintOddEvenSyn {
private static int count;
private static final Object lock = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while( count < 100) {
synchronized (lock) {
if ( (count & 1) == 0 ) {
System.out.println(Thread.currentThread().getName()+":"+count++);
}
}
}
}
},"偶数").start();
new Thread(new Runnable() {
@Override
public void run() {
while( count < 100) {
synchronized (lock) {
if ( (count & 1) == 1 ) {
System.out.println(Thread.currentThread().getName()+":"+count++);
}
}
}
}
},"奇数").start();
}
}
package Thread;
/**
* 两个线程交替打印0~100的奇偶数
* 用 wait 和 notify 关键字实现
* @author 陈觉如
*
*/
public class WaitNotifyPrintOddEvenWait {
static class TurningRunner implements Runnable{
private static int count;
private static final Object lock = new Object();
@Override
public void run() {
while (count <= 100) {
synchronized (lock) {
System.out.println(Thread.currentThread().getName()+":"+count++);
lock.notify();
if (count <= 100) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public static void main(String[] args) {
new Thread(new TurningRunner(),"偶数").start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new TurningRunner(),"奇数").start();
}
}
为什么wait()需要在同步代码块内使用,而sleep()方法不需要
- 为了让通信的可靠,防止死锁永久等待的发生,wait() 与 notify()方法
- sleep()只是指定自己线程的,与其他线程关系不大
为什么线程通信的方法wait()、notify()和notifyAll()被定义在Object类中?而sleep定义在Thread类里?
- wait()、notify()和notifyAll()是锁级别的操作,是属于每一个对象的,对象的对象头中都保存了锁的状态,锁是绑定了对象,而不是线程
wait方法是属于Object对象的,那调用Thread.wait会怎么样?
- Thread.wait()可以调用,但是Thread类比较特殊,在线程退出的时候,会自动调用notify方法,所以不适合
如何选择用notify还是nofityAll ?
- 唤醒一个或者多个线程
notifyAll之 后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?
- 回到最初始的状态Runnable状态
用suspend(和resume()来阻塞线程可以吗?为什么?
- 不可以,不安全弃用了
- 推荐wait\notify去阻塞线程
wait/notify、sleep异同 (方法属于哪个对象?线程状态怎么切换? )
- 相同:阻塞、响应中断
- 不同:同步方法中、释放锁、指定时间、所属类
在join期间,线程处于哪种线程状态?
- waiting 主线程等待子线程
守护线程和普通线程的区别
- 守护线程作用:给用户线程提供服务
- 守护线程的特性:线程类型默认继承父线程、一般被jvm自动启动、不影响jvm的退出
- 整体无区别、唯一区别在于jvm的退出、作用
我们是否需要给线程设置为守护线程?
- 不应该这么做,会变的非常危险,因为守护线程和普通线程的区别
java异常体系
如何全局处理异常?为什么要全局处理?不处理行不行?
- 用实现Thread.UncaughtExceptionHandler接口,实现void uncaughtException(Thread t, Throwable e)进行处理操作
- 在线程中设置默认的获取器,Thread.setDefaultUncaughtExceptionHandler(获取器对象);
run方法是否可以抛出异常?如果抛出异常,线程的状态会怎么样?
- 不可以,run方法底层没有实现向外抛的操作,只能try-catch
一共有哪几类线程安全问题?
- 运行结果错误
- 死锁等活跃性问题
- 对象发布和初始化的时候的安全问题
哪些场景需要额外注意的线程安全问题?
- 访问共享的变量或资源,会有并发风险
- 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题
- 不同的数据之间存在捆绑关系的时候
- 我们使用其他类的时候,如果对方没有声明自己的线程是安全的,大概率也会存在并发问题
什么是多线程的上下文切换?
上下文切换可以认为是内核(操作系统的核心)在CPU上对于进程(包括线程)进行以下的活动:
(1)挂起一个进程,将这个进程在CPU中的状态(上下文)存储于内存中的某处
(2)在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复
(3)跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程
JMM应用的实例:单例模式8种写法、单例和并发的关系
- 饿汉式(静态常量)[可用] JVM加载的时候保证线程的安全性
/**
* 饿汉式(静态常量)[线程安全]
* @author 陈觉如
*
*/
public class Singleton2 {
private final static Singleton2 INSTANCE = new Singleton2();
private Singleton2() {
}
public static Singleton2 getInstance() {
return INSTANCE;
}
}
- 饿汉式(静态代码块)[可用] JVM加载的时候保证线程的安全性
package single;
/**
* 饿汉式(静态代码块)[线程安全]
* @author 陈觉如
*
*/
public class Singleton1 {
private final static Singleton1 INSTANCE ;
static {
INSTANCE = new Singleton1();
}
private Singleton1() {
}
public static Singleton1 getInstance() {
return INSTANCE;
}
}
- 懒汉式(线程不安全)
package single;
/**
* 懒汉式(线程不安全)[不可用]
* @author 陈觉如
*
*/
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {
}
public static Singleton3 getInstance() {
if (instance == null) {
//这里不安全,多个线程可能创建多个实例
instance = new Singleton3();
}
return instance;
}
}
- 懒汉式(线程安全)
package single;
/**
* 懒汉式(线程安全)[不推荐]
* @author 陈觉如
*
* 效率太低
*/
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {
}
//多个线程访问的时候,无法及时相应
public synchronized static Singleton4 getInstance() {
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
}
- 懒汉式(线程不安全)同步代码块
package single;
/**
* 懒汉式(线程不安全)[不推荐]
* @author 陈觉如
*
* 效率太低
*/
public class Singleton5 {
private static Singleton5 instance;
private Singleton5() {
}
public static Singleton5 getInstance() {
if (instance == null) {
//这里不安全,多个线程到达这个,会等待创建实例
synchronized (Singleton5.class) {
instance = new Singleton5();
}
}
return instance;
}
}
- 双重检查[懒汉式]
package single;
/**
* 双重检查(线程安全)[推荐面试使用]
* @author 陈觉如
*
* 线程安全,延迟加载,效率高
*/
public class Singleton6 {
private volatile static Singleton6 instance;
//新建对象实际上有三个过程,重排序会带来NPE
//创建一个空对象,调用构造方法, 对象赋给引用
private Singleton6() {
}
public static Singleton6 getInstance() {
if (instance == null) {
synchronized (Singleton6.class) {
if (instance == null) {
instance = new Singleton6();
}
}
}
return instance;
}
}
- 静态内部类[懒汉式]
package single;
/**
* 静态内部类(线程安全)[推荐使用]
* @author 陈觉如
*
* 线程安全,延迟加载,效率高
*/
public class Singleton7 {
private Singleton7() {
}
private static class SingletonInstance{
private final static Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance() {
return SingletonInstance.INSTANCE;
}
}
- 枚举单例
package single;
/**
* 单例枚举(线程安全)[推荐使用]
* @author 陈觉如
*
*/
public enum Singleton8 {
INSTANCE;
//下面定义方法
}
饿汉式的缺点、懒汉式的缺点,为什么要用double-check?为什么双重检查模式要用valatile?
上面的例子
什么是Java内存模型?
- 为什么需要JMM?C语音不存在内存模型的概念,依赖处理器,不同处理器结果不一样,无法保证并发安全,需要一个标准,让多线程运行的结果可预期
- 是一种规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便地开发多线程程序
- 是工具类和关键字的原理,例如:synchronized、volatile、Lock等原理,底层都是JMM指定什么时候用内存栅栏
- 最主要的就是重排序、内存可见性、原子性
- 重排序:CPU根据自己的判断可能会对某些代码的指令进行重排序,会导致实际执行顺序和代码在java文件中的顺序不一致,此外还有编译器优化、内存的“重排序”
- 可见性:CPU缓存结构,有多级缓存,会导致读的数据过期;
volatile 和 synchronized的异同?
- volatile是一种同步的机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为
- 变量修饰成volatile,JVM就知道这个变量可能会被并发修改
- volatile是用来同步的保证线程安全的,做不到synchronized那样的原子保护
- volatile适用于一个共享变量自始至终只被各个线程赋值,而没有其他的操作,因为赋值自身有原子性;还适用于作为刷新之前变量的触发器
- volatile的作用:可见性:读一个valatile变量之前,需要现使相应的本地缓存失效,这样就必须到主内存读取最新的值,写一个volatile属性会立即刷入到主内存;禁止指令重排序优化
- valatile的读写是无锁的,不能替代synchronized,因为它没有提供原子性和互斥性
什么是原子操作?java中有哪些原子操作?生成对象的过程是不是原子操作?
- 一系列的操作,要么全部执行成功,要么全部不执行
- 除long、double之外的基本类型和所有引用类型的赋值操作、java.concurrent.Atomic.*包中的所有类的原子操作
内存可见性?
- 可见性:CPU缓存结构,有多级缓存,会导致读的数据过期;
- JMM的抽象:主内存和本地内存:所有变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存的变量内容是主内存中的拷贝;
- 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中
- 主内存是多个线程共享的,但线程间不共享工作内容,线程通信通过主内存中转来完成
64位的double 和 long 写入的时候是原子的吗
- 对于64位的值的写入,是分为两个32位的操作进行写入
- 商用Java虚拟机中不会出现这种问题
写一个必然死锁的例子,生产中什么场景下会发生死锁?
package deadlock;
/**
* 必定发生死锁的情况
* @author 陈觉如
*
*/
public class MustDeadLock implements Runnable{
int flag = 1;
static Object o1 = new Object();
static Object o2 = new Object();
@Override
public void run() {
System.out.println("flag = "+flag);
if (flag == 0) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("线程1成功拿到两把锁");
}
}
}else {
synchronized (o2) {
synchronized (o1) {
System.out.println("线程2成功拿到两把锁");
}
}
}
}
public static void main(String[] args) {
MustDeadLock mustDeadLock1 = new MustDeadLock();
MustDeadLock mustDeadLock2 = new MustDeadLock();
mustDeadLock1.flag = 1;
mustDeadLock2.flag = 0;
new Thread(mustDeadLock1).start();
new Thread(mustDeadLock2).start();
}
}
在一个方法中获取多个锁,可能会发生死锁
发生死锁的必要条件
- 互斥条件
- 请求与保持条件
- 不剥夺条件
- 循环等待条件
如何定位死锁?
- jstack命令对程序进行堆栈分析
- ThreadMXBean
有哪些解决死锁问题的策略
- 避免策略:哲学家就餐的换手方案,转账换序方案
- 检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁
- 鸵鸟策略
哲学家就餐问题
- 服务员检查
- 改变一个哲学家拿叉子的顺序
- 餐票
- 领导调节
实际工程中如何避免死锁?
- 设置超时时间(tryLock)
- 多使用并发类而不是自己设计锁
- 尽量降低锁的使用粒度:用不同的锁而不是一个锁
- 如果能使用同步代码块,就不使用同步方法:自己指定锁对象
- 给线程起有意义的名字,方便debug和排查
- 避免锁的嵌套
- 分配资源前先看能不能收回来:银行家算法
- 专锁专用
什么是活跃性问题?
- 死锁
- 活锁
- 饥饿