深入洞察synchronized

1 CAS

1.1 CAS概念

  CAS,即Compare And Swap或Compare And Exchange,意为比较更新。如图:
在这里插入图片描述
  当一个线程要对某个变量a做更新操作时,先读取a的当前值E,通过计算得到结果值V,将V重新赋值给a之前,先要比较a的当前新值N是否和E相等。若不相等,意味着a的值已经被其他线程修改过,则不能将V赋值给a,需要重新读取a的当前值并计算,循环往复,直到N和E相等时,才能将结果值V赋值给变量a。

1.2 CAS的ABA问题

  在上述过程中,当前新值N和前一次读取的值E相等时,对变量a进行更新操作。但是,变量a的值可能已经被另外一个线程修改过并且又改回了原来的值,a的值经历了从A变成B又变成A的过程,而主线程并不知道这个过程,这就是著名的ABA问题。解决这个问题的方法,只需要在a中加入一个版本号,每次修改a的值之后,版本号加1。那么,在修改a的值的时候,只需比较a的版本号是否被更改就可以了。

1.3 CAS在JDK中的应用

  JDK中使用了大量的CAS操作,比如原子类AtomicInteger、jdk1.8中的ConcurrentHashMap和AQS等。AtomicInteger.incrementAndGet()方法:

    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
    
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

  通过以上AtomicInteger.incrementAndGet()的源码可知,其底层实现是CAS操作。compareAndSwapInt方法是一个本地方法,跟踪其C++源码可知,其最终实现是一条汇编指令:lock cmpxchg。

2 Java对象内存布局

2.1 Java对象内存布局

  普通Java对象在内存中由对象头、实例数据、8字节对齐位三部分组成,而数组是由对象头、数组长度、实例数据、8字节对齐位四部分组成。其中对象头又包括Markword和ClassPointer两部分,Markword占据8个字节,ClassPointer类型指针在默认情况下(开启指针压缩-XX:+UseCompressedClassPointers)占4个字节。类型指针指向的是该对象所属的类,而Markword将在下面的内容中详细介绍。

2.2 工具JOL(Java Object Layout)

  首先,需要在pom.xml文件中引入依赖:

        <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.14</version>
        </dependency>

运行以下代码:

    public static void main(String[] args) {
		Object o = new Object();
		String layout = ClassLayout.parseInstance(o).toPrintable();
		System.out.println(layout);
	}

输出:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

以上结果证实了2.1中所述的关于Java对象内存布局的结论,而当给对象o加锁的时候,对象会有什么变化呢?

public static void main(String[] args) {
		Object o = new Object();
		synchronized (o) {
			String layout = ClassLayout.parseInstance(o).toPrintable();
			System.out.println(layout);
		}
	}

输出:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           40 f7 53 03 (01000000 11110111 01010011 00000011) (55834432)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

从上面可以看出,给对象加锁之后,对象头中前8个字节也就是Markword发生了变化。所以,对一个对象进行加锁,实际上就是改变了Markword里面的内容。

2.3 Markword

Markword到底包含了什么信息呢?查看HotSpot源码markOop.hpp:

// Bit-format of an object header (most significant first, big endian layout below):
//
//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

在这里插入图片描述

3 synchronized锁升级

在这里插入图片描述

  1. 新建一个对象o,如果JVM还未开启偏向锁,则该对象为无锁状态的普通对象,如果JVM已开启偏向锁,则该对象为匿名偏向锁状态。
  2. 当一个线程对o进行加锁时,如果o是一个普通对象,则o会进入轻量级锁的状态;如果o是一个匿名偏向对象,则该对象会进入具体的偏向锁状态,其Markword中将会存在线程的指针。
  3. 当o处于偏向锁的状态下,如果有轻度的竞争出现,JVM会撤销o的偏向锁,升级为轻量级锁状态。多个线程通过CAS操作将自身的LockRecord设置到Markword中,设置成功的线程表示获得锁。如果有重度竞争,JVM会撤销o的偏向锁,直接升级为重量级锁。
  4. 当o处于轻量级锁的状态下,如果竞争愈发激烈,对象会从轻量级锁升级为重量级锁。轻量级锁升级为重量级锁的条件为,当线程自旋次数超过10或者自旋线程数超过CPU核数的一半。在JDK1.6之后,加入了自适应自旋,由JVM来控制自旋次数。
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值