Java并发编程是面试和笔试中的一大考点,也是一大难点,更是进入大厂的必备知识。
相关考点主要分为两类
- Java并发编程基础
- JUC工具类
本文主要就Java并发编程基础相关的数十个考点展开讲解,不涉及JUC相关的问题,更适合于基础相对薄弱或者没有太多实践经验的同学学习。后续也会整理关于JUC相关问题的专题,可以关注我的博客了解。
若表达有误或有修正的建议请及时私信我
快速到达看这里-->
- 进程和线程的区别
- Java进程与线程的关系
- 有多少种实现线程的方法?
- 实现Runnable接口和继承Thread类哪种方式更好?
- 一个线程两次调用start()方***出现什么情况?为什么?
- 既然start()方***调用run方法,为什么我们选择调用start方法而不是直接调用run方法呢?
- 如何停止线程?
- 如何处理不可中断的阻塞
- 线程有哪几种状态?生命周期是什么?
- 用程序实现两个线程交替打印(0-100)的奇偶数
- 什么是生产者消费者模式
- 手写生产者消费者模式
- 为什么wait方法需要在同步代码块内使用,而sleep不需要
- 为什么线程通信的方法wait、notify、notifyAll被定义在Object类中?而slepp方法被定义在Thread类中?
- wait方法是属于Object对象的,那调用Thread.wait()会怎么样
- 如何选择用notify还是notifyAll
- notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?
- suspend和resume来阻塞线程可以吗?为什么?
- wait/notify、sleep异同
- yield和sleep的区别
- 在join期间,线程处于哪种线程状态?
- 守护线程和普通线程的区别
- 我们是否需要给线程设置守护线程?
- run方法是否可以抛出异常?
- 如何全局处理异常
- 什么是多线程的上下文切换
- 为什么多线程会带来性能问题?
- 何时会导致密集的上下文切换
- 单例模式的作用和适用场景
- 单例模式的八种写法及相关知识点
- 工作中哪种单例模式的实现最好
- 讲一讲什么是Java内存模型
- 关于死锁你知道多少?
进程和线程的区别
- 线程不能看做独立应用,而进程可看做独立应用
- 进程有独立的地址空间,互不影响,线程只是进程的不同执行路径
- 线程没有独立的地址空间,多进程的程序比多线程的程序健壮
- 进程切换比线程切换大
- 进程是资源分配的最小单位,线程是CPU调度的最小单位
Java进程与线程的关系
- Java对操作系统提供的功能进行封装,包括进程和线程
- 运行一个程序会产生一个进程,进程至少包含一个线程(main)
- 每个进程对应一个JVM实例,多个线程共享JVM中的堆
- Java采用单线程编程模型,程序会自动创建主线程
- 主线程可以创建子线程,原则上要后于子线程完成执行
有多少种实现线程的方法?
本质是一种,通常是两种,外在形式是多种
- 从本质上来讲,创建线程只有一种方式就是构造一个Thread类
- 实现线程的执行单元run()方法有两种方式
- 实现Runnable接口的run方法,并把Runnable的实例传给Thread
- 继承Thread类,重写Thread的run方法
- 其外在形式包括线程池、定时器、匿名内部类、lambda表达式等,都是对本质的调用
实现Runnable接口和继承Thread类哪种方式更好?
实现Runnable接口更好
- 可以避免Java中的单继承的限制
- 增强代码的健壮性,代码可以被多个线程共享
- 适合多个相同的程序去处理同一个资源
一个线程两次调用start()方***出现什么情况?为什么?
结果:第二次调用抛出IllegalThreadStateException
原因:
- Java在调用start方法时会对线程做状态监测,判断线程所处的状态是否为NEW状态,如果不是就会抛出IllegalThreadStateException
- 线程在第一次执行start方法后由NEW状态转换为了Runnable状态,且转换不可逆
既然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方法需要在同步代码块内使用,而sleep不需要
-
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
优先选用notifyAll,notify可能出现很多问题不好控制
- notify()随机的唤醒一个线程
- notifyAll()唤醒所有的wait状态的线程
notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?
陷入等待状态,等待这把锁被释放后再次竞争,不会有错误发生
suspend和resume来阻塞线程可以吗?为什么?
不可以,由于安全问题,这两个方法都弃用。
wait/notify、sleep异同
-
相同
- 都会发生阻塞
- 都会响应中断
-
不同
- wait/notify必须在同步方法中执行,sleep不要求
- wait/notify会释放锁,sleep不释放锁
- sleep必须指定时间,wait可传可不传
- wait/notify属于Object类,sleep属于Thred类
yield和sleep的区别
- sleep方法给其他线程机会时不考虑线程优先级(优先级低的也有可能);
- yield方法只会给相同优先级或者更高优先级线程机会(如果没有相同或者更高优先级的线程,该线程会继续运行)
- 线程执行sleep方法进入阻塞状态
- 线程执行yield方法进入就绪状态
在join期间,线程处于哪种线程状态?
join期间属于waiting状态
守护线程和普通线程的区别
- 普通线程会影响JVM的退出,当普通线程没有全部结束JVM不会退出,守护线程不会影响JVM退出
- 普通线程的作用是执行我们所写的逻辑,守护线程的作用是服务于普通线程
我们是否需要给线程设置守护线程?
不需要设置,并且设置了可能会很危险。当只剩下这一个线程时,JVM认定为是守护线程就直接停掉了,造成线程错误结束
###为什么程序设计不应依赖于优先级
- 优先级带来的效果无法保证在不同的操作系统上一致
- 操作系统可以修改代码的优先级,导致设置的失效
run方法是否可以抛出异常?
- 不可以抛出,在方法签名中说明了不能往外抛异常
- 如果抛出异常,线程就会终止
如何全局处理异常
实现UncaughtExceptionHandler接口生成一个全局异常处理器
再将处理器配置在Thread中
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler("捕获器1"));
什么是多线程的上下文切换
上下文切换可以认为是操作系统内核对CPU上进程(包括线程)进行以下活动:
- 挂起一个进程,将这个进程在CPU中的状态(上下文)存储在内存中的某处
- 在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复
- 跳转到程序计数器所指的位置(进程被中断的代码行),以恢复该进程
为什么多线程会带来性能问题?
- 调度上,频繁的上下文切换
- 协作上,Java内存模型,为了数据的正确性往往会使用禁止编译器优化,使缓存失效
何时会导致密集的上下文切换
频繁的IO操作和抢锁时
单例模式的作用和适用场景
- 无状态的工具类:如日志工具类
- 全局信息类:如网站访问次数记录类
单例模式的八种写法及相关知识点
见我的另一篇博客《Java面试-通过单例模式的8种写法搞定单例模式面试》
内容包括–>
- 饿汉式(静态常量)(可用)
- 饿汉式(静态代码块)(可用)
- 懒汉式(线程不安全)(不可用)
- 懒汉式(线程安全)(不推荐)
- 懒汉式(加锁,线程不安全)(不可用)
- 双重检查(推荐面试使用)(可用)
- 静态内部类(推荐用)(可用)
- 枚举(推荐用)(可用)(生产中最佳写法)
工作中哪种单例模式的实现最好
枚举最好!
- 《Effective Java》中明确表示枚举是最佳的
- 写法简单
- 线程安全
- 符合懒加载机制
- 避免反序列化破坏单例
讲一讲什么是Java内存模型
见我的另一篇博客《Java面试-讲一讲什么是Java内存模型》
内容包括–>
- 为什么会有Java内存模型?
- 辨析JVM内存结构、Java内存模型、Java对象模型
- JVM内存结构
- Java对象模型
- Java内存模型
- 重排序
- 例子演示:
- 什么是重排序
- 重排序的好处
- 重排序的3种情况
- 可见性
- 什么是可见性问题
- 为什么会有可见性问题
- JMM主内存与本地内存的关系
- happens-before规则有哪些?
- volatile是什么
- 什么时候适合用volatile
- volatile的作用
- volatile与synchronized的关系
- 原子性
- 什么是原子性
- Java中的原子操作有哪些
- 生成对象的过程是不是原子操作?
关于死锁你知道多少?
见我的另一篇博客《Java面试-彻底搞懂死锁的前世今生》
内容包括–>
- 死锁是什么,有什么危害?
- 写一个死锁的例子
- 案例一:必然发生死锁
- 案例二:两个账户转账
- 案例三:多人多次转账
- 发生死锁必须满足哪些条件
- 如何定位死锁
- 有哪些解决死锁问题的策略?
- 线上发生死锁怎么办
- 常见修复策略
- 哲学家就餐问题
- 问题描述
- 代码演示
- 多种解决方案
- 改变一个哲学家拿叉子的顺序的实现
- 工程中如何避免死锁
本文整理自
- 慕课网《Java并发核心知识体系精讲》
- 慕课网《剑指Java面试-Offer直通车》
- 以及Google到的一些解答
更多Java面试复习笔记和总结可访问我的面试复习专栏《Java面试复习笔记》,或者访问我另一篇博客《Java面试核心知识点汇总》查看目录和直达链接