Java面试-线程核心基础知识
在系统的复习了Java的线程相关基础知识后,将其中可能出现的一些面试题做一个总结如下:
- 有多少种实现线程的方法?
- 实现Runnable接口和继承Thread类哪种方式更好?
- 一个线程两次调用start()方法会出现什么情况?为什么?
- 既然start()方法会调用run方法,为什么我们选择调用start方法而不是直接调用run方法呢?
- 如何停止线程?
- 如何处理不可中断的阻塞
- 线程有哪几种状态?生命周期是什么?
- 用程序实现两个县城交替打印(0-100)的奇偶数
- 手写生产者消费者设计模式
- 为什么wait方法需要在同步代码块内使用,而slepp不需要
- 为什么线程通信的方法wait、notify、notifyAll被定义在Object类中?而slepp方法被定义在Thread类中?
- wait方法是属于Object对象的,那调用Thread.wait()会怎么样
- 如何选择用notify还是notifyAll
- notifyAll之后所又的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?
- suspend和resume来阻塞线程可以吗?为什么?
- wait/notify、sleep异同
- 在join期间,线程处于哪种线程状态?
- 守护线程和普通线程的区别
- 我们是否需要给线程设置守护线程?
- run方法是否可以抛出异常?如果抛出异常,线程的状态会怎么样?
- 线程中如何处理某个未处理异常
- 什么是多线程的上下文切换
- 为什么多线程会带来性能问题?
下面,我将对上述的问题的问题一一的作出我认为较为正确的回答。
有多少种实现线程的方法?
-
从Oracel官方文档可以得知,创建线程的方法有两类,一类是实现Runnable接口,另一类是继承Thread类
-
准确的讲,创建线程只有一种方式那就是构造一个Thread类,而实现线程的执行单元run()方法有两种方式
- 实现Runnable接口的run方法,并把Runnable的实例传给Thread
- 继承Thread类,重写Thread的run方法
-
通常不准确的说法包括通过线程池、定时器、匿名内部类、lambda表达式等方式方式创建线程都是对上述方式的调用
实现Runnable接口和继承Thread类哪种方式更好?
实现Runnable接口更好
- 可以避免Java中的单继承的限制
- 增强代码的健壮性,代码可以被多个线程共享
- 适合多个相同的程序去处理同一个资源
一个线程两次调用start()方法会出现什么情况?为什么?
结果:第二次调用抛出IllegalThreadStateException
原因:
- 线程在第一次执行start方法后由NEW状态转换为了Runnable状态,且转换不可逆
- Java在调用start方法时会判断线程所处的状态是否为NEW状态,如果不是就会抛出IllegalThreadStateException
既然start()方法会调用run方法,为什么我们选择调用start方法而不是直接调用run方法呢?
- 使用start方法调用时才会真正启动一个线程,并让线程从NEW状态转为RUNNABLE状态,从而经历完整的生命周期
- 使用run方法调用时就只是一个普通的主线程的方法而已,并不会进入子线程
如何停止线程?
- 用interrupt来请求,而不是用stop/volatile
- 用interrupt好处是保证线程安全,将主动权交给被中断的线程
- 想要停止线程,要请求方,被停止方,子方法被调用的相互配合
- 请求方发出请求信号
- 被停止方要适当的时候检查中断信号,并在可能抛出interruptedException的时候去处理这个信号,并进行处理
- 如果是写子方法调用的,优先是在方法层抛出这个exception,以便于上层进行处理,或者收到中断信号后,再次将它设为中断状态(收到后,默认会清除中断状态)
- 错误的停止方法:
- stop:已经被弃用了,不能保证数据的完整性
- volatile的boolean标识无法处理长时间阻塞的情况
(生产者生产快,消费者消费慢,发送中断时标识位即使改变了,已经生产了的还是会继续被消费)
如何处理不可中断的阻塞
- 根据不同的情况做不同的处理,不同情况下可能有相应的方法进行处理,在编写时使用可以响应中断的锁的方法
- 如果不能进行处理,就让它苏醒后尽快感受到中断进行处理
线程有哪几种状态?生命周期是什么?
- 线程有6种状态:NEW、RUNNABLE、TERMINATED、BLOCKED、WAITTING、TIMED_WAITTING
用程序实现两个线程交替打印(0-100)的奇偶数
synchronized实现
/**
* 〈用程序实现两个线程交替打印0-100的奇偶数
* 本类采用synchronized〉
*
* @author Chkl
* @create 2020/2/29
* @since 1.0.0
*/
public class waitnotifyPrintEvenSYyn {
private static int count;
private static Object lock = new Object();
//建两个线程,一个只处理偶数,一个只处理奇数(位运算)
//用synchronized做通信
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) != 0) {
System.out.println
(Thread.currentThread().getName()
+ ":" + count++);
}
}
}
}
}, "奇数").start();
}
}
wait和notify优化实现
/**
* 〈连个线程交替打印0-100的两个奇偶数〉
* 用wait和notify实现
*
* @author Chkl
* @create 2020/2/29
* @since 1.0.0
*/
public class WaitNotifyprintEvenWait {
//拿到锁,就打印
//打印完,唤醒其他线程,就休眠
private static int count;
private static Object lock = new Object();
static class TurningRunning implements Runnable {
@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 TurningRunning(), "偶数").start();
try {
//休眠100毫秒,保证偶数先行
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new TurningRunning(), "奇数").start();
}
}
手写生产者消费者设计模式
什么情况下需要这种设计模式
任务队列中,生产者和消费者存在步调不一致时
使用wait/notify的实现
/**
* 〈用wait/notify实现生产者和消费者〉
*
* @author Chkl
* @create 2020/2/29
* @since 1.0.0
*/
public class ProducerCustomerModel {
public static void main(String[] args) {
EventStorage storage = new EventStorage();
Producer producer = new Producer(storage);
Consumer consumer = new Consumer(storage);
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 LinkedList<Date> storage;
public EventStorage() {
maxSize = 10;
storage = new LinkedList<>();
}
// 添加方法
public synchronized void put() {
// 当队列满了就调用wait方法释放锁,等待消费后唤醒
while (storage.size() == maxSize) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 添加到队列
storage.add(new Date());
System.out.println("仓库有了" + storage.size() + "个产品");
// 添加完成后提醒消费者消费
notify();
}
public synchronized void take() {
// 当队列空了调用wait方法释放锁等待生成
if (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 进行消费
System.out.println("拿到了" + storage.poll() + ",现在还剩下" + storage.size());
// 消费后提醒生产者进行生产
notify();
}
}
为什么wait方法需要在同步代码块内使用,而slepp不需要
-
wait方法定义在同步代码块中为了让通信变得可靠,防止死锁或者永久等待的发生
如果不把wait和notify方法都放在同步块里,可能在执行wait之前,线程突然切出去了,到一个将要执行notify的线程,把notify的都执行完了之后再切回将执行wait的线程执行完wait之后,不再有线程唤醒它,造成永久等待
-
sleep方法主要针对单个线程,与其他线程没有太多关联,不需要同步
为什么线程通信的方法wait、notify、notifyAll被定义在Object类中?而slepp方法被定义在Thread类中?
-
wait(),notify(),notifyAll()是锁级别的操作,而锁是属于某一个对象的。每一个对象的对象头中都有几个字节是存放锁的状态的,所以锁是绑定在对象中,而不是线程中。如果定义在Thread中,如果每一个线程持有多把锁,就不能灵活地使用了。
-
sleep()是针对于单个线程的操作,所以在Thread类中
wait方法是属于Object对象的,那调用Thread.wait()会怎么样
- 不应该调用Thread.wait(),Thread不适合作为锁对象
- 当线程结束的时候,会自动的调用notify方法,会干扰设计的整个流程
如何选择用notify还是notifyAll
根据业务需求选择
- notify()随机的唤醒一个线程
- notifyAll()唤醒所有的wait状态的线程
notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?
陷入等待状态,等待这把锁被释放后再次竞争
suspend和resume来阻塞线程可以吗?为什么?
不可以,由于安全问题,这两个方法都弃用。
wait/notify、sleep异同
-
相同
- 都会发生阻塞
- 都会响应中断
-
不同
- wait/notify必须在同步方法中执行,sleep不要求
- wait/notify会释放锁,sleep不释放锁
- sleep必须指定时间,wait可传可不传
- wait/notify属于Object类,sleep属于Thred类
在join期间,线程处于哪种线程状态?
waiting
守护线程和普通线程的区别
- 普通线程会影响JVM的退出,当普通线程没有全部结束JVM不会退出,守护线程不会
- 普通线程的作用是执行我们所写的逻辑,守护线程的作用是服务于普通线程
我们是否需要给线程设置守护线程?
不需要设置,并且设置了可能会很危险。当只剩下这一个线程时,JVM认定为是守护线程就直接停掉了,造成线程错误结束
run方法是否可以抛出异常?
- 不可以抛出,在方法签名中说明了不能往外抛异常
线程中如何处理某个未处理异常
实现UncaughtExceptionHandler接口生成一个全局异常处理器
再将处理器配置在Thread中
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler("捕获器1"));
什么是多线程的上下文切换
上下文切换可以认为是操作系统内核对CPU上进程(包括线程)进行以下活动:
- 挂起一个进程,将这个进程在CPU中的状态(上下文)存储在内存中的某处
- 在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复
- 跳转到程序计数器所指的位置(进程被中断的代码行),以恢复该进程
为什么多线程会带来性能问题?
- 调度上,频繁的上下文切换
- 协作上,Java内存模型,为了数据的正确性往往会使用禁止编译器优化,使缓存失效
暂时复习到这里,后续的在复习了之后再补充