文章目录
- 乐观锁与悲观锁
- 乐观锁(无锁编程,版本号机制)
- 悲观锁
- 两种锁的伪代码比较
- 通过 8 种锁运行案例,了解锁
- 锁相关的 8 种案例演示
- 场景一
- 场景二
- 场景三
- 场景四
- 场景五
- 场景六
- 场景七
- 场景八
- synchronized 有三种应用方式
- 8 种锁的案例实际体现在 3 个地方
- 从字节码角度分析 synchronized 实现
- `javap -c ****.class`文件反编译
- synchronized 同步代码块
- synchronized 普通同步方法
- synchronized 静态同步方法
- 对于 synchronized 的深入研究
- 面试题:为什么任何一个对象都可以成为一个锁
- 什么是管程 monitor
- 公平锁与非公平锁
- 为什么会有公平锁和非公平锁的设计?
- 为什么默认使用非公平锁?
- 什么时候用公平锁?什么时候用非公平锁?
- 可重入锁,又叫,递归锁
- 隐式锁(synchronized 默认是可重入锁)
- 同步块
- 同步方法
- synchronized 的可重入原理(基于 objectMonitor.hpp)
- 显示锁(Lock,ReentrantLock)
- 死锁以及排查
- 死锁是什么
- 编写一个死锁 case
- 死锁的故障排查
- 小结
- 见后续
- 自旋锁 SpinLock
- 无锁->独占锁->读写锁->邮戳锁
- 无锁->偏向锁->轻量锁->重量锁
乐观锁与悲观锁
乐观锁(无锁编程,版本号机制)
- 认为自己在使用数据时,不会有别的线程修改数据或资源,所以不会加锁 。
- 在 Java 中通过使用无锁编程来实现,只在更新数据时去判断,之前是否存在其它线程更新此数据。
- 如果这个数据没有被更新,当前线程将自己修改的数据成功写入
- 如果这个线程数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
- 放弃修改,尝试抢锁等
- 判断规则
- 版本号机制 Version
- 最常采用的是 CAS 算法,Java 原子类的递增操作就通过 CAS 自旋实现的
- 适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升
- 乐观锁直接去操作同步资源,是一种无锁算法
- 乐观锁的两种实现方式
- 采用 Version 版本号机制
- CAS(Compare-and-Swap,比较替换算法) 实现
悲观锁
- 认为自己在使用数据时,必然有别的线程来修改数据,因此在获取到数据的时候,进行操作之前,会先加锁,保证数据不被别的线程所修改
- synchronized 关键字与 Lock 锁的实现类均为悲观锁
- 适合写操作多的场景,先加锁可以保证写操作时数据正确,
- 显示锁定之后再进行同步资源
两种锁的伪代码比较
- 悲观锁
//悲观锁基于synchronized关键字
public synchronized void m1(){
//code logic segment....
}
//悲观锁基于Lock对象实现
ReentrantLock reentrantLock = new ReentrantLock();
public void m2(){
reentrantLock.lock();
try {
//code logic segment....
} finally {
reentrantLock.unlock();
}
}
- 乐观锁
//乐观锁的调用方式,保证多个线程使用的是同一个AtomicInteger
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
通过 8 种锁运行案例,了解锁
锁相关的 8 种案例演示
场景一
a,b 两个线程分别使用同步监视器修饰
a 线程启动,0.2 秒后 b 线程启动
可以预见,先执行 a 后执行 b
- 代码
class Phone {//资源类
public synchronized void sendEmail() {
System.out.println(Thread.currentThread().getName() + "-----sendEmail");
}
public synchronized void sendSMS() {
System.out.println(Thread.currentThread().getName() + "-----sendSMS");
}
}
...
public static void main(String[] args) {//一切程序的入口
Phone phone = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "a").start();
//暂停毫秒,保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.sendSMS();
}, "b").start();
}
- 效果
场景二
在场景一的资源类中,sendEmail方法中加入暂停3秒钟
由于 synchronized 是悲观锁,并且 sleep 不回释放锁,因此 a 线程先执行,并且执行 sleep 时程序也会阻塞,当 a 线程执行完毕时,b 线程才会执行
可以预见结果和场景一致
- 代码
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-----sendEmail");
}
- 效果
一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
场景三
添加一个普通的hello方法,先打印邮件还是hello?
普通方法线程共享
class Phone {//资源类
....
public void hello() {
System.out.println("-------hello");
}
....
}
//main方法
public static void main(String[] args) {//一切程序的入口
Phone phone = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "a").start();
//暂停毫秒,保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.hello();
}, "b").start();
}
- 效果
场景四
有两部手机,请问先打印邮件还是短信
两次方法的调用 synchronized 锁住的对象不同
- 代码
//资源类不变
public static void main(String[] args) {//一切程序的入口
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "a").start();
//暂停毫秒,保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone2.sendSMS();
}, "b").start();
}
- 效果
场景五
在场景 一的情况下将两个方法均添加 static 修饰,测试代码同场景一
静态同步方法(类锁)
- 代码
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-----sendEmail");
}
public static synchronized void sendSMS() {
System.out.println(Thread.currentThread().getName() + "-----sendSMS");
}
- 效果
场景六
在场景五的情况下,添加一部手机,用 phone2 调用 sentSMS
- 代码
//资源类同场景五
//测试代码
public static void main(String[] args) {//一切程序的入口
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "a").start();
//暂停毫秒,保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone2.sendSMS();
}, "b").start();
}
- 效果
对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——>实例对象本身(方法调用者),
对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板(调用者所属的类型)
对于同步方法块,锁的是 synchronized 括号内的对象
场景七
有1个静态同步方法,有1个普通同步方法,有1部手机,请问先打印邮件还是短信
//将短信方法还原为普通同步方法
public static synchronized void sendSMS() {
System.out.println(Thread.currentThread().getName() + "-----sendSMS");
}
- 测试代码同场景一
public static void main(String[] args) {//一切程序的入口
Phone phone = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "a").start();
//暂停毫秒,保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.sendSMS();
}, "b").start();
}
- 效果
类锁与对象锁不是同一个,各种执行,a 线程睡眠 3 秒
场景八
有1个静态同步方法,有1个普通同步方法,有2部手机,请问先打印邮件还是短信
//资源类同场景七
//测试代码
public static void main(String[] args) {//一切程序的入口
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "a").start();
//暂停毫秒,保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone2.sendSMS();
}, "b").start();
}
- 效果
当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。
所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this
也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
synchronized 有三种应用方式
8 种锁的案例实际体现在 3 个地方
- 作用于实例方法,为当前方法调用者加锁,进入同步代码前需要获得当前实例的锁
- 作用于代码块,synchronized(obj){},obj 为加锁对象
- 作用于静态方法,当前类加锁,进入同步代码块之前需要获取类对象的锁
从字节码角度分析 synchronized 实现
javap -c ****.class
文件反编译
- -c 作用: 对代码进行反编译
- -v (verbose) 作用 输出附加信息(行号,本地变量表,反编译等详细信息)
synchronized 同步代码块
- 编写测试代码->运行产生 class 文件->进入 class 类路径–>执行 javap -c
Object object = new Object();
public void m1() {
synchronized (object) {
System.out.println("----hello synchronized code block");
}
}
- 反编译结果
- 一般情况下,一个 enter 对应 2 个 exit
- 极端情况==>手动抛出一个异常
Object object = new Object();
public void m1() {
synchronized (object) {
System.out.println("----hello synchronized code block");
throw new RuntimeException("-----exp");
}
}
- synchronized 同步代码块的实现
- 进入锁使用 monitorenter 指令
- 退出锁使用 monitorexit 指令
synchronized 普通同步方法
public synchronized void m2() {
System.out.println("----hello synchronized m2");
}
- 使用
javap -v .\LockSyncDemo.class
进行编译
- 调用指令时检查方法的 ACC_SYNCHRONIZED 访问标志位是否被设置
- 若被设置了,则线程会将先持有 monitor 锁,然后再执行方法
- 最后在方法完成时(无论正常完成还是非正常完成)均会释放 monitor
synchronized 静态同步方法
public static synchronized void m3() {
System.out.println("----hello static synchronized m3");
}
- 执行反编译
- ACC_STATIC, ACC_SYNCHRONIZED 访问标志位区分该方法是否为静态同步方法
对于 synchronized 的深入研究
面试题:为什么任何一个对象都可以成为一个锁
什么是管程 monitor
- HotSport 虚拟机中,monitor 采用 ObjectMonitor 实现
- C++源码执行过程
- Object 底层实现基于 ObjectMonitor.cpp,所有类默认继承自 Object 类,因此每个对象天生就带着一个 monitor 对象
- 每一个锁住的对象均会与 monitor 进行关联
- objectMonitor.hpp 中的源码片段
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
_header = NULL;
_count = 0; //记录该线程获取锁的次数
_waiters = 0,
recursions = 0; //锁的重入次数
_object = NULL;
_owner = NULL; //指向持有ObjectMonitor对象的线程
_WaitSet = NULL; //存放的处于wait状态的线程队列
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //存放处于等待锁block状态的线程队列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
公平锁与非公平锁
公平锁是指多线程按照申请锁的顺序来获取锁,先来先得
Lock lock = new ReentrantLock(true); //true
表示先来先得
非公平锁是指,获取锁的顺序不是按照申请锁的顺序
存在后申请的线程比先申请的线程优先获取锁
在高并发环境下,存在优先级翻转或者锁饥饿的状态
锁饥饿 : 某个线程长时间得不到锁
Lock lock = new ReentrantLock(false); //false
表示非公平锁,并发抢锁
Lock lock = new ReentrantLock(); //默认非公平锁
为什么会有公平锁和非公平锁的设计?
- 恢复挂起的线程到真正锁的获取还是有时间差的
- 对 CPU 而言时间差较为明细,非公平锁能够更充分利用 CPU 的时间片,减少 CPU 空闲时间
为什么默认使用非公平锁?
- 使用多线程的一个考量点就是线程切换的开销
- 采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,则刚释放锁的线程在此时再次获得同步状态的概率就变得非常大,因此减少了线程的开销
- 采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,则刚释放锁的线程在此时再次获得同步状态的概率就变得非常大,因此减少了线程的开销
什么时候用公平锁?什么时候用非公平锁?
- 为了提高系统的吞吐量,提升性能,减少不必要的时间开销,应选择非公平锁
- 公平锁需要结合具体场景进行讨论
可重入锁,又叫,递归锁
同一个线程在外层方法获取锁的时候,再进入该线程都内层方法会自动获取锁(前提,锁的对象是同一个),不会因为之前已经获取过还没释放而阻塞
- Java 中 synchronized 和 ReentrantLock 都是可重入锁
- 可重入锁可以一定程度上避免死锁
- 可重入锁,即可多次进入同步域==>同步代码块,同步方法,lock()与 unlock()包裹的区域
隐式锁(synchronized 默认是可重入锁)
- 在一个 synchronized 修饰的方法或代码块内部调用本类的其它 synchronized 修饰的方法或代码块时,永远可以得到锁
同步块
private static void reEntryM1() {
final Object object = new Object();
new Thread(() -> {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "\t ----外层调用");
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "\t ----中层调用");
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "\t ----内层调用");
}
}
}
}, "t1").start();
}
- 在 main 方法中进行调用,测试结果
同步方法
public synchronized void m1() {
//指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
System.out.println(Thread.currentThread().getName()+"\t ----come in");
m2();
System.out.println(Thread.currentThread().getName()+"\t ----end m1");
}
public synchronized void m2(){
System.out.println(Thread.currentThread().getName()+"\t ----come in");
m3();
}
public synchronized void m3(){
System.out.println(Thread.currentThread().getName()+"\t ----come in");
}
//main方法调用
public static void main(String[] args){
ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
new Thread(() -> {
reEntryLockDemo.m1();
}, "t1").start();
}
- 效果
synchronized 的可重入原理(基于 objectMonitor.hpp)
- 每个锁对象都拥有一个锁计数器和一个指向持有锁的线程指针
- 当执行 monitorenter 时,如果目标锁的计数器为零,那么没有被其他线程所持有,JVM 会将该锁对象持有的线程设置为当前线程,并将计数器加 1
- 在目标锁对象的计数器不为零的情况下,如果锁的持有线程是但前线程,那么 JVM 可以将其计数器加 1,否则进入等待,直至持有线程释放该锁
- 当执行 monitorexit 时,JVM 将锁对象的计数器减 1,计数器为 0 表示锁已被释放
显示锁(Lock,ReentrantLock)
new Thread(() -> {
lock.lock();
try
{
System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");
lock.lock();
try
{
System.out.println(Thread.currentThread().getName()+"\t ----come in内层调用");
}finally {
lock.unlock();
}
}finally {
// 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
lock.unlock();// 正常情况,加锁几次就要解锁几次
}
},"t1").start();
new Thread(() -> {
lock.lock();
try
{
System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");
}finally {
lock.unlock();
}
},"t2").start();
- 效果
- lock()与 unlock()未一一匹对
- 效果
- t1 外层未释放锁,t2 陷入持续等待…
死锁以及排查
死锁是什么
参考往期文章
编写一个死锁 case
public static void main(String[] args) {
final Object objectA = new Object();
final Object objectB = new Object();
new Thread(() -> {
synchronized (objectA) {
System.out.println(Thread.currentThread().getName() + "\t 自己持有A锁,希望获得B锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectB) {
System.out.println(Thread.currentThread().getName() + "\t 成功获得B锁");
}
}
}, "A").start();
new Thread(() -> {
synchronized (objectB) {
System.out.println(Thread.currentThread().getName() + "\t 自己持有B锁,希望获得A锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectA) {
System.out.println(Thread.currentThread().getName() + "\t 成功获得A锁");
}
}
}, "B").start();
}
死锁的故障排查
jps -l
jstack pid
- jconsole
小结