Java并发编程是面试和笔试中的一大考点,也是一大难点,更是进入大厂的必备知识。

相关考点主要分为两类

  • Java并发编程基础
  • JUC工具类

本文主要就Java并发编程基础相关的数十个考点展开讲解,不涉及JUC相关的问题,更适合于基础相对薄弱或者没有太多实践经验的同学学习。后续也会整理关于JUC相关问题的专题,可以关注我的博客了解。

若表达有误或有修正的建议请及时私信我

快速到达看这里-->

进程和线程的区别

  • 线程不能看做独立应用,而进程可看做独立应用
  • 进程有独立的地址空间,互不影响,线程只是进程的不同执行路径
  • 线程没有独立的地址空间,多进程的程序比多线程的程序健壮
  • 进程切换比线程切换大
  • 进程是资源分配的最小单位,线程是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面试复习笔记》,或者访问我另一篇博客《Java面试核心知识点汇总》查看目录和直达链接