signed

QiShunwang

“诚信为本、客户至上”

synchronized原理和锁膨胀过程

2021/4/26 22:55:23   来源:

对象头

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机的对象头(Object Header)包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。

下面介绍对象头:

  • 普通对象头:Mark Word + Kclass Word

  • 数组对象头:Mark Word + Kclass Word + 数组长度
    解释关键名称

  • Mark Word:存储运行时的数据,如hash、分代年龄age、是否为可偏向状态、锁标志位

  • Kclass Word(Klass Pointer):指向方法区的类元数据的指针,虚拟机通过这个来确定这个对象是哪个类

64位系统

锁状态56bit1bit4bit1bit2bit
54bit2bit是否偏向锁锁标志位
无锁态对象hashcode0分代年龄001
偏向锁Thread IdEpoch0分代年龄101
特殊的可偏向锁101
轻量级锁指向栈中锁的记录指针00
重量级锁指向互斥量(Monitor初始物理地址)的指针10
GC11

对象头的参数说明

参数说明
unused未使用位,可以说是预留位
identity_hashcode对象标识hash码,采用延迟加载技术。当对象使用HashCode()计算后,并会将结果写到该对象头中
age分代年龄,固定4位 
biased_lock是否开启偏向状态(0--关闭,1--开启),固定1位
lock锁标志位(01--无锁或者偏向锁,00--轻量级锁,10--重量级锁),固定2位
thread持有偏向锁的线程ID和其他信息。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。 偏向锁才有,固定54位
epoch偏向时间戳。偏向锁才有,固定占2位
ptr_to_lock_record指向栈中锁记录的指针。轻量级锁才有,固定62位
ptr_to_heavyweight_monitor指向线程Monitor的指针。重量级锁才有,固定62位

偏向锁状态

  • 匿名偏向(Anonymously biased)
    在此状态下thread_ptr为NULL(0),意味着还没有线程偏向于这个锁对象。第一个试图获取该锁的线程将会面临这个情况,使用原子CAS指令可将该锁对象绑定于当前线程。这是允许偏向锁的类对象的初始状态。
  • 可重偏向(Rebiasable)
    在此状态下,偏向锁的epoch字段是无效的(与锁对象对应的klass的mark_prototype的epoch值不匹配)。下一个试图获取锁对象的线程将会面临这个情况,使用原子CAS指令可将该锁对象绑定于当前线程。在批量重偏向的操作中,未被持有的锁对象都被至于这个状态,以便允许被快速重偏向。
  • 已偏向(Biased)
    这种状态下,thread ptr非空,且epoch为有效值——意味着其他线程正在只有这个锁对象。
  • 批量撤销
    当class(类)偏向撤销超过40次,会触发批量撤销,禁用偏向
  • 批量重偏向
    当一个Class(类),被单独撤销偏向20次时,将会触发批量重偏向

轻量级锁获取锁的过程

获取锁过程设计到的知识点:

  • Lock Record
    线程的栈中开辟的锁记录空间
  • owner
    对象头的指针
  • Displaced Mark Word
    用于储存锁对象目前的Mark Word的拷贝(官方把这份拷贝加了个Displaced前缀,即Displaced Mark Word)。
  1. 在线程进入同步方法、同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为"01"状态,是否为偏向锁为"0"),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Recored)的空间,用于储存锁对象目前的Mark Word的拷贝(官方把这份拷贝加了个Displaced前缀,即Displaced Mark Word)。
    在这里插入图片描述

  2. 将对象头的Mark Word拷贝到线程的锁记录(Lock Recored)中。

  3. 拷贝成功后,虚拟机将使用CAS操作(如果对象的Mark Word与当代女线程栈中的Displaced Mark Word一致则认为CAS成功)尝试将对象的Mark Word更新为指向Lock Record的指针。如果这个更新成功了,则执行步骤4,否则执行步骤5

  4. 更新成功,这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位将转变为"00",即表示此对象处于轻量级锁的状态。
    在这里插入图片描述

  5. 更新失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,可以直接进入同步块继续执行,否则说明这个锁对象已经被其其它线程抢占了。进行自旋执行步骤3,如果自旋结束仍然没有获得锁,轻量级锁就需要膨胀为重量级锁,锁标志位状态值变为"10",Mark Word中储存就是指向monitor对象的指针,当前线程以及后面等待锁的线程也要进入阻塞状态。
    在这里插入图片描述

释放锁的过程

  1. 使用CAS操作将对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来(依据Mark Word中锁记录指针是否还指向本线程的锁记录),如果替换成功,则执行步骤2,否则执行步骤3
  2. 如果替换成功,整个同步过程就完成了,恢复到无锁的状态(01)。
  3. 如果替换失败,说明有其他线程尝试获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

无锁分析

