锁: 对象中的 一个标识
加锁:就是改变这个对象的标识的值
加锁成功:方法正常返回
加锁失败:失败线程死循环,阻塞
synchronized
一、Java头信息
JAVA对象结构包含:对象头、实例对象、对齐填充数据,一个对象的大小必须是8的倍数
对象头:mark word(64bit)、klass point(32bit)

mark word 包含锁信息、hashcode、gc信息等。
klass point 主要指向对象的元数据、她是一个指针;


对象状态:无锁不偏向、无锁可偏向、轻量级锁、重量锁、GC标记 (jvm做的比较好的是 把是否可偏向表示为一个状态,然后根据图中偏向锁的标识再去标识是可偏向的还是不可偏向的);
可偏向标识在无锁的情况下会根据是否计算hashcode而变化、因为如果计算了hashcode之后 对象变的不可偏向;为什么?
mark word 前五十六位 如果有hashcode 则存的是hashcode(对象.hashcode 调用的是一个本地的native方法是一个C++的方法,计算对象所在内存的地址)没有则存放要偏向的线程Id,
锁膨胀的大概过程:偏向锁->轻量级锁->重量级锁
当一把锁第一次被线程持有的时候是偏向锁;如果这个线程再次加锁h还是偏向锁,如果别的线程来加锁(交替执行)膨胀为轻量锁,如果是资源竞争膨胀为重量级锁
synchronized是不是重量级锁:如果同一个线程加锁->偏向锁,交替执行-轻量锁,资源竞争->mutex实现
二、锁膨胀过程(准备知识)
关于偏向锁
CAS:compare and swap比较和交换
CAS操作包含三个操作数-----内存位置(V)、预期值(A)和新值(B)如果内存位置的值与预期 原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况, 它都会在 CAS 指令之前返回该 位置的值;整个过程都是不可打断的,所以CAS是一个原子操作;主要 是利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法;
公平锁/非公平锁
假设主线程加锁创建10个线程,子线程run方法和主线程同一把锁,主线程创建完成后 子线程的执行情况是什么?
synchronized实现的锁,如果有多个线程阻塞,最先阻塞的最后执行;最后阻塞的最先执行
lock实现的同步如果有多个线程阻塞,唤醒的时候是按阻塞顺序唤醒的
注意:公平锁和非公平锁主要体现在 加锁的时候进行抢锁,如果加锁失败,它进入队列(还没有睡眠)这个时候还会进行自旋再次去获取锁,如果失败就睡眠。醒来时候 无论是公平还是不公平 都按次序执行 也就是“一朝排队,永远排队”
ReentrantLock 默认非公平锁,构造参数可控公平还是非公平,队列的 数据结构是双向链表
公平锁:
1.先进入tryAcquire(是否能获取锁,不能则入队),先看是否有对象持有锁 如果state= 0 没人持有 ,他进入hasQueuedPredecessors(判断队列里面有没有人排对),看队列里Node t = head,Node h=tail,队列里是否有对象存在,没有则不需要排队,进入compareAndSetState加锁 ,第一次加锁,连队列都不需要进入初始化,直接加锁,性能比较好
2.如果已经有对象持有锁了 state = 1 ,先判断是否是同一个线程,是否可以进行锁的重入 可重入则state+1 ,不是同一个线程则 进入addWaiter,进入队列(并不等于排队,还没睡眠),然后进入acquireQueued 询问它前面节点是不是head,是的话再次去tryAcquire()尝试获取锁,还不行的话就去 LockSupport.park 去睡眠了
总结:Lock第一次加锁,连队列都不需要进入初始化,直接加锁,性能比较好。如果他们是交替执行 轮流,则永远不会进行初始化队列(交替执行t1合tn一模一样);如果有资源竞争 t2 会初始化队列,如果她前面有head还会进行自旋
非公平锁:
1.上来就进行抢-锁 compareAndSetState(),如果没有拿到锁,进入 acquire中的tryAcquire 再次进行尝试拿锁
2.如果还没拿到锁则进行 公平锁描述的第二步,
总结: 非公平锁他们首先会在lock方法调用加锁的时候去抢锁(公平锁调用lock不会上来就拿锁); * 如果加锁失败 * 则去看为什么失败(是否锁被人持有了),他在判断的时候如果锁没有被人持有 * 非公平锁有会去直接加锁(不会判断是否有人排队),成功则进入同步块;失败则进入队列 * 进入队列后如果前面那个人是head则再次尝试加锁,成功则执行同步代码块,失败则park(真正的排队)

关于队列如何设计和形成
1、AQS类的设计主要属性
private transient volatile Node head; //队首
private transient volatile Node tail;//尾
private volatile int state;//锁状态,加锁成功则为1,重入+1 解锁则为0
2、Node类的设计
public class Node{
volatile Node prev;
volatile Node next;
volatile Thread thread;
}

三、锁膨胀过程
轻量锁 00 偏向锁101 无锁001 第一个 0标识不可偏向 1可偏向
前言:jvm在编译class文件的时候遇到synchronized会在编译前生成了monitor enter和monitor exit 俩个字节码指令,当遇到monitorenter指令的时候,获取当前锁对象的mark word,当前线程栈的栈桢中产生一个lock record(结构:displaced header和ref_obj),他里面的obj_ref指向锁对象,把当前锁对象的mark_word存储到loack Record当中的_displaced_header,判断jvm有没有把偏向禁用,判断偏向是不是自己,判断是否过期,
偏向锁在退出同步块的时候,依然偏向线程Id;
轻量锁在退出同步块的时候,他会把锁的状态置为无锁;
Jvm有偏向延迟(4s),jvm在启动的时候 它自己有很多锁,他这些锁时不需要偏向,不需要膨胀的,所以一开始它禁用了4s,到时间自动恢复,为了避免偏向撤销 减少性能消耗;
偏向锁加锁过程:

- 第一次偏向:
1.当前线程栈的栈桢中产生一个lock record(结构:displaced header和obj_ref ),他里面的obj_ref指向锁对象
2.在内存中创建一个匿名可偏向mark(无锁可偏向的)header,再创建一个偏向自己的mark new header ,通过cas_set_mark判断当前对象头里的mark 是不是和这个header相同,相同则把锁对象头里的mark改
成new header,得到锁也就是把自己线程id赋值给锁对象tid+101
cas失败?
t1 t2同时偏向,并发指向到cas_set _mark,t2先成功被 t2先偏向了,t1失败了 则进入轻量锁的升级模式
- 偏向锁再次加锁:
1.当前线程栈的栈桢中产生一个lock record(结构:displaced header和ref_obj),他里面的obj_ref指向锁对象
2.判断是否支持偏向,是的话,判断当前线程Id和对象头中存的ID是否相同,相同直接获取到锁
轻量锁加锁过程:
-
第一次加锁:
1.新的线程进来后在栈桢中创建lock record,指向锁对象,产生了一个无锁的mark word (displaced)
2.通过cas判断当前对象头mark word和 displaced是不是相同,第一次进来当前对象头还是存放的t1的 线程ID 是偏向锁,cas失败,锁开始膨胀为轻量级锁进入下图中的方法 CALL_VM(), 在下图中的实现方法 void ObjectSynchronizer::fast_enter()中 对t1进行偏向撤销,撤销后再进入slow_enter()执行轻量锁加锁逻辑

- 轻量锁再次加锁
1.轻量锁释放后对象头是无锁的,新的线程进来后在栈桢中创建lock record,指向锁对象,
2.在CPU内存中产生一个无锁的mark word对象,把lock record锁记录中的mark改为这个新的 new mark word 通过cas_set_mark 判断锁对象的中的mark word 和 内存中产的的无锁对象是否相同 相同则把lock record的指针记录到锁对象的markword中 加锁完毕执行完后 从lockrecord恢复锁对象里面的mark word,栈桢清除
批量重偏向
static Thread t2;
static Thread t3;
static Thread t1;
static int loopFlag=38;
public static void main(String[] args) throws InterruptedException {
//101 可偏向 没有线程偏向
//a 没有线程偏向---匿名 101偏向锁
List<A> list = new ArrayList<>();
t1 = new Thread(){
@Override
public void run() {
for(int i=0;i<loopFlag;i++){
A a = new A();
list.add(a);
synchronized (a){
log.debug(i+" "+ClassLayout.parseInstance(a).toPrintableTest(a));
}
}
log.debug("=========================");
LockSupport.unpark(t2);
}
};
t2.join();
t2 = new Thread(){
@Override
public void run() {
LockSupport.park();
for (int i = 0; i <30 ; i++) {
A a = list.get(i);
log.debug(i+" "+ClassLayout.parseInstance(a).toPrintableTest(a));
synchronized (a){
log.debug(i+" "+ClassLayout.parseInstance(a).toPrintableTest(a));
}
log.debug(i+" "+ClassLayout.parseInstance(a).toPrintableTest(a));
}
log.debug("======t3=====================================");
LockSupport.unpark(t3);
}
};
}
例子:t1 对A对象循环加锁放到list中,t2 循环从list取出A对象 加锁 这样会用很大的性能消耗
首先拿出第0个A出来 ,这时A是偏向t1的锁 ,t2要对他进行加锁,要发生一个撤销 升级成轻量锁 ,轻量锁执行后进行解锁,依次类推,当jvm中对同一个类A进行了20次偏向撤销,jvm认为你写的代码有问题 ,所有A类的所有对象都要重新偏向给t2 这叫做批量重偏向
批量撤销
t1~t2 对A进行了20次撤销进行了 发送了批量重 偏向,t2~t3 对A又进行了20次撤销累计40次,不会发生批量重偏向,新建的对象也不会偏向了
当 jvm对一个类进行了40次的撤销时,jvm会对该对象不再偏向

偏向锁是否过期
发生在批量重偏向中,t2第20次加锁,触发epoch过期 ,生成一个偏向当前线程值的mark word ,cas_set_mark 设置对象头
过期需要进行重新偏向
void ObjectSynchronizer::fast_enter(Handler obj,BasicLock* lock,bool attempt_rebias,Traps)

重量锁加锁过程:
锁膨胀成功之后----010 进入enter()
1.cas加锁,有可能在我膨胀之后 别人刚好把锁释放了,我就去加锁,为了不去排对
2.失败,--进行自旋 (2-3次)继续cas加锁 失败 ---进入队列---再次自旋---失败---睡眠

各种自旋还是拿不到锁后 它会new一个 ObjectMonitor, t2就会进入到EntryList进行睡眠 通过ParkEvent.park()阻塞 底层使用的是mutex
本文详细探讨了Java中的锁机制,包括锁的概念、锁的状态变迁、锁膨胀的过程,以及偏向锁、轻量级锁和重量级锁的具体实现原理。特别关注了锁在不同状态下的加锁流程,如偏向锁的首次加锁、再次加锁,轻量锁的加锁过程,以及重量锁的加锁流程。同时,文章还讨论了锁膨胀的条件,批量重偏向和批量撤销的触发机制。
170万+

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



