synchronized锁升级过程及验证


synchronized锁升级过程

其实“锁”本身就是个对象,synchronized这个关键字不是锁,而是在加上synchronized时,仅仅是相当于“加锁”这个操作。

synchronized 是通过锁对象来实现的。因此了解一个对象的布局,对我们理解锁的实现及升级是很有帮助的。

对象布局

image

对象头(Object Header)

在64位JVM上有一个压缩指针选项-XX:+UseCompressedOops,默认是开启的。开启之后 Class Pointer 部分就会压缩为4字节,对象头大小为 12 字节

  • 对象头

    • Mark Word

      • 默认存储对象的HashCode,分代年龄和锁标志位信息。
      • 这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。
      • 它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
      • 包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等。
    • Class Pointer

      • 对象指向它的类元数据的指针;
      • 虚拟机通过这个指针来确定这个对象是哪个类的实例;
    • Length:如果是数组对象,还有一个保存数组长度的空间,占4个字节;

  • 实例数据

    • 对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定;
  • 对齐填充
    Java对象占用空间是8字节对齐的,即所有Java对象占用bytes数必须是8的倍数。

例如,一个包含两个属性的对象:int和byte,这个对象需要占用8+4+1=13个字节,这时就需要加上大小为3字节的padding进行8字节对齐,最终占用大小为16个字节。

Mark Word

image

偏向锁位锁标志位 是锁升级过程中承担重要的角色。

Jol 查看对象信息

我们可以使用 jol 查看一个对象的对象头信息,已达到观测锁升级的过程

//依赖
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

//输出对象信息
ClassLayout layout = ClassLayout.parseInstance(object)
System.out.println(layout.toPrintable());

image

image

普通对象到轻量级锁

因为偏向锁的延迟,创建的对象为普通对象(偏向锁位 0,锁标志位 01),获取锁的时候,无锁(偏向锁位 0,锁标志位 01) 升级为 轻量级锁(偏向锁位 0,锁标志位 00),释放锁之后,对象的锁信息(偏向锁位 0,锁标志位 01)

为什么要延迟4s?

因为JVM虚拟机自己有一些默认的启动线程,里面有好多sync代码,这些代码启动时就肯定会有竞争,如果直接使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。

synchronized (a) 的时候,由 aMark Word 中锁偏向 0,锁标志位 01 知道锁要升级为轻量级锁。java 虚拟机会在当前的线程的栈帧中建立一个锁记录(Lock Record)空间,Lock Record 储存锁对象的 Mark World拷贝和当前锁对象的指针。

java 虚拟机,使用 CAS 将 a 的 Mark Word(62 位) 指向当前线程(main 线程)中 Lock Record 指针,CAS 操作成功,将 a 的锁标志位变为 00,升级为轻量级锁。

轻量级锁解锁,就是将 Lock Record 中的 a 的 mark word 拷贝,通过 CAS 替换 a 对象头中的 mark word ,替换成功解锁顺利完成。

import org.openjdk.jol.info.ClassLayout;

/**
 * <P><B>Description: </B> 普通对象升级到轻量级锁  </P>
 */
public class Snychronized1 {
    public static class A {
    }
    public static void main(String[] args) throws Exception {
        A a = new A();
        ClassLayout layout = ClassLayout.parseInstance(a);
        System.out.println("**** 对象创建,没有经过锁竞争");
        System.out.println(layout.toPrintable());
        synchronized (a) {
            System.out.println("**** 获取到锁");
            System.out.println(layout.toPrintable());
        }
        System.out.println("**** 锁释放");
        System.out.println(layout.toPrintable());
    }


}

偏向锁

偏向锁是比轻量级锁更轻量的锁。轻量级锁,每次获取锁的时候,都会使用 CAS 判断是否可以加锁,不管有没有别的线程竞争。

当线程要进入synchronized修饰的方法或代码块时,jvm会判断对象头中的MarkWord中有没有偏向锁指向当前线程ID,如果有,若此时无其他线程竞争,保持偏向锁状态。当该线程重复进入方法或代码块时(重入),直接在MarkWord中判断有没有偏向锁指向它的线程ID,就不用通过 CAS 操作获取偏向锁了。

当有其他线程加入竞争后,线程会暂停检查,若果该线程执行完了,则撤销锁,其他线程占有该锁,如果该线程还未执行完还需要该锁,则将锁升级为轻量级锁。


import org.openjdk.jol.info.ClassLayout;

import java.util.concurrent.TimeUnit;

/**
 * <P><B>Description: </B> 偏向锁  </P>
 */

// 查看偏向锁配置的默认参数  -XX:+PrintFlagsInitial | grep -i biased
// BiasedLocking
public class Snychronized2 {
    public static class A {

    }

    public static void main(String[] args) throws Exception {
        //因为偏向锁加锁机制延迟4秒启动,所以我们这里阻塞6s再创建对象。
        TimeUnit.SECONDS.sleep(6);
        final A a = new A();
        ClassLayout layout = ClassLayout.parseInstance(a);
        System.out.println("**** 创建对象,对象获得偏向锁");
        System.out.println(layout.toPrintable());

        synchronized (a) {
            System.out.println("**** 没有其他线程竞争,依旧保持偏向锁");
            System.out.println(layout.toPrintable());
        }

        System.out.println("**** 解锁后,对象还是持有偏向锁");
        System.out.println(layout.toPrintable());

    }


}