如果是jdk1.6及之后默认开启的话,答案1:JVM开启4s前创建的对象是无锁状态的,此时这些对象只可以升级为轻量级锁和重量级锁(不能转为偏向锁);答案2:4s之后创建的对象默认是无锁可偏向的,可以理解成特殊的无锁状态,这个特殊的无锁状态只可以转为偏向锁,并且只有获得了偏向锁后,进行撤销偏向锁或者直接升级为轻量级锁,后面还可以升级为重量级锁。

偏向锁

偏向锁作用:就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。

获取锁的过程

  1. 检查Mark Word是否为可偏向锁的状态,即是否偏向锁即为1即表示支持可偏向锁,否则为0表示不支持可偏向锁。
  2. 如果是可偏向锁,则检查Mark Word储存的线程ID是否为当前线程ID,如果是则执行同步块,否则执行步骤3
  3. 如果检查到Mark WordID不是本线程的ID,则通过CAS操作去修改线程ID修改成本线程的ID,如果修改成功则执行同步代码块,否则执行步骤4
  4. 当拥有该锁的线程到达安全点之后,挂起这个线程,升级为轻量级锁。

锁释放的过程

  1. 有其他线程来获取这个锁,偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。
  2. 等待全局安全点(在这个是时间点上没有字节码正在执行)。
  3. 暂停拥有偏向锁的线程,检查持有偏向锁的线程是否活着,如果不处于活动状态,则将对象头设置为无锁状态,否则设置为被锁定状态。如果锁对象处于无锁状态,则恢复到无锁状态(01),以允许其他线程竞争,如果锁对象处于锁定状态,则挂起持有偏向锁的线程,并将对象头Mark Word的锁记录指针改成当前线程的锁记录,锁升级为轻量级锁状态(00)

实战代码

//==================偏向锁实战start=====================
    //四秒内创建的对象为:无锁不可偏向,即001
    public static void testFourSeconds() {
        A a = new A();
        System.out.println(ClassLayout.parseInstance(a).toPrintable());//打印对象头的最初状态
    }

    //代码测试一:让主线程睡眠四秒以上,四秒内创建的对象为:无锁可偏向,即101
    public static void testMoreFourSeconds() throws InterruptedException {
        Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
        A a = new A();
        System.out.println(ClassLayout.parseInstance(a).toPrintable());//打印对象头的最初状态
    }

    //代码测试二:不让主线程睡眠,无锁不可偏向001,然后再对其进行加锁----升级为轻量锁000
    public static void test1() throws InterruptedException {
        //Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
        A a = new A();
        System.out.println(ClassLayout.parseInstance(a).toPrintable());//打印对象头的最初状态

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println("当前线程拿到锁,升级为轻量锁");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                }
            }
        };

        thread1.start();
    }

    /**
     * 代码测试三:对象调用hashCode方法后,将会进行撤销偏向锁操作,变成无锁不可偏向状态,之后也不能使用偏向锁。
     * 调用hashCode后将撤销偏向锁,并将hashCode值存进对象头。
     * @throws InterruptedException
     */
    public static void testHashCode() throws InterruptedException {
        Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
        A a = new A();
//        System.out.println(ClassLayout.parseInstance(a).toPrintable());//打印对象头的最初状态

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println("当前线程拿到锁,升级为偏向锁");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                }
            }
        };
        thread1.start();
        Thread.sleep(5000);//睡眠,让线程打印完对象头
        System.out.println(a.hashCode());
        System.out.println(Integer.toHexString(a.hashCode()));
        System.out.println("对象A调用hashCode方法后的对象头状态");
        System.out.println(ClassLayout.parseInstance(a).toPrintable());//打印对象头信息
    }

    /**
     * 代码测试四:3、偏向的线程不存活,开启了重偏向,将会将对象头设置成无锁可偏向的状态,然后重偏向线程,拿到偏向锁。
     * 代码说明:线程1启动拿到偏向锁,给主线程个睡眠时间,等线程1结束了后再启动线程2,线程2拿的是可重偏向,就是偏向锁重新偏向了线程2。测试上述可能
     */
    public static void test4() throws InterruptedException {
        Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
        A a = new A();

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println(Thread.currentThread());
                    System.out.println("当前线程拿到锁,升级为偏向锁");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                }
            }
        };

        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println(Thread.currentThread());
                    System.out.println("测试可重偏向");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                }
            }
        };
        thread1.setName("my is thread1");
        thread2.setName("my is thread2");
        thread1.start();
        thread1.join();
        Thread.sleep(6000);

        thread2.start();
    }
    //==================偏向锁实战end=====================

轻量锁

