synchronized锁的优化过程

    在java并发中synchronized一直是一个重要的角色,有人称它为重量级锁,但在jdk1.6之后synchronized得到了优化,引入了偏向锁和轻量级锁,避免线程上下文切换带来的耗时,所以看起来就没有那么重了。

对象的组成

    因synchronized锁信息都是保存在对象头部中,故而先从对象头入手。

    对象的组成:

  • 头部信息(头部信息分为两块Mark Word(如图)与类型指针(java虚拟机默认开启类型指针的压缩(4) )
  • 实例数据(各种类型的变量)
  • 对其填充(起到占位符的作用,主要是因为HotSpot虚拟机的内存管理要求对象的大小必须是8字节的整数倍,而对象头正好是8个字节的整数倍,但是实例数据不一定,所以需要对齐填充补全。)
    在这里插入图片描述
        由图上可知,Mard Word中保存着锁的信息、分代年龄、hashCode等;Biased是偏向锁,标志位为101,54位thread用力存储线程的id;Lightweight Locked是轻量级锁,标志位为00,ptr_to_lock_record用来存储指向锁记录的指针;Heavyweight Locked是重量级锁,标志位为10,ptr_to_heavyweight_monitor用来存储指向锁记录的指针。
CAS

    在优化synchronized过程中大量使用到CAS操作。CAS全称(Compare And Set),CAS是一种乐观锁操作并且包含三个操作数:内存位置(V)、原值(A)、新值(B)。当条件有且只能满足V与A相等时,才会将B赋值给A。

    AtomicInteger当中常用的自增方法 incrementAndGet:

public final int incrementAndGet() {
	for (;;) {
		int current = get();
		int next = current + 1;
		if (compareAndSet(current, next)) return next;
	}
}
private volatile int value;
public final int get() {
	return value;
}

    CAS的自旋。循环体当中做了三件事:

  1. 获取当前值。
  2. 当前值+1,计算出目标值。
  3. 进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤。
轻量级锁

    一个对象虽然有多个线程加锁,但是加锁的时间是错开的(也就是没有锁竞争),那么会使用轻量级锁来优化,轻量级锁是透明的,语法仍然是synchronized

static final Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
	// 同步块 A
	method2();
	}
}
public static void method2() {
	synchronized( obj ) {
	// 同步块 B
	}
}

在这里插入图片描述
    结果如图mark word信息000000000370f0f0转为二进制11011100001111000011110000为轻量级锁:
在这里插入图片描述

轻量级锁执行步骤
  • 当前线程创建锁记录(包含锁记录地址和锁的标志位)
  • 尝试cas原子操作,把锁记录信息存入对象头的mark word中,并将mark word中的信息存入当前线程的锁记录中。
  • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁
  • 如果 cas 失败,有两种情况;1.如果是其它线程已经持有了该 对象的轻量级锁,这时表明有竞争,进入锁膨胀过程升级为重量级锁;2.如果是自己执行了 synchronized 锁重入,那么再添加一条 锁记录作为重入的计数。
  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一,使用cas将锁记录中的信息恢复到mark word中。
锁膨胀

    如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

锁膨胀过程
  • 当前线程进行轻量级加锁时,别的线程已经对该对象加了轻量级锁。
  • 当前线程加锁失败,进入膨胀流程,即当前对象申请Moniter锁,当前加锁对象指针指向重量级锁地址。
  • 当别的线程退出同步块时,使用cas操作将获取的mark word值恢复到mark word中失败,这时会进入重量解锁流程,按照Moniter地址找到Moniter对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。
自旋锁

    重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

    自旋成功情况:

线程 1 (core 1 上)对象 Mark Word线程 2 (core 2 上)
-10(重量锁)-
访问同步块,获取 monitor10(重量锁)-
成功(加锁)10(重量锁)-
执行同步块10(重量锁)-
执行同步块10(重量锁)访问同步块,获取 monitor
执行同步块10(重量锁)自旋重试
执行完毕10(重量锁)自旋重试
成功(解锁)01(无锁)自旋重试
-10(重量锁)成功(加锁)
-10(重量锁)执行同步块

    自旋失败情况:

线程 1 (core 1 上)对象 Mark Word线程 2 (core 2 上)
-10(重量锁)-
访问同步块,获取 monitor10(重量锁)-
成功(加锁)10(重量锁)-
执行同步块10(重量锁)-
执行同步块10(重量锁)访问同步块,获取 monitor
执行同步块10(重量锁)自旋重试
执行同步块10(重量锁)自旋重试
执行同步块10(重量锁)自旋重试
执行同步块10(重量锁)阻塞
执行同步块10(重量锁)阻塞
偏向锁

    轻量级锁在没有竞争时(就当前自己这把锁),为了避免每次获取锁都需要多次执行cas原子指令操作,jdk1.6之后引入了偏向锁。

