锁机制和关键字
synchronized底层原理
synchronized的实现原理依赖JVM的Monitor监视器锁和对象头Mark Word
synchronized修饰方法和代码块的方式不同:
- 方法:在方法的访问加上标志,线程进入方法前,JVM会检查这个标志,有标志就得先拿到监视器锁才能执行
- 代码块:编译器会在代码块前后插入加锁和解锁字节码指令,而且编译器会自动生成异常处理的解锁字节码指令,保证抛异常也能解锁
重点讲代码块中的执行流程:编译后会在代码块入口插入加锁字节码指令,在正常和异常出口插入解锁字节码指令。线程执行加锁指令时。如果成功则进入临界区执行代码,失败就阻塞等待。

对象头
每个Java对象内存中都有对象头,他会根据锁的状态保存不同信息。无锁存哈希码,偏行锁存线程ID,轻量级锁存指针,重量级锁存指向加锁对象的指针。
synchronized 锁升级流程
锁升级:
- 从无锁到偏向锁:当一个线程首次访问加锁指令,会将对象头设置为偏向锁
- 从偏向锁到轻量级锁:当另一个线程尝试获取被偏向的锁时,JVM讲取消偏向模式,升级为轻量锁
- 从轻量锁到重量级锁:当轻量级锁发生竞争时,及CAS失效,就会升级。此时线程进入阻塞,等待锁释放
偏向锁、轻量级锁、重量级锁各自原理和适用场景?
偏向锁
原理:偏向锁会在对象头的MarkWord中记录:当前持有锁的线程ID
若后续还是该线程访问,不需要CAS,不需要加锁就能执行
适用场景:
单线程反复进入同步代码块
轻量级锁
原理:CAS+自旋
线程会在自己的帧栈中创建Lock Record,通过CAS尝试修改对象头获取锁
CAS成功-获得锁
失败-说明存在竞争;自旋等待
适用场景:
少量线程交替竞争
重量级锁
原理:Monitor+操作系统Mutex
竞争失败就会进入Blocked
适用场景:杠比广发激烈竞争
synchronized 修饰普通方法、静态方法、代码块,锁的对象分别是谁?
普通方法:this对象;当前调用这个方法的实例对象。每个对象都有自己的独立锁;对象A和对象B互补影响
静态方法:这个类的Class对象,不管new了多少个实例,都公用同一把锁
同步代码块:锁obj对象(括号中的指定对象)
vloatile关键字的作用是什么?
核心作用两个:可见性 和禁止指令重排
可见性:一个线程改了vloatile,其他线程额能立即看到最新值。
禁止指令重排:编译器和CPU为了性能会重排指令。在多线程环境下会出问题,打乱了逻辑顺序。
为什么volatile不能用来保证原子性?
java
private static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> count++);
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println(count); // 结果大概率不是 1000
}
原因分析:count++实际由三部分构成(读取;+1;写回)当两个线程同时读到5,都执行+1.结果变成了6.但是预期是7.volatile只保证每次读取的都是最新值,不保证读-改-写这个操作是原子性的。
什么是可重入锁,为什么需要可重入锁
可重入锁:同一个线程能重复获取同一把锁,而不会死锁
直观理解就是以嵌套为例,当线程第一次调用a获取到锁时,第二次调用时发现a已经被锁,只能阻塞等待锁释放。锁永远无法释放。这就是死锁
所以可重入锁的产生背景就是Java方法调用经常嵌套。
synchronized 和 ReentrantLock 都是可重入锁。
底层通过:记录当前持有锁线程(owner)和计数器(count)实现
什么是公平锁、非公平锁?优缺点?
两者区分的本质时:多个线程抢锁时,是否按先来后到的顺序
默认时非公平锁,业务性能更高
公平锁需要维护等待队列,每次要检查是不是队首,有额外开销
公平锁:
优点:1.不会出现线程饿死2.更适合资源分配场景
缺点:性能低。额外维护等待队列,线程切换更多
非公平锁:
优点:性能高,减少线程挂起和唤醒
缺点:有线程饿死
悲观锁和乐观锁区别、适用场景?
两种不同的并发控制思想
悲观锁:假设竞争必然发生,先加锁,后操作
优点:数据安全
缺点:性能低,并发能力差
适用场景:
写多读少
实现方式
在MySQL中有两种实现方式。
1.排他锁x锁,用select for update。拿到后独占这行数据,别的事务什么都不能进行
2.共享锁s锁,用select lock in share mode。多个事务可以同时持有同一行s锁,但是拿不到x锁
乐观锁:假设竞争不存在,先操作。等操作完之后检查数据有没有被修改。通常用version字段,读的时候将version读出来,更新时通过where来查看version是否被修改过
优点:性能高
缺点:不安全,会出现ABA问题
实现方式
有两种
1.version字段(版本号):额外维护一个版本号。每次数据更新带上版本号。如果查询的影响行数是0,说明数据被别人改过
2.用CAS比较原值:当预期值和当前值不一样,说明数据被别人改过
CAS 原理是什么?自旋、Unsafe 类作用?
CAS是:硬件级别的原子操作
实现方式:需要三个值:当前值,预期值,更新值。如果当前值==预期值,将当前值更新为更新值。如果!=,自旋重试,直到成功
自旋
while循环
目的:避免线程阻塞。当线程阻塞会出现用户态与内核态的转换(当线程b拿不到锁时,会请求操作系统将线程B挂起),这一步的开销是极高的
自旋适合:锁占用时间短,因为频繁自旋很耗CPU
Unsafe类
JVM提供的底层工具类
作用:绕过JVM安全机制,直接操作:内存,对象,线程,CAS
CAS 三大问题:ABA、循环耗时、只能保证单个变量原子性,怎么解决?
循环耗时
简单来说就是自旋开销大问题
- 限制自旋次数:重试10次失败就阻塞
- 锁升级:将轻量级锁升级为重量级锁
- LongAdder:一个变量拆成多个cell,不同线程操作不同cell,减少CAS冲突
只能保证单变量原子性
问题原因:CAS只能操作一个值
- 加锁
- 合并变量:将多个变量封装成一个对象,然后CAS整个对象引用
什么是死锁,死锁产生的条件是什么
多个线程互相等待对方释放资源,导致有所线程都无法继续执行
死锁产生的四个必要条件
- 互斥:资源同一时刻只能被一个线程占用
- 持有并等待:献策韩国拿着一个锁的同时,还想要另一个锁
- 不可剥夺:线程拿到的锁不能被强行抢走
- 循环等待:线程之间形成环形的锁依赖
避免死锁:
- 固定加锁顺序
- 适用trylock超时机制:ReentrantLock的tryLock尝试获取锁,超时就放弃,避免无限等待。
- 缩小锁粒度
- 避免锁嵌套