//==================轻量锁start=====================

    /**
     * JVM启动4s前创建的对象为无锁不可偏向状态,当第一个线程拿锁时,就直接升级为轻量级锁
     * @throws InterruptedException
     */
    public static void lightWeightTest1() throws InterruptedException {
        A a = new A();
        System.out.println(ClassLayout.parseInstance(a).toPrintable());//打印对象头的最初状态

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println("当前线程拿到锁,升级为轻量锁");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                }
            }
        };

        thread1.start();
    }

    /**
     * 偏向的线程1存活,不在执行同步代码,线程2进来拿锁直接升级为轻量级锁
     * @throws InterruptedException
     */
    public static void lightWeightTest2() throws InterruptedException {
        Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
        A a = new A();

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println(Thread.currentThread());
                    System.out.println("当前线程拿到锁,升级为偏向锁");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                }
            }
        };

        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println(Thread.currentThread());
                    System.out.println("thread2拿锁,此时偏向锁thread1存活不再执行同步代码块,锁升级成轻量级锁");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                }
            }
        };
        thread1.setName("my is thread1");
        thread2.setName("my is thread2");
        thread1.start();
        thread1.join();

        thread2.start();
    }

    /**
     * 代码测试三:偏向的线程1存活,且在执行同步代码,线程2进来拿锁竞争升级成轻量锁升级重量级锁
     */
    public static void lightWeightTest3() throws InterruptedException {
        Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
        A a = new A();

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println(Thread.currentThread());
                    System.out.println("当前线程拿到锁,升级为偏向锁");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                    try {
                        Thread.sleep(7000); //让线程存过且保持在代码块
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println(Thread.currentThread());
                    System.out.println("thread2拿锁,产生资源竞争,导致thread1升级为轻量级锁,线程2自旋拿锁超时,升级为重量级锁");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                }
            }
        };
        thread1.setName("my is thread1");
        thread2.setName("my is thread2");
        thread1.start();
        Thread.sleep(7000);

        thread2.start();
    }

    /**
     * 代码测试四:当拿到轻量级锁的线程2执行完毕,不存活的时候就会执行解锁过程
     */

    public static void lightWeightTest4() throws InterruptedException {
        Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
        A a = new A();

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println(Thread.currentThread());
                    System.out.println("当前线程拿到锁,升级为偏向锁");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                }
            }
        };

        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println(Thread.currentThread());
                    System.out.println("thread2拿到锁,thread1存活,升级为轻量级锁");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                }
            }
        };
        thread1.setName("my is thread1");
        thread2.setName("my is thread2");
        thread1.start();
        thread1.join();

        thread2.start();

        Thread.sleep(6000);//休眠让线程thread2结束运行,轻量级锁解锁为无锁不可偏向状态
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
    }
    //==================轻量锁end=====================

重量锁

//==================重量级锁测试start=====================
    /**
     * 代码测试一:两个线程资源竞争,直接升级为重量级锁。
     * 线程1在执行时,是拿到了偏向锁,但是输出对象头信息大概需要3s中,就是同步代码需要执行时间比较长,所以当线程2来拿锁时,
     * 线程1还在执行,就造成了锁的竞争,直接升级轻量级锁,升级重量级锁的过程。
     */
    public static void weightTest1() throws InterruptedException {
        Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
        A a = new A();

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println(Thread.currentThread());
                    System.out.println("当前线程拿到锁,升级为偏向锁");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                }
            }
        };

        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println(Thread.currentThread());
                    System.out.println("thread2拿到锁,thread1存活,升级为轻量级锁");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                }
            }
        };
        thread1.setName("my is thread1");
        thread2.setName("my is thread2");
        thread1.start();
        thread2.start();

    }

    /**
     * 代码测试二:当线程执行完毕,不存活后,重量级锁解锁成无锁不可偏向状态(解锁后拿锁又是轻量级、重量那些操作)
     */
    public static void weightTest2() throws InterruptedException {
        Thread.sleep(4500);//休眠4.5秒,保证偏向锁开启
        A a = new A();

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println(Thread.currentThread());
                    System.out.println("当前线程拿到锁,产生资源竞争,直接升级为重量级锁");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                }
            }
        };

        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (a) {
                    System.out.println(Thread.currentThread());
                    System.out.println("当前线程拿到锁,产生资源竞争,直接升级为重量级锁");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                }
            }
        };
        thread1.setName("my is thread1");
        thread2.setName("my is thread2");
        thread1.start();
        thread2.start();
        Thread.sleep(6000);
        System.out.println("重量级锁解锁,线程不存活后,解锁成无锁不可偏向状态");
        System.out.println(ClassLayout.parseInstance(a).toPrintable());

    }
    //==================重量级锁测试end=====================

Monitor

它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

Contention List: 竞争队列,所有请求锁的线程首先被放在这个竞争队列中
Entry List: Contention List中那些有资格成为候选资源的线程被移动到Entry List中
Wait Set: 哪些调用wait方法被阻塞的线程被放置在这里
OnDeck: 任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck
Owner: 当前已经获取到所资源的线程被称为Owner

过程:
JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。

OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。

处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。

Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck线程的锁资源。

synchronized锁的升级过程

在这里插入图片描述

三种锁的优缺点比较

在这里插入图片描述