偏向锁执行步骤:

  • 首先判断mark word是否是偏向状态,是否偏向为1,锁标志位为01。
  • 进入偏向状态后,判断当前线程id与mark word中线程id是f否相等,相等则为偏向锁
  • 若线程id不等,则尝试采用cas操作改变 mark word中线程id,成功则为偏向锁
  • 若 失败,说明当前线程存在锁竞争关系,则转换为轻量级锁。
public static void main(String[] args) {

     Object o=new Object();
     //o.hashCode();
     new Thread(()->{
         System.out.println("synchronized前..");
         System.out.println(ClassLayout.parseInstance(o).toPrintable());
         synchronized (o){
             System.out.println("synchronized中..");
             System.out.println(ClassLayout.parseInstance(o).toPrintable());
         }
         System.out.println("synchronized后..");
         System.out.println(ClassLayout.parseInstance(o).toPrintable());
     }).start();
 }

    偏向锁默认是延迟的,程序启动不会立即生效,启用参数-XX:BiasedLockingStartupDelay=0禁用延迟,运行如上代码结果如图:
在这里插入图片描述

  • synchronized前,使用JOL查看对象的内存布局,对象头信息(0000000000000005)转成二进制是101可知,对象默认开启的是偏向锁(是否偏向1,标志位为01)。
  • synchronized中,头部信息发生了改变000000001b0ad805,转成二进制为11011000010101101100000000101,依然是偏向锁,但是多了当前线程的id(1101100001010110110)(id为54位,0补全)。
  • synchronized后,释放锁,但是线程id依然存放在mark word中。
撤销偏向锁(hashCode)

    如上偏向锁程序打开注释调用Object的hashCode()方法,代码就不贴出了,直接查看执行结果:
在这里插入图片描述

  • synchronized前,将mark word中0x00000053e25b7601信息转成二进制为101001111100010010110110111011000000001,根据后三位可知是无锁状态。
  • synchronized中,mark word信息000000001c91f460转成二进制11100100100011111010001100000,根据后三位可知是轻量级锁。
  • synchronized后,释放锁,轻量级锁转变为无锁状态。

    调用Object的hashCode()方法会撤销偏向锁,因hashCode值wark word中为31位,线程id是54位,存储空间不够,所以会转为轻量级锁,将hashCode值存入当前线程的锁记录中。

撤销偏向锁(其它线程使用对象)
private static Thread a,b;
 public static void main(String[] args) {
     Object o=new Object();
     a = new Thread(() -> {
         synchronized (o) {
             System.out.println("a线程:" + ClassLayout.parseInstance(o).toPrintable());
         }
         LockSupport.unpark(b);//唤醒线程
     }, "a");

     a.start();
     b = new Thread(() -> {
         LockSupport.park();//阻塞线程
         System.out.println("synchronized前..");
         System.out.println(ClassLayout.parseInstance(o).toPrintable());
         synchronized (o) {
             System.out.println("synchronized中..");
             System.out.println(ClassLayout.parseInstance(o).toPrintable());
         }
         System.out.println("synchronized后..");
         System.out.println(ClassLayout.parseInstance(o).toPrintable());
     }, "b");
     b.start();
 }

    执行结果如图,从偏向锁转为了轻量级锁。
在这里插入图片描述

批量重偏向:

    启动程序设置jvm参数-XX:+PrintFlagsFinal

  • intx BiasedLockingBulkRebiasThreshold = 20 默认偏向锁批量重偏向阈值
  • intx BiasedLockingBulkRevokeThreshold = 40 默认偏向锁批量撤销阈值
    在这里插入图片描述
public static void main(String[] args) throws InterruptedException {
    Thread.sleep(3000);

    List<Object> list = new ArrayList<>();
    Thread a = new Thread(() -> {
        for (int i = 0; i < 30; i++) {
            Object o = new Object();
            list.add(o);
            synchronized (o) {

                System.out.println("a"+i + "\t" + ClassLayout.parseInstance(o).toPrintable());
            }

        }
        synchronized (list){
            list.notify();
        }
    }, "a");

    a.start();

    Thread b = new Thread(() -> {

        synchronized (list){
            try {
                list.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        for (int i = 0; i < 30; i++) {
            Object o = list.get(i);

            synchronized (o) {
                if( i==19 || i==20) {
                    System.out.println("b" + i + "\t" + ClassLayout.parseInstance(o).toPrintable());
                }
            }

        }
    }, "b");

    b.start();
    //b.join();
   	//System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
}

    结果如图,当阈值满足于20,当前线程b进入偏向状态:
在这里插入图片描述

批量撤销

    当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。

    如上代码注释拿掉运行,epoch不等于0,表示新创建的对象不可偏向。
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值