目录
- Java多线程
- 前言
- 面试题
- synchronized?作用?
- synchronized 怎么使用?
- 构造方法可以用 synchronized 修饰吗?
- synchronized 底层原理了解吗?
- 除了原子性,synchronized 可见性,有序性,可重入性怎么实现?
- JDK1.6 后对 synchronized 的优化?
- 锁升级?
- synchronized 和 volatile 有什么区别?
- AQS 了解吗?
- 讲讲对 AQS 原理的理解?
- 讲讲 CAS?
- CAS 有什么问题?如何解决?
- Java 有哪些保证原子性的方法?如何保证多线程下 i++ 结果正确?
- Atomic 原子类?
- AtomicInteger 的原理?
- 什么是乐观锁?什么是悲观锁?使用场景?
- 如何实现乐观锁?
- 公平锁和非公平锁的区别?
- 可中断锁和不可中断锁的区别?
- 共享锁和独占锁有什么区别?
- 线程持有读锁还能获取写锁吗?
- 读锁为什么不能升级为写锁?
- CountDownLatch(倒计数器)了解吗?
- CyclicBarrier(同步屏障)了解吗?
- CyclicBarrier 和 CountDownLatch 有什么区别?
- Semaphore(信号量)了解吗?
- Exchanger 了解吗?
- ThreadLocal 是什么?
- 你在工作中用到过 ThreadLocal 吗?
- ThreadLocal 怎么实现的呢?
- ThreadLocal 内存泄露是怎么回事?
- ThreadLocalMap 的结构了解吗?
- 父子线程怎么共享数据?
- 为什么要用线程池/线程池的好处?
- 能简单说一下线程池的工作流程吗?
- 线程池常见参数有哪些?如何解释?
- 线程池的饱和策略有哪些?/线程池的拒绝策略有哪些?
- 线程池常用的阻塞队列有哪些?
- 线程池提交 execute 和 submit 有什么区别?/execute() vs submit()?
- 线程池怎么关闭知道吗?
- 线程池的线程数应该怎么配置?/ 配置线程池的参数?/ 如何设定线程池的大小?
- Executor 框架?
- 创建线程池的两种方式?
- 有哪几种常见的线程池?
- 线程池异常怎么处理知道吗?
- 能说一下线程池有几种状态吗?
- Fork/Join 框架了解吗?
- Runnable vs Callable?
- Thread、Runnable、Callable、Future、FutureTask 关系?
- CompletableFuture 类有什么用?
- ReentrantLock?
- ReentrantLock 实现原理?
- ReentrantLock 怎么实现公平锁?
- synchronized 和 ReentrantLock 的区别?
- ReentrantReadWriteLock 是什么?适合什么场景?
Java多线程
前言
已经找到工作了,分享秋招时的笔记。祝大家都能顺利找到自己心仪的工作。
面试题
synchronized?作用?
- synchronized 是 Java 用于实现线程同步的关键字
- 被修饰的代码在同一时刻只能被一个线程执行,从而实现线程的互斥访问,保证操作的原子性和线程安全
synchronized 怎么使用?
synchronized 用来保证代码的原子性和线程安全
- 修饰实例方法(锁当前对象实例):当方法被 synchronized 修饰时,该方法在同一时刻只能被一个线程执行,其他线程需要等待
- 修饰静态方法(锁当前类):类级别的锁,修饰的静态方法在同一时刻只能被一个线程执行,其他线程需要等待,这个锁是当前类的锁,会作用于这个类的所有对象实例
- 修饰代码块(锁指定对象或类):对括号中指定的对象或类加锁,进入同步代码前要获得给定对象或给定类的锁
构造方法可以用 synchronized 修饰吗?
- 构造方法不能用 synchronized 修饰
- 构造方法本身就是线程安全的,不需要同步
synchronized 底层原理了解吗?
- synchronized 底层原理是基于对象监视器 monitor 对象实现的
- 每个对象都有一个 monitor 对象和一个关联的等待队列,当线程进入 sychronized 代码块时,它会获取 monitor 对象的锁,其他线程必须等待锁被释放才能执行
除了原子性,synchronized 可见性,有序性,可重入性怎么实现?
- 可见性:synchronized 关键字在释放锁之前,会将当前线程对共享变量的修改刷新到主存中,保证其他线程可以看到最新的值
- 有序性:synchronized 同步的代码块,一次只能被一个线程访问,也就是同一时刻,代码是单线程执行的
- 可重入性:线程要再次执行 synchronized 代码块的时候,线程不会被堵塞,而是直接进入该代码块,这是通过给每个锁添加一个计时器来实现的
JDK1.6 后对 synchronized 的优化?
增加了适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等优化策略,提高了同步性能
- 适应性自旋:当线程发现被阻塞的时间很短时,采用自旋,持续尝试获取锁,避免线程进入阻塞状态
- 锁消除:编译器会分析代码,判断是否可能存在共享数据共享,如果不存在,会消除不必要的锁
- 锁粗化:将连续的同步操作合并为一个锁的范围
- 轻量级锁:当前线程争夺锁的时候,使用 CAS 操作进行锁竞争
- 偏向锁:程序进入同步代码块时自动获取锁
锁升级?
锁的升级过程时为了在不同的竞争情况下,提供不同的锁状态,以达到更好的性能和线程安全,锁只会升级不会降级,所以当锁从重量级锁状态升级到其他状态时,会先解锁再升级
- 无锁状态:当没有线程占有锁时,对象处于无锁状态
- 偏向锁状态:只有一个线程访问对象时,会将对象标记为偏向锁状态,线程再次访问时,不需要重新获取锁
- 轻量级锁状态:当多个线程竞争同一个锁时,会将对象标记为轻量级锁状态,在轻量级锁状态,线程会通过 CAS 操作来竞争锁
- 重量级锁状态:当多个线程竞争同一个锁时,无法通过轻量级锁来解决竞争,锁会升级为重量级锁状态,此时线程会进入阻塞状态,通过操作系统的互斥量来保持同步
synchronized 和 volatile 有什么区别?
- volatile 关键字
- 主要用来解决变量在多线程之间的可见性
- 保证当一个线程修改变量时,其他线程能够立即看到变量的最新值
- 适用于简单的场景,可以用于修饰共享变量以保证可见性
- sychronized 关键字
- 不仅保证了可见性,还保证原子性
- 用来实现线程之间的同步,可以修饰方法和代码块
- 适用于多线程同步的场景
AQS 了解吗?
- 抽象队列同步器,是 Java 中一个抽象类
- 提供用于实现同步器的框架,为构建基于锁或其他同步器的大部分实现提供了底层的支持
- Java 中的很多同步工具,如 ReentrantLock、CountDownLatch、Semaphore 等都是基于 AQS 实现的
讲讲对 AQS 原理的理解?
- 状态变量:AQS 使用一个状态变量来表示资源的可用性
- 等待队列:当一个线程想要访问资源但资源已经被其他线程占用时,该线程会被放入一个等待队列,这个队列按照先进先出管理
- 获取和释放:当资源被释放时,AQS 会从等待队列中唤醒一个或多个线程,让它们继续执行
- 同步模式:AQS 支持独占模式和共享模式
- 独占模式:一次只有一个线程能获取锁,比如独占锁
- 共享模式:允许多个线程同时访问资源,比如读写锁
- CAS 操作:AQS 使用 CAS 操作来维护状态变量和等待队列,确保线程安全的队列操作
讲讲 CAS?
- CAS:CompareAndSwap,比较并交换,是一种并发编程中的原子操作
- 用于实现多线程环境下的同步和数据更新
- 基本思想:检查某个内存位置的值是否和预期值相等,如果相等,则将新的值写入该内存位置,否则不做任何操作
CAS 有什么问题?如何解决?
- ABA 问题:添加版本号和时间戳
- 循环性能开销:限制自旋次数
- 只能保证一个变量的原子操作:结合锁机制
Java 有哪些保证原子性的方法?如何保证多线程下 i++ 结果正确?
- 使用 synchronized 关键字
- 使用原子类
- 使用 Lock 接口中的锁
Atomic 原子类?
- Atomic 原子类提供简单高效的方式来实现线程安全的操作
- 避免了使用显式的锁机制
- 在低并发的情况下,性能优于传统的锁机制
AtomicInteger 的原理?
- 原子类主要利用 CAS 来实现原子性操作
什么是乐观锁?什么是悲观锁?使用场景?
- 乐观锁:
- 乐观锁认为数据在多线程之间不会产生冲突
- 因此在读取数据时不会加锁,而是在更新数据时检查数据版本或者使用 CAS 操作来保证数据的一致性
- 适用于读多写少,且数据冲突概率低的情况
- 悲观锁:
- 悲观锁认为并发访问的时候数据冲突的概率较大
- 因此在读写数据的时候会加锁,阻止其他线程对数据的访问
- 适用于读写操作频繁,数据冲突概率高的情况
如何实现乐观锁?
- 乐观锁使用版本号机制或 CAS 算法实现
公平锁和非公平锁的区别?
- 公平锁:锁被释放的时候,先等待的线程先得到锁
- 非公平锁:锁被释放的时候,线程随机获取锁
可中断锁和不可中断锁的区别?
- 可中断锁:获得锁的过程可以被中断,不需要等到获取锁之后才能进行逻辑处理,如 ReentrantLock
- 不可中断锁: 一旦线程申请了锁,就只有拿到锁以后才能进行逻辑处理,如 synchronized
共享锁和独占锁有什么区别?
- 共享锁:一把锁可以被多个线程同时获得
- 独占锁:一把锁只能被一个线程获得
线程持有读锁还能获取写锁吗?
- 线程持有读锁的情况下,线程不能获取写锁
- 线程持有写锁的情况下,线程可以继续获得读锁
读锁为什么不能升级为写锁?
- 写锁可以降级为读锁,读锁不能升级为写锁,这是因为写锁是独占锁,读锁升级为写锁会引起线程的争夺
CountDownLatch(倒计数器)了解吗?
- CountDownLatch,倒计时器,是 Java 并发包的一个同步辅助类
- 主要用于等待一组线程的执行完成
- 工作方式:通过一个初始计数器,然后在主线程中等待,直到计数器减少到零
- 两个常见的使用场景
- 等待一组线程完成
- 统一各线程动作开始的时机
CyclicBarrier(同步屏障)了解吗?
- CyclicBarrier,可循环使用的屏障,是 Java 并发包中的同步辅助类
- 用于让一组线程等待,直到所有线程到达共同的屏障点,然后在屏障点继续执行
CyclicBarrier 和 CountDownLatch 有什么区别?
- 可循环性:
- CountDownLatch 是一次性的
- CyclicBarrier 可以多次设置屏障,实现循环使用
- 线程等待方式:
- CountDownLatch 中的每个线程在到达指定计数前不需要等待其他线程
- CylicBarrier 中的所有线程需要等待其他线程到达屏障
Semaphore(信号量)了解吗?
- Semaphore,信号量,用来控制同时访问特定资源的线程数量,从而对线程的并发访问
- 适用于资源有限的场景,比如数据库连接池、线程池、网络连接等
- 还可以实现流量控制
Exchanger 了解吗?
- Exchanger,是 Java 并发编程的同步工具
- 提供一个同步点,让两个线程可以交换彼此的数据
- 每个线程在交换数据时都会被阻塞,直到另一个线程也准备好交换数据
- 可以用于多线程间的数据交换,适用于需要线程间协作的场景
ThreadLocal 是什么?
- ThreadLocal 是一个创建线程局部变量的类
- 线程局部变量:每个线程都拥有自己独立的变量副本,在不同线程之间互不干扰
- 通过 ThreadLocal,每个线程可以独立访问和修改自己的变量副本,不会影响其他线程的副本
你在工作中用到过 ThreadLocal 吗?
- 用户会话管理
- 数据库连接管理
- 事务管理
- 线程上下文信息传递
ThreadLocal 怎么实现的呢?
- ThreadLocal 通过在每一个线程中维护一个 ThreadLocalMap 来实现
- 在存储值时,实际上是将这个值存储在当前线程的 Map 中
- ThreadLocalMap 是一个类似于哈希表的结构,key 是 ThreadLocal 实例的弱引用,value 是需要存储的值
ThreadLocal 内存泄露是怎么回事?
- ThreadLocalMap 中的 key 是 ThreasLocal 的弱引用,而 value 是强引用
在垃圾回收的时候,会出现 ThreadLocal 对象被回收,但是 value 还在 ThreadLocalMap 中的情况,就会导致内存泄露 - 解决方法:使用完 ThreadLocal 后,调用 remove() 方法释放内存空间
ThreadLocalMap 的结构了解吗?
ThreadLocalMap 是一个类似于哈希表的结构,key 是 ThreadLocal 实例的弱引用,value 是需要存储的值
父子线程怎么共享数据?
- 使用 InheritableThreadLocal 类
为什么要用线程池/线程池的好处?
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁的开销
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行
- 提高线程的可管理性:使用线程池可以对线程进行统一的分配
能简单说一下线程池的工作流程吗?
- 创建线程池:初始化阶段创建一定数量的线程
- 将任务加入任务队列:当一个任务请求到达时,将其加入任务队列
- 线程执行任务:空闲的线程从消息队列中获取任务,执行任务
- 继续获取任务执行:任务完成后,线程并不终止,而是回到线程池中,准备获取新的任务继续执行
- 线程销毁:系统关闭时会回收所有线程资源
线程池常见参数有哪些?如何解释?
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- workQueue:等待队列
- handler:饱和拒绝策略
线程池的饱和策略有哪些?/线程池的拒绝策略有哪些?
-
AbortPolicy:直接抛出异常
-
CallerRunsPolicy:用调用者所在的线程执行任务
-
DiscardOldestPollicy:丢弃队列最老的任务
-
DiscardPolicy:直接丢弃任务
-
核心线程池:当提任务到达线程池时,线程池判断核心线程池是否已满,如果没有,则创建线程,执行任务
-
队列:如果核心线程池已满,线程池判断工作队列是否已满,如果没有,则将新的任务存储在队列中
-
线程池:如果队列已满,线程池判断判断线程池是否已满,如果没有,创建新的线程,执行任务
-
饱和策略:如果线程池已满,则交给饱和策略来处理这个任务
线程池常用的阻塞队列有哪些?
- ArrayBlockingQueue:基于数组实现的堵塞队列
- LinkedBlockingQueue:基于链表实现的阻塞队列
- SynchronousQueue:同步队列,不存储元素
- DelayQueue:延迟队列
线程池提交 execute 和 submit 有什么区别?/execute() vs submit()?
- execute:用于提交不需要返回值的任务
- submit:用于提交需要返回值的任务
线程池怎么关闭知道吗?
- 可以调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池
- 原理:遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程
- shutdown:关闭提交通道,等已有线程执行完,关闭线程池
- shutdownNow:立即关闭线程池
线程池的线程数应该怎么配置?/ 配置线程池的参数?/ 如何设定线程池的大小?
- 计算密集型的线程池:一般是 CPU 数 +1,+1 是因为可能存在页缺失
- IO 密集型的线程池:一般是 CPU 数 *2
Executor 框架?
- Executor 框架是 JDK5 引进的并发编程工具
- 使用 Executor 框架替代 Thread 的方法来启动线程 ,可以更好管理线程资源
创建线程池的两种方式?
- 通过 ThreadPoolExecutor 构造函数创建
- 通过 Executor 框架的工具类创建
有哪几种常见的线程池?
- FixedThreadPool:固定数据线程的线程池
- SingleThreadExecutor:单线程的线程池
- CachedThreadPool:可缓存线程的线程池
- ScheduleThreadPool:定时执行的线程池
线程池异常怎么处理知道吗?
- try-catch 捕获异常
- Future 类的 get 方法接收异常
- 自定义处理未检测的异常
能说一下线程池有几种状态吗?
- RUNNING:运行状态
- SHUTDOWN:不接受新任务,但处理已有任务状态
- STOP:不接受新任务,中断所有任务
- TIDYING:所有任务已停止的状态
- TERMINATED:线程池终止状态
Fork/Join 框架了解吗?
- Fork/Join 框架是用于实现任务并行的框架
- 基于分治的思想,把大任务拆分成若干个小任务,最终并行执行小任务,最后将结果合并后得到大任务的结果
Runnable vs Callable?
- Runnable 接口不会返回结果或抛出检查异常
- Callable 接口可以通过 Future 类的 get 方法来获取返回结果和抛出异常
Thread、Runnable、Callable、Future、FutureTask 关系?
- Thread:Java 中的线程类,通过继承 Thread 类,重写 run 方法创建线程
- Runnable:一个接口,通过实现 Runnable 接口,实现 run 方法可以创建线程
- Callable:类似于 Runnable,但是可以返回执行结果或抛出异常
- Future:表示异步计算的结果,可以用来检查任务是否完成、获取任务的执行结果
- FutureTask:实现 Future 接口,可以接受 Runnable 或 Callable 实例作为参数,能够获取任务的执行结果
Runnable 和 Callable 是为了将任务逻辑和线程分离,提高代码的灵活性
Future 和 FutureTask 提供一种处理异步任务结果的机制,使得主线程可以等待异步任务的完成并获取结果
CompletableFuture 类有什么用?
- CompletableFuture 是 Java8 引入的一个类,用于处理异步编程
- 针对 Future 类做了改进,可以方便进行异步任务的组合和转换
ReentrantLock?
- ReentrantLock 是 Java并发包中的一个可重入的互斥锁
- 实现 Lock 接口
- 提供比使用 synchronized 关键字更高级别的同步控制机制
ReentrantLock 实现原理?
-
ReentrantLock 的核心实现是通过一个 AQS 同步器来实现锁的获取和释放
-
获取锁的过程:
- 调用 ReentrantLock 的 lock() 方法
- 调用 AQS 的方法尝试获取锁
- 锁是空闲的,获取锁成功
- 锁已经被持有,就将线程加入到等待队列中
- 当锁被释放后,AQS 会从等待队列中唤醒一个线程,将锁分配给该线程
-
释放锁的过程:
- 调用 ReentrantLock 的 unlock() 方法
- 调用 AQS 的方法释放锁
ReentrantLock 怎么实现公平锁?
- new ReentrantLock() 构造函数默认创建的是非公平锁,非公平锁调用 lock 后,会调用 CAS 进行抢锁
- 可以在创建锁构造函数中传入具体参数创建公平锁,公平锁会判断等待队列是否有线程处于等待状态,如果有就不进行抢锁
synchronized 和 ReentrantLock 的区别?
- 锁的实现:
- synchronized 关键字,基于 JVM 实现
- ReentrantLock 基于 JDK 的 API 实现
- 功能特点:相比较 synchronized,ReentrantLock 增加了高级功能,比如等待可中断、可实现公平锁和可实现选择性通知
- 等待可中断:ReentrantLock 提供能够中断等待锁的线程的机制
- 可实现公平锁:ReentrantLock 可以指定是公平锁还是非公平锁,而 synchronized 只能是非公平锁
- 可实现选择性通知:用 ReentrantLock 结合 Condition 实例可以实现选择性通知
ReentrantReadWriteLock 是什么?适合什么场景?
- ReentrantReadWriteLock:是一个可重入的读写锁
读锁是共享锁,写锁是独占锁,底层也是基于 AQS 实现的 - ReentrantReadWriteLock 能够保证多个线程同时读的效率,也能保证写入操作的线程安全,适合读多写少情况
秋招后端开发面试题系列目录
一、Java
1.1 Java基础上
1.2 Java基础下
1.3 Java集合
1.4 JavaIO
1.5 Java多线程上
1.6Java多线程下
二、JVM
2.1 JVM底层原理
2.2 垃圾回收器
2.3 垃圾回收算法
2.4 类加载机制
2.5 运行时数据区
三、MySQL
3.1 MySQL基础
3.2 事务
3.3 索引
3.4 锁机制
3.5 MVCC
四、Redis
4.1 Redis基础
4.2 缓存原理
五、中间件
5.1 RabbitMQ
六、Spring开源框架
6.1 Spring
6.2 Spring MVC
6.3 Spring Boot
6.4 MyBatis
七、操作系统
八、计算机网络
九、设计模式
十、微服务架构
十一、Spring Cloud分布式
11.1 分布式基础
11.2 Spring Cloud
11.3 GateWay
11.4 Nacos
11.5 OpenFeign
11.6 Ribbon
十二、算法
十三、项目