轻量级锁

就是偏向锁升级来的。该线程还未执行完,继续占有资源,其他线程等待,这是其他线程就会自旋,等待资源释放。若自旋解锁失败,锁升级为重量级锁。

重量级锁

验证 偏向锁,轻量级锁,重量级锁的逐渐升级过程。

/**
 * <P><B>Description: </B> 偏向锁,轻量级锁,重量级锁的逐渐升级  </P>
 */
public class Snychronized3 {
    public static void main(String[] args) throws Exception {
        // 延迟六秒执行例子,创建的 a 为可偏向对象
        TimeUnit.SECONDS.sleep(6);
        final A a = new A();
        ClassLayout layout = ClassLayout.parseInstance(a);
        System.out.println("**** 查看初始化 a 的对象头");
        System.out.println(layout.toPrintable());
        // 这里模拟获取锁,当前获取到的锁为 偏向锁
        Thread t = new Thread(() -> {
            synchronized (a) {
            }
        });
        t.start();
        // 阻塞等待获取 t 线程完成
        t.join();
        System.out.println("**** t 线程获得锁之后");
        System.out.println(layout.toPrintable());

        final Thread t2 = new Thread(() -> {
            synchronized (a) {
                // a 的存在两个想成竞争锁,偏向锁升级为轻量级锁
                System.out.println("**** t2 第二次获取锁");
                System.out.println(layout.toPrintable());
                try {
                    //阻塞3秒,模拟任务执行
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 开启 t3 线程模拟竞争,t3 会自旋获得锁,由于 t2 阻塞了 3 秒,t3 自旋是得不到锁的,锁升级为重量级锁
        final Thread t3 = new Thread(() -> {
            synchronized (a) {
                System.out.println("**** t3 不停获取锁");
                System.out.println(layout.toPrintable());
            }
        });
        t2.start();
        // 为了保证 t2 先获得锁,这里阻塞 10ms ,先开启t2线程,再开启 t3 线程
        TimeUnit.MILLISECONDS.sleep(10);
        t3.start();
        t2.join();t3.join();

        // 验证 gc 可以使锁降级
        System.gc();
        System.out.println("**** After System.gc()");
        System.out.println(layout.toPrintable());
    }
    public static class A {}
}

t2 线程持有锁 a轻量级锁 的时候,t3 也在获得 a 的 轻量级锁CAS 修改 a 的 Mark Word 为 t3 所有失败。导致了锁升级为重量级锁,设置 a 的锁标志位为 10,并且将 Mark Word 指针指向一个 monitor对象,并将当前线程阻塞,将当前线程放入到 _EntryList 队列中。当 t2 执行完之后,它解锁的时候发现当前锁已经升级为重量级锁,释放锁的时候,会唤醒 _EntryList 的线程,让它们去抢 a 锁。

自旋,底层其实调用的是native方法,涉及到汇编相关的问题,说白了就是为了保持线程不进入睡眠状态,让cpu做无用功。其实自旋最大的一个作用就是避免了线程在用户态和内核态之间切换。减少cpu资源的调度消耗,但是也不能一直自旋,不然另一个线程一直占用着锁,而你在这一直自旋消耗cpu资源,导致cpu占用率一路飙升也不行,所以jvm有设置最大自旋次数,10次。

到底什么时候锁会降级呢?

正常情况下,是不会发生锁降级的,锁降级一般只会发生在GC的时候,GC的时候,对象都即将被回收,没用了,所以说锁降级没什么太大的意义。

jdk1.6 其他优化:

锁消除:JIT编译时,检测到共享数据区存在不可能出现竞争情况,就会进行锁消除。例如同步方法内的局部变量,不可能被其他线程使用,就会进行锁消除

虚拟机默认开启了锁消除 -XX:-EliminateLocks 关闭锁消除

锁粗化:把多次锁请求合并成一个锁请求,降低性能消耗。

/**
 * <P><B>Description: </B> 锁消除,锁粗化  </P>
 */
public class SynchronizedTest {


/*    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }*/

// 从源码中可以看出,append方法用了synchronized关键词,它是线程安全的。
// 但我们可能仅在线程内部把StringBuffer当作局部变量使用,
// 这时候,编译器就会判断出sb这个对象并不会被这段代码块以外的地方访问到,
// 更不会被其他线程访问到,这时候的加锁就是完全没必要的,编译器就会把这里的加锁代码消除掉,
// 体现到java源码上就是把append方法的synchronized修饰符给去掉了。


    public static String getString(String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }


    //锁消除测试
    public static void test1() {

        long tsStart = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            getString("TestLockEliminate ", "Suffix");
        }
        System.out.println("一共耗费:" + (System.currentTimeMillis() - tsStart) + " ms");
    }


    //锁粗化测试
    public static void test2() {
        long tsStart = System.currentTimeMillis();
        Object object = new Object();
         synchronized (object) {
        for (int i = 0; i < 100000000; i++) {
           // synchronized (object) {
                object.hashCode();

            }
        }
        System.out.println("一共耗费:" + (System.currentTimeMillis() - tsStart) + " ms");
    }

    public static void main(String[] args) {
        //锁消除测试
        //   -XX:-EliminateLocks 关闭锁消除
        //test1();

        //锁粗化测试
        test2();

    }
}


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM