本文详细探讨了Java中的锁机制,包括锁的概念、锁的状态变迁、锁膨胀的过程,以及偏向锁、轻量级锁和重量级锁的具体实现原理。特别关注了锁在不同状态下的加锁流程,如偏向锁的首次加锁、再次加锁,轻量锁的加锁过程,以及重量锁的加锁流程。同时,文章还讨论了锁膨胀的条件,批量重偏向和批量撤销的触发机制。

锁: 对象中的 一个标识

加锁:就是改变这个对象的标识的值

加锁成功:方法正常返回

加锁失败:失败线程死循环,阻塞

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值