JUC面试内容(常问点)
写在前面
- 博客首页
- 内容由思维导图转换而来
JUC
锁
Sychronized
写在哪?
静态方法,锁的是类对象(class)
成员方法,代码块,锁的是对象本身(this)
同步原理:临界区代码不受CPU时间片轮换的影像,就是不切换线程的上下文
JVM层面的加锁解锁,moniterenter、moniterexit、ACC_SYCHRONIZED,代码块上就是moniterenter和moniterexit两个原子操作,方法上好像是ACC_SYCHRONIZED
操作系统层面,调用的是其Monitor锁对象,所以当真正锁住时,是操作系统级别的
Monitor锁对象中存在waitSet和entrySet两个队列,以及owner拥有锁
对象头中的markword32位,低两位是锁标识,无锁01,轻量级00,重量级10
每个对象可以拥有一个minitor过程:当第一个线程到达临界区,会把对象头中markword锁标识置为重量级锁,将对象头前面地址指向monitor,monitor对象中的owner则指向当前线程,然后进入临界代码区。多个线程的话,会进入到entryset中阻塞住,然后被唤醒时会进入waitset进行等待。
锁优化
轻量级锁
锁标识00
第一个线程先到达,将锁标志位置为轻量级锁,然后将栈帧中开辟一个锁记录的空间,存放对象头内容,然后将锁记录的地址存放到对象头,这样就完成了线程与锁对象的关联。
当第二个线程到达时,先判断锁标识
如果有轻量级锁,再判断是不是当前线程自己内部的锁记录,是就在创建一个锁记录地址存null,这就支持了可重入特性
如果不是当前线程,则自旋等待,多次尝试超过一个阈值次数,还是失败,就进入重量级锁加锁流程
如果没有锁
偏向锁
- 锁标识01,bias_lock偏置锁标识1
现在加锁默认是加偏向锁,偏向锁标志位置为1,然后前面的几位存放偏向线程的信息
Volatitle
内存屏障
防止指令重排
StoreStore 写写屏障
StoreLoad 写读屏障
LoadLoad 读读屏障
loadStore 读写屏障解决多线程读写数据可见性
遵循happensBefore原则中的volatile读原则,每次单个线程修改volatile修饰的数据后,其他线程拿到的数据就会失效,需要去共享内存中获取
- Lock指令:将当前处理器缓存行的数据写回到系统内存
- Lock指令:写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效。
先行原则 happens-before
七大原则
- 程序顺序远测
- 锁原则
- volatile变量原则:如果一个线程先去写一个volatile变量,那么他必须先去读这一个变量,所以,再任何情况下volatil修饰的变量的值再修改时都是对其他线程可见的
- 线程启动原则
- 线程终止原则
- 传递性原则
- 终结原则
- 原则定义上:确保可见性
ReentrantLock
Reentrantlock与AQS
功能及关系:可重入锁、可设置为公平锁和非公平锁、AQS是一个实现ReentranLock的关键组件,r锁中sync继承了AQS
AQS
AQS全程AbstractQueuedSynchronizer 这个类是JUC实现Lock和同步器的关键组件,其核心思想就是,对于请求的资源空闲时(state = 0),将请求线程设置为共享线程,然后将共享资源设置为锁定状态(state > 0),如果资源被占用了,就设计出CLH队列,双向虚拟队列用来“存储”这些获取锁失败的线程。CLH本质上使用内部的Node类来实现锁的分配,Node内部比较关键的几个属性,waitStatus标识当前节点状态,为1时是线程被取消了,state(为0是标识当前资源空闲,等于1标识被独占了,大于1标识被重入了,在设置该值的时候,AQS提供了CAS的方法去设置该值),shared,exclusive,表示当前节点的状态共享、独占,独占表示只有一个线程能执行,并且有fair和NonFair两种方式,如借助其实现的ReentranLock,而共享表示有多个线程可同时执行。关于自己设计自定义同步器的话,重要的是tryAcquire() releaseAcquire(),其实就是对共享状态state的获取和释放,其底层采用模板方法设计模式,提供模板方法给子类去实现,如果没记错的话,有五个,独占获取和释放,共享获取和释放,以及isHeldExclusively()当前线程是否正在独占资源。
当然AQS中还有一个ConditionObject,等待队列,用于放执行了condition.await()后等待的线程,并且同一个锁可以有多个condition,例如在R锁中是lock.newCondition()来使用这个condition.await()与object的await()的主要区别有,其可以设置多个等待队列,可以理解为创建多个休息室,然后让不同类型的线程进入不同的休息室等待,当被唤醒时,加入到阻塞队列后面去排队。
状态 Node.waitStatus 1 -1 -2 -3,小于0表示需要唤醒后继节点
CLH队列,虚拟的双向队列 存储获取锁失败而阻塞的线程
锁状态 Node.state 为0表示当前锁状态为未加锁,为1表示加锁了, 大于1则是重入了
继承了AbstractOwnableSynchronizer,来设置独占资源线程和获取独占资源线程
非公平锁NonFairSyn 和 FaitSync公平锁的区别
公平锁只要资源被其他线程占用,该线程就会添加到sync queue中的尾部,而不会先尝试获取资源。这也是和Nonfair最大的区别,Nonfair每一次都会尝试去获取资源,如果此时该资源恰好被释放,则会被当前线程获取,这就造成了不公平的现象,当获取不成功,再加入队列尾部
关于wait,sleep,notify notifyAll,Signal,SignalAll以及Sychronized和ReentrantLock相关理论阐述!
- 首先,wait是Object的final方法,调用后呢会释放对cpu的占用,进入到阻塞队列,sleep是Thread线程类的静态方法,调用之后,睡眠一段时间,但是不会释放对cpu的占用其次,wait方法就是必须和sychronized一起使用,每个对方被sychornized锁住的时候,都会产生一个monitor对象,先进来的线程,会将自己的线程信息,比如id存放到monitor对象上的owner属性,这样标明目前被谁占用,另外当有调用wait()方法时,会进入到_WaitSet等待集合中,调用signalAll就会唤醒线程,去争抢锁,如果失败了,就会进入阻塞队列_EntryList上。而ReentrantLock是和Conition配合使用,可以产生多个condition对象,这就相当于是多个等待室,condition.await()会进入到对应的等待室,然后在调用condition.signal()或者condition.signalAll()唤醒等待。
JMM内存模型
功能:定义了线程与主内存之间的抽象关系,是一种控制Java线程之间的通信的规范,用于解决多线程中存在的问题,JMM 说白了就是定义了一些规范来解决这些问题,开发发者可以利用这些规范更方便地开发多线程程序
线程与内存关系:每个线程都拥有自己的工作内存,所有的数据都保存在主内存
解决CPU多级缓存,JIT即时编译器、处理器自身优导致的重排序导致的内存访问问题,大抵就是并发编程问题(三个问题)
内存模型实现
- 可见性:Java 中的 Volatile 关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存。
- 有序性:Volatile 关键字会禁止指令重排。Synchronized 关键字保证同一时刻只允许一条线程操作
- 原子性:提供了两个高级的字节码指令 Monitorenter 和 Monitorexit
TreadLocal
功能:当多个线程要访问某个变量时,让每个变量保存单独的一份变量副本
描述:灭个thread内部会拥有一个ThreadLocal.ThreadLocalMap,然后其key是ThreadLocal对象且是weakReference
实现
hash
- 使用斐波那契数进行散列,容量capacity为2的次方,这样减1与hashcode进行与操作,达到和取模同等效果,但是会更快
- 通过散列探测清理解决冲突,不是用链表
threadLocalMap
- 内部entry[] table数组
- 扩容阈值:2/3, 初始大小16(默认)
- 扩容机制:达到2/3进行rehash,会进行清理,然后当清理后的长度等于数组的一半:ken*2/3 *3/4
- 探测式清理 与 启发式清理
内存泄漏
- 问题:Entry继承了WeakReference,ThreadLocal对象key的弱引用,而外部存在对ThreadLocal的强引用,当失去强引用,并且进行了GC回收,那么就有可能出现内存泄漏,就是因为其value作为强引用没有被回收,但是其实就算Entry的key是强引用,GC时不回收key,那么这个entry也不会被回收,长时间也会导致内存泄漏,本质原因,其实是因为ThreadLocalMap和Thread的生命周期是一致的。
- 解决:set的时候是会进行key值为null数据进行清理,使用完没有remove有内存泄漏的风险,使用的时候也尽量不要存大对象
使用场景:作为用户登录进来后,在当前会话内保存用户的信息,token当然是存储在redis中的
线程池
核心属性
corePoolSize 核心线程数
maxPoolSize 最大线程数
keepAliveTime:等待时间
blokingQueue
- ArrayBlockingQueue(数组结构的有界阻塞队列,按FIFO排序)
- LinkedBlockingQueue(链表结构的阻塞队列,FIFO)
- SynchronousQueue(不存储任务的队列,需要等待移除操作后,才能插入新的,不然插入操作会一直阻塞)
- PriorityBlockingQueue(具有优先级的无界阻塞队列)
handler 拒绝策略
- 直接拒绝,并抛出异常(RejectedExecutionException)AbortPolicy(默认)
- 直接丢失任务DiscardPolicy
- 丢出队首的任务,并执行当前的任务 DiscardOldestPolicy
- 用调用者所在的线程运行当前任务 CallerRunPolicy
三种线程池
- newFixedThreadPool: core = max即使线程池没有执行的任务也不会即使释放,因为其阻塞的队列采用的LinkedBlockingQueue,无界队列所以拒绝策略会失效
- newSingleThreadExecutor:程池中只有一个线程,如果一个线程异常结束,会创建新的线程出来,唯一的线程可以保证按顺序执行,并且阻塞队列采用的是LinkedBlockingQueue,所以不会拒绝
- newCachedThreadPool:线程池中的最大线程数为Integer.MAX_VALUE,阻塞队列采用的是SynchronizeQueue,如果线程空闲时间超过等待时间就销毁,当有新任务进来时,如果存在空闲进程读取队列,则交给该线程执行,如果线程都正在执行的任务,就直接创建新线程执行该任务,因为空闲后超过一定时间,就会被销毁。
功能:避免无限制的去创建线程,结束后销毁线程,所造成的资源消耗,提高线程的可管理性。
线程与进程
- 进程是资源分配的最小单位,线程是cpu调度的最小单位,以java为例,main函数启动了一个JVM进程,而main函数所在的线程就是主线程
- 线程比进程更轻量,同一进程的线程切换比进程间切换快,因为共享了一些内存
- 再Java的Jvm中,像堆和方法区这些就是进程内线程共享的,而虚拟机栈,本地方法栈,程序记数器就是线程私有的