synchronized的进化过程
jdk1.6 synchronized就是单纯是个重量级锁,后来通过加入了偏向锁等等四种锁状态,使得synchronized性能大大提升。引入了锁消除,即,通过编译阶段对上下文的扫描,删除不必要的加锁操作。
注意事项
【1】一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;
【2】每个实例都对应有自己的一把锁(this),不同实例之间互不影响;
例外:锁对象是*.class以及 synchronized修饰的是 static方法的时候,所有对象公用同一把锁;
【3】synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁;
锁消除
编译器认为不会发生线程安全问题,就无视了你的锁,不执行monitor相关操作。
锁粗化
比如执行插入数据商品时,是对店铺加锁。那么批量执行的时候,只需要加一次锁。而不是每插入一次就加/释放一次锁。
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
// 线程安全的buffer类,append会加锁,但显然这是可以锁粗话的,会优化成只获得/释放一次锁
synchronized锁状态
一个对象其实有四种锁状态,级别由低到高:
- 无锁状态
- 偏向锁状态
- 轻量级锁状态
- 重量级锁状态
这是对于synchronized来说的,所以它底层是偏向锁+CAS(偏向锁/轻量级锁)+wait/notify(重量级锁)实现的
锁升级容易,降级很难
Java对象头:非数组类型,则用2个字宽来存储对象头,如果是数组,则会用3个字宽来存储对象头。在32位处理器中,一个字宽是32位;在64位虚拟机中,一个字宽是64位。其中mark word 存储对象的hashCode或锁信息等。Mark Word如下:
| 锁状态 | 29 bit 或 61 bit | 1 bit 是否是偏向锁? | 2 bit 锁标志位 |
|---|---|---|---|
| 无锁 | 0 | 01 | |
| 偏向锁 | 线程ID | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 此时这一位不用于标识偏向锁 | 00 |
| 重量级锁 | 指向互斥量(重量级锁)的指针 | 此时这一位不用于标识偏向锁 | 10 |
| GC标记 | 此时这一位不用于标识偏向锁 | 11 |
偏向锁
大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。于是有了偏向锁。
偏向于第一个访问锁的线程。偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不做了,提高了程序的运行性能。
具体原理:第一次拿到锁,给锁加上自己的线程ID。再次访问,锁ID==自己ID,不做CAS。否则尝试使用CAS来替换锁的线程ID变量为我的ID
- 成功,表示之前的线程不存在了,仍然为偏向锁
- 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁。
CAS
Compare and Swap
比较并交换。用于在硬件层面上提供原子性操作。在 Intel 处理器中,CAS通过指令cmpxchg实现。 比较是否和给定的数值一致,如果一致则修改,不一致则不修改。
CAS有如下三个变量
- V:要更新的变量(var)
- E:预期值(expected) 就是旧值
- N:新值(new)
CAS操作:判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。
当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
CAS实现原子操作三大问题
ABA问题
A->B->A,CAS检查不出变化。加上版本号或者时间戳。
循环时间长开销大
如果自旋CAS长时间不成功,会占用大量的CPU资源。
让JVM支持处理器提供的pause指令
只能保证一个共享变量的原子操作
锁的升级流程
每一个线程在准备获取共享资源时:
- 第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。
- 第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。
- 第三步,两个线程都把锁对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作, 把锁对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord。
- 第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋 。
- 第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则依然处于轻量级锁的状态,如果自旋失败(默认自旋阈值为10次,也就是CAS10次还没拿到锁,就是自旋失败)
- 第六步,自旋失败,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。
具体实现可以看这篇文章,以及各种锁的优缺点对比 好文
死锁
死锁只有同时满足以下四个条件才会发生:
- 互斥条件;
- 持有并等待条件;
- 不可剥夺条件;
- 环路等待条件;
文章详细介绍了Java中synchronized的优化过程,包括锁消除、锁粗化和四种锁状态(无锁、偏向锁、轻量级锁、重量级锁),强调了锁升级的优化策略。同时提到了CAS(CompareandSwap)在锁机制中的作用,以及死锁的四个必要条件。

7910

被折叠的 条评论
为什么被折叠?



