1. 进程和线程的区别,进程间如何通信
进程:系统运行的基本单位,进程在运行过
程中都是相互独立,但是线程之间运行可以
相互影响
线程:独立运行的最小单位,一个进程包含
多个线程且它们共享同一进程内的系统资源
进程间通过管道、 共享内存、信号量机制、
消息队列通信
2. 什么是线程上下文切换
当一个线程被剥夺 cpu 使用权时,切换到另
外一个线程执行
3. 什么是死锁
死锁指多个线程在执行过程中,因争夺资源
造成的一种相互等待的僵局
4. 死锁的必要条件
① 互斥条件:同一资源同时只能由一个线
程读取
② 不可抢占条件:不能强行剥夺线程占有
的资源
③ 请求和保持条件:请求其他资源的同时
对自己手中的资源保持不放
④ 循环等待条件:在相互等待资源的过程
中,形成一个闭环
想要预防死锁,只需要破坏其中一个条件即可,
比如使用定时锁、尽量让线程用相同的加锁顺
序,还可以用银行家算法可以预防死锁
5. Synchrpnized 和 lock 的区别
① synchronized 是关键字,lock 是一个类
② synchronized 在发生异常时会自动释放
锁,lock 需要手动释放锁
③ synchronized 是可重入锁、非公平锁、
不可中断锁,lock 的 ReentrantLock 是
可重入锁,可中断锁,可以是公平锁也
可以是非公平锁
④ synchronized 是 JVM 层次通过监视器
实现的,Lock 是通过 AQS 实现的
6. 什么是 AQS 锁?
AQS 是一个抽象类,可以用来构造锁和同
步类,如 ReentrantLock,Semaphore,
CountDownLatch,CyclicBarrier
AQS 的原理是,AQS 内部有三个核心组件:
① state 代表加锁状态初始值为 0
② 获取到锁的线程
③ 阻塞队列
当有线程想获取锁时,会以 CAS 的形式将
state 变为 1,CAS 成功后便将加锁线程设
为自己
当其他线程来竞争锁时会判断 state 是不是
0,不是 0 再判断加锁线程是不是自己,不
是的话就把自己放入阻塞队列,这个阻塞队
列是用双向链表实现的
可重入锁的原理就是每次加锁时判断一下加
锁线程是不是自己,是的话 state + 1,释放
锁的时候就将 state - 1,当 state 减到 0 的
时候就去唤醒阻塞队列的第一个线程
7. 为什么 AQS 使用的双向链表?
因为有一些线程可能发生中断 ,而发生中断
时候就需要在同步阻塞队列中删除掉,这个
时候用双向链表方便删除掉中间的节点
8. 有哪些常见的 AQS 锁
AQS 分为独占锁和共享锁
ReentrantLock (独占锁):可重入,可中断,
可以是公平锁也可以是非公平锁,非公平锁
就是会通过两次 CAS 去抢占锁,公平锁会
按队列顺序排队
Semaphore (信号量):设定一个信号量,当
调用 acquire() 时判断是否还有信号,有就获
取一个信号量,没有就阻塞等待其他线程释
放信号量,当调用 release() 时释放一个信号
量,唤醒阻塞线程
应用场景:允许多个线程访问某个临界资源
时,如上下车,买卖票
CountDownLatch (倒计数器):给计数器设置
一个初始值,当调用 CountDown() 时计数器
减一,当调用 await() 时判断计数器是否归 0,
不为 0 就阻塞,直到计数器为 0
应用场景:启动一个服务时,主线程需要等待
多个组件加载完毕,之后再继续执行
CyclicBarrier (循环栅栏):给计数器设置一个
目标值,当调用 await() 时会计数 + 1 并判断计
数器是否达到目标值,未达到就阻塞,直到计
数器达到目标值
应用场景:多线程计算数据,最后合并计算结
果的应用场景
9. sleep() 和 wait() 的区别
① wait() 是 Object 的方法,sleep() 是 Thread
类的方法
② wait() 会释放锁,sleep() 不会释放锁
③ wait() 要在同步方法或者同步代码块中执行,
sleep() 没有限制
④ wait() 要调用 notify() 或 notifyall() 唤醒,
sleep() 自动唤醒
10. yield() 和 join() 区别
yield() 调用后线程进入就绪状态
A 线程中调用 B 线程的 join() ,则 B 执行完前
A 进入阻塞状态
11. 线程池七大参数
① 核心线程数:线程池中的基本线程数量
② 最大线程数:当阻塞队列满了之后,逐一
启动
③ 最大线程的存活时间:当阻塞队列的任务
执行完后,最大线长的回收时间
④ 最大线程的存活时间单位
⑤ 阻塞队列:当核心线程满后,后面来的任
务都进入阻塞队列
⑥ 线程工厂:用于生产线程
⑦ 任务拒绝策略:阻塞队列满后,拒绝任务,
有四种策略:抛异常
丢弃任务不抛异常
打回任务
尝试与最老的线程竞争
12. Java 内存模型
JMM (Java内存模型) 屏蔽了各种硬件和操作
系统的内存访问差异,实现让 Java 程序在各
平台下都能达到一致的内存访问效果,它定
义了 JVM 如何将程序中的变量在主存中读取
具体定义为:
所有变量都存在主存中,主存是线程共享区域;
每个线程都有自己独有的工作内存,线程想要
操作变量必须从主从中 copy 变量到自己的工
作区,每个线程的工作内存是相互隔离的由于
主存与工作内存之间有读写延迟,且读写不是
原子性操作,所以会有线程安全问题
13. 保证并发安全的三大特性?
原子性:一次或多次操作在执行期间不被其他
线程影响
可见性:当一个线程在工作内存修改了变量,
其他线程能立刻知道
有序性:JVM 对指令的优化会让指令执行顺
序改变,有序性是禁止指令重排
14. volatile
保证变量的可见性和有序性,不保证原子性,
使用了 volatile 修饰变量后,在变量修改后
会立即同步到主存中,每次用这个变量前会
从主存刷新
单例模式双重校验锁变量为什么使用 volatile 修
饰?
禁止 JVM 指令重排序,
new Object() 分为三个步骤:
① 申请内存空间
② 将内存空间引用复制给变量
③ 变量初始化
如果不禁止重排序,有可能得到一个未经初始化的变量
15. 线程使用方式
① 继承 Thread 类
② 实现 Runnable 接口
③ 实现 Callable 接口,带有返回值
④ 线程池创建线程
16. ThreadLocal 原理
原理是为每个线程创建变量副本,不同线程
之间不可见,保证线程安全,每个线程内部
都维护了一个 Map,key 为 threadLocal 实
例,value 为要保存的副本
但是使用 ThreadLocal 会存在内存泄露问题,因为 key 为弱引用,而 value 为强引用,每
次 gc 时 key 都会回收,而 value 不会被回
收
解决内存泄漏问题:
每次使用完后删除 value 或者使用 static 修
饰 ThreadLocal,可以随时获取 value
17.什么是CAS锁
CAS 锁可以保证原子性,思想是更新内存时会
判断内存值是否被别人修改过,如果没有就直
接更新,如果被修改,就重新获取值,直到更
新完成为止
缺点:
① 只能支持一个变量的原子操作,不能保证整
个代码块的原子操作
② CAS 频繁失败导致 CPU 开销大
③ ABS 问题:线程 1 和线程 2 同时去修改一
个变量,将值从 A 改为 B,但线程 1 突然阻
塞,此时线程 2 将 A 改为 B,然后线程 3 又
将 B 改成 A,此时线程 1 将 A 又改为 B,这
个过程线程 2 是不知道的,这就是 ABA 问题,
可以通过版本号或时间戳解决
18. Synchronized 锁原理和优化
Synchronize 是通过对象头的 markwordk 来表
明监视器的,监视器本质是依赖操作系统的互
斥锁实现的,操作系统实现线程切换要从用户
态切换为核心态,成本很高,此时这种锁叫重
量级锁,在 JDK1.6 以后引入了偏向锁、轻量
级锁、重量级锁
偏向锁:当一段代码没有别的线程访问,此时
线程去访问会直接获取偏向锁
轻量级锁:当锁是偏向锁时,有另外一个线程
来访问,会升级为轻量级锁,线程会通过 CAS
方式获取锁,不会阻塞,提高性能
重量级锁:轻量级锁自旋一段时间后线程还没
有获取到锁,会升级为重量级锁,重量级锁时,
来竞争锁的所有线程都会阻塞,性能降低注意,
锁只能升级不能降级
19. 如何根据 CPU 核心数设计线程池线程数量
IO 密集型:线程中十分消耗 Io 的线程数 * 2
CPU 密集型: cpu 线程数量
20. AtomicInteger 的使用场景
AtomicInteger 是一个提供原子操作的 Integer
类,使用 CAS + volatile 实来现线程安全的数
值操作
因为 volatile 禁止了 jvm 的排序优化,所以它
不适合在并发量小的时候使用,只适合在一些
高并发程序中使用