JUC 并发面试题
进程和线程的区别
进程是一个程序运行的基本单位,电脑上每运行一个软件都对应着一个进程
线程是程序运行的最小单位,一个进程在执行的过程中可以有多个线程共同执行
线程的创建方式
- 继承 Thread 类
- 实现Runable 接口
- 实现Callable 接口
- 通过线程池创建
通常当我们需要返回值的时候,我们会实现 Callable 接口重写里面的 call 方法来获取线程运行后的返回值,当我们不需要返回值的时候,我们会用实现 Runable 接口的方法来创建线程,因为 Java 只支持单继承,为了不违背单一原则,我们通常不会选择让线程多重继承,而是实现 Runable 接口,而且 Thread 类也实现了 Runable 接口,所以继承 Thread 类本质上也是实现 Runable 接口
线程池
线程池的工作流程:
- 当有新的任务被提交的时候,会先判断线程池中的核心线程池是否已经满了
- 如果核心线程已经全部在处理任务,那么当前任务会进入阻塞队列中进行等待
- 如果阻塞队列也塞满了待处理的任务,那么线程池就会创建临时线程处理任务
- 如果当前线程池的核心线程池、阻塞队列、临时线程全部满编,线程池就会执行拒绝策略来处理当前任务
拒绝策略
- AbortPolicy:丢弃任务并抛出异常
- DiscardPolicay:丢弃任务但不抛出异常
- DiscardOldestPolicy:丢弃队列中最早提交的任务
- CallerRunsPolcicy:将任务返回给调用者执行
线程池的核心参数
-
最大核心线程数
-
最大线程数
-
最大存活时间
-
最大存活时间单位
-
拒绝策略
-
阻塞队列
-
线程工厂
提问:当线程池创建后,里面是直接有线程的吗,
答:不是,只有提交了任务后才会创建线程,其中核心线程不会被回收,非核心线程会在最大存活时间到达后被回收
线程状态
根据 Java 源代码可知,线程共有6种状态:
- NEW 初始状态,当一个线程被创建且没有被start方法调用的时候的状态
- RUNABLE 运行状态,RUNABLE分为READY 就绪状态:线程被启动后但还没有分配到 CPU 的资源, RUNNING状态 : 分到了 CPU 的资源开始执行任务
- WAITING 等待状态:线程调用
wait()
join()
park()
方法后所处的状态 - TIMED_WAITING 有时限的等待状态:线程调用
wait(Long)
join(Long)
park(Long)
sleep(LOng)
方法后进入的状态 - BLOCKED 阻塞状态:线程获取锁失败后进入的状态
- TREMINATED 终止状态:线程执行完任务后的状态
线程安全问题
Synchronized 和 Lock 锁的区别
Synchronized 是Java 的关键字 ,是 JVM 层面的锁,Lock 是 Java 的API 锁
Synchronized 只有在运行完代码块中的代码或者出现异常后才会自动释放锁,Lock是必须手动释放锁,所以一般搭配final代码块使用,不然会产生死锁问题
Synchronized 是排他锁,Lock 锁的读锁是共享锁,而写锁是排他锁
Synchronized 不能判断锁的状态,而Lock锁可以通过 API 查看
Synchronized 是可重入的非公平锁 ,Lock 锁是可重入的公平锁
锁分类
- 悲观锁:默认会假设最坏的情况,认为每次共享资源被修改的时候就会出现问题,所以在每次获取资源的操作的时候都会上锁,如:Synchronizd Lock 数据库的行锁表锁等
- 乐观锁:总是会假设最乐观的情况,认为共享资源每次被访问的时候不会出现问题,实现如:CAS
CAS 乐观锁导致的 ABA 问题:
当一个线程修改一个 元值为 A 的数据时,此时有另一个线程来将 A 修改为 B 又修改为 A ,此时虽然 A的值和原来的值一样,但是它已经是一个被修改后的值了
解决方法是我们在每次修改值的时候加上版本号,如果版本号相同再修改,如果觉得每次的版本号++过于繁琐,可以使用 boolean 方法,判断当前的版本是否是自己的 true 或者 false
volatile
volatile 关键字可以保证多线程运行时的可见性和有序性
volatile 底层运用的是内存屏障来保证可见性和有序性的,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。同时内存屏障之前的写操作会刷到主内存中,内存屏障后的读操作都能获取到主内存中的最新结果
证可见性和有序性的,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。同时内存屏障之前的写操作会刷到主内存中,内存屏障后的读操作都能获取到主内存中的最新结果