你真的了解synchronized和volatile吗?

Posted 三不猴

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你真的了解synchronized和volatile吗?相关的知识,希望对你有一定的参考价值。

原文来自于:公众号三不猴子

什么是cas?

cas:compare and swap 比较然后交换,它在没有锁的状态下可以保证多线程的对值得更新。我们可以看一下在jdk中对cas的应用:

/**
 * 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;
}

在Atomic原子类中的自增操作中就使用到了compareAndSwapInt,这里的cas的实现使用的native方法。用一张流程图来理解什么是cas。

我们先会存一下要修改的值,再修改之后再去看一下要修改的值是不是还是我们存的值如果是一致的则修改,我们在更新数据常用的乐观锁就是用的cas的机制。

在这里面有个ABA的问题:所谓ABA就是在线程A存了值之后,有个线程B对这个值进行修改,B修改了多次最后结果还是原来那个值,这就是ABA问题,此时需要根据业务场景判断这个值得修改是否需要感知。如果需要感知就可以给这个值再加上一个版本号。

我们用一段代码演示一下cas中ABA的问题吧

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * create by yanghongxing on 2020/5/8 11:03 下午
 */
public class ABA {
    private static AtomicInteger atomicInt = new AtomicInteger(100);

    public static void main(String[] args) throws InterruptedException {
        // 对一个AtomicInteger的值该两次,最后结果与之前相同
        Thread intT1 = new Thread(() -> {
            atomicInt.compareAndSet(100, 101);
            atomicInt.compareAndSet(101, 100);
        });
        
        Thread intT2 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
            boolean c3 = atomicInt.compareAndSet(100, 101);
            // true,执行成功
            System.out.println(c3);
        });
        intT1.start();
        intT2.start();
    }
}

使用jdk中的AtomicStampedReference可以解决这个问题。最后我们看一下cas实现原理,看一下最后native方法的源码 jdk8u: atomic\\_linux\\_x86.inline.hpp

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;

汇编指令 我们看这一条

__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"

\\_\\_asm\\_\\_表示汇编指令,lock表示锁,if如果 mp(%4)表示cpu是多核, cmpxchgl表示 cmp exchange  全称 compare and exchange。最终实现:

lock cmpxchg 指令

这条汇编指令(硬件指令)表示如果是多核CPU则加上锁。

Java对象在内存的布局

我们先了解一下Java对象在内存中的(详细)布局,这个布局与Java锁的实现息息相关。使用工具:JOL = Java Object Layout

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

使用示例

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

输出

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 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

OFFSET:从第几个位置开始
size:大小,单位字节,

TYPE DESCRIPTION:类型描述,上面的示例就是object header对象头,

VALUE:值

loss due to the next object alignment: 由于下一个对象对齐而造成的损失,我们看下面这张图。

markword:关于锁的信息。
class pointer: 表示对象是属于哪个类的。
instance data:字面理解实例数据,比如在在对象中创建了一个int a 就占4个字节,long b就占8个字节。
padding data:如果上面的数据所占用的空间不能被8整除,padding则占用空间凑齐使之能被8整除。被8整除在读取数据的时候会比较快。

对着这张图我们再看看上面JOL打印出来的数据,第一个和第二个都是markword各 4个字节,第三个是class pointer4个字节,本来还有  instance data 用来存成员变量的但是我们写的没有所以为0,这些总共加起来12个字节不能被8整除,所以我们要对齐加4个字节。(注这里的内存占用是默认开启字节压缩XX:+UseCompressedClassPointers -XX:+UseCompressedOops)

看完了这些东西我们再来执行一下下面的代码

/**
 * create by yanghongxing on 2020/5/11 11:52 下午
 */
public class ShowJOL {
    public static void main(String[] args) {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

执行结果:

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 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

对比这个的输出和第一次我们打印的输出,我们可以得出结论synchronized锁的信息是记录在markword上。
我们做Java开发的经常听到的一句话就是synchronized是个重量级的锁,事实上一定是这样吗?我们可以通过分析markword看看synchronized加锁过程,在早期jdk1.0的时候jdk每次申请的就是重量级的锁,性能比较差,随着后面jdk的升级synchronized的性能有所提升,synchronized并不是一开始就加重量级的锁,而是有个慢慢升级的过程。先来看表格

偏向锁Biased Locking:Java6引入的一项多线程优化,偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
自旋锁:自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。一直在自旋也是占用CPU的,如果自旋的线程非常多,自旋次数也非常大CPU可能会跑满,所以需要升级。
重量级锁:内核态的锁,资源开销较大。内部会将等待中的线程进行wait处理,防止消耗CPU。

结合这张表格我们再写一个示例看看synchronized在没有锁竞争的情况下默认是怎么样的。

/**
 * create by yanghongxing on 2020/5/11 11:52 下午
 */
public class ShowJOL {
    public static void main(String[] args) {
        Object o = new Object();
        System.out.println(Integer.toHexString(o.hashCode()));
        System.out.println(ClassLayout.parseInstance(o).toPrintable());

        synchronized (o) {
            System.out.println(Integer.toHexString(o.hashCode()));
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

然后看输出:

5f8ed237

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 37 d2 8e (00000001 00110111 11010010 10001110) (-1898825983)
      4     4        (object header)                           5f 00 00 00 (01011111 00000000 00000000 00000000) (95)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           90 29 7d 06 (10010000 00101001 01111101 00000110) (108865936)
      4     4        (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Disconnected from the target VM, address: \'127.0.0.1:62501\', transport: \'socket\'

Process finished with exit code 0

我们在第一行打印了这个Object的hashcode的16进制编码,对比没有加锁的输出这hashcode是存在对象的markword中的。我们再看这个未加锁的markword的二级制值:00000001 00110111 11010010 10001110,看前8位的倒数3位也就001(口语描述不知道是不是准确

以上是关于你真的了解synchronized和volatile吗?的主要内容,如果未能解决你的问题,请参考以下文章

volatile 关键字没用 啥时候用synchronized?

你真的了解 volatile 关键字吗?

你真的了解volatile关键字吗?

java中volatile关键字,你真的了解吗?volatile原理剖析实例讲解(简单易懂)

java中volatile关键字,你真的了解吗?volatile原理剖析实例讲解(简单易懂)

java中volatile关键字,你真的了解吗?volatile原理剖析实例讲解(简单易懂)