跳到主要内容

synchronized解读

synchronized

对象结构

对象结构介绍

对象结构介绍

  • HotSpot虚拟机 中,对象在内存中存储的布局可以分为三个区域: 对象头(Header)实例数据(Instance Data)对齐填充(Padding)
    • mark-word: 对象标记字段,占4个字节,用于存储一些列的标记位,例如: 哈希值、轻量级锁的标记位、偏向锁标记位、分代年龄等
    • Klass Pointer: Class对象的类型指针,JDK1.8默认开启指针压缩后占4个字节,关闭指针压缩后(-XX:-UseCompressedOops)长度为8个字节,其指向的位置是对象对应的Class对象(对应的元数据对象)的内存地址
    • 对象实际数据: 包括对象的所有成员变量,大小由哥哥成员变量决定,比如: byte = 1个字节(8比特位)int = 4个字节(32比特位)
    • 对齐: 非必须,起占位符作用,由于 HotSpot虚拟机 的内存管理系统要求对象起始地址必须是8字节的整数倍,所以对象头正好是8字节的倍数,当对象实例数据部分没有对齐时,就要通过对齐填充来补全
  • 另外在 mark-word 锁类型标记中,无锁、偏向锁、轻量锁、重量锁以及GC标记,5种锁类型没法用2比特标记(2比特最终有4种组合: 00011011),所以无锁、偏向锁前又占了一位偏向锁标记,最终 001 为无锁,101 为偏向锁

Monitor 对象

  • HotSpot虚拟机monitor 是由C++中的 ObjectMonitor 实现的
  • synchronized 的运行机制就是当 JVM 监测到对象在不同竞争状况时,会自动切换到合适的锁实现,即锁的升级、降级
  • 三种不同的 Monitor 的实现就是常说的三种不同的锁: 偏向锁(Biased Locking)、轻量级锁和重量级锁,当一个 Monitor 被某个线程持有之后,它便处于锁定状态
  • Monitor 主要数据结构:
    // initialize the monitor, exception the semaphore, all other fields
    // are simple integers or pointers
    ObjectMonitor() {
    _header = NULL;
    _count = 0; // 记录个数
    _waiters = 0,
    _recursions = 0; // 线程重入次数
    _object = NULL; // 存储 Monitor 对象
    _owner = NULL; // 持有当前线程的 owner
    _WaitSet = NULL; // 处于wait状态的线程,会被加入到 _WaitSet
    _WaitSetLock = 0 ;
    _Responsible = NULL ;
    _succ = NULL ;
    _cxq = NULL ; // 单向列表
    FreeNext = NULL ;
    _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq = 0 ;
    _SpinClock = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
    }
    源码地址: jdk8/hotspot/file/vm/runtime/objectMonitor.hpp
    • ObjectMonitor: 有两个队列: _WaitSet_EntryList,用于保存 ObjectWaiter 对象列表
    • _owner: 获取 Monitor 对象的线程进入 _owner 时,_count-1,如果线程调用了 wait() 方法,此时会释放 Monitor 对象,_owner 恢复为空,_count-1,同时该等待线程进入 _WaitSet 中等待被唤醒
    • 锁执行效果:
      锁执行效果
      每个Java对象头中都包括 Monitor 对象(存储的指针的指向),synchronized 也是通过这种方式获取锁,因此 synchronized() 传入任何对象都能获取锁

synchronized 特性

原子性

  • 原子性指一个操作不是不可中断的,只能全部执行成功或者全部执行失败
  • 测试:
    private static volatile int counter = 0;
    public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10; i++) {
    Thread thread = new Thread(() -> {
    for (int i1 = 0; i1 < 10000; i1++) {
    add();
    }
    });
    thread.start();
    }
    // 等10个线程运行完毕
    Thread.sleep(1000);
    System.out.println(counter);
    }
    public static void add() {
    synchronized (AtomicityTest.class) {
    counter++;
    }
    }
    反编译后的指令码:
    public static void add();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
    stack=2, locals=2, args_size=0
    0: ldc #12 // class org/itstack/interview/AtomicityTest
    2: dup
    3: astore_0
    4: monitorenter
    5: getstatic #10 // Field counter:I
    8: iconst_1
    9: iadd
    10: putstatic #10 // Field counter:I
    13: aload_0
    14: monitorexit
    15: goto 23
    18: astore_1
    19: aload_0
    20: monitorexit
    21: aload_1
    22: athrow
    23: return
    Exception table:
    同步方法: ACC_SYNCHRONIZED 是一个同步标识,对应的16进制值是 0x0020,线程进入方法时,都会判断是否有此标识,然后开始竞争 Monitor 对象
    同步代码:
    • monitorenter: 在判断拥有同步标识 ACC_SYNCHRONIZED 后抢先进入此方法的线程会优先拥有 Monitorowner,此时计数器 +1
    • monitorexit: 当执行完成推出后,计数器 -1,归 0 后被其他进入的线程获取

可见性

  • 为什么添加 synchronized 后能保证变量的可见性
    1. 线程解锁前,必须把共享内存的最新值刷新到主内存中
    2. 线程加锁前,将清空工作内存中共享变量的值,从而导致使用共享变量时需要从主内存中重新读取最新的值
    3. volatile 的可见性通过内存屏障(Memory Barrier)来实现,而 synchronized 靠操作系统内核互斥锁实现,相当于 JMM 中的 lockunlock,退出代码块时刷新变量到主内存

有序性

  • as-if-serial: 保证不管编译器和处理器为了性能优化会如何进行指令重排序,都需要保证单线程下运行结果的正确性,即: 在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,所有的操作都是无序的

  • 双重检验锁(Double-checked Locking):

    public class Singleton {
    private Singleton() {
    }

    private volatile static Singleton instance;

    public Singleton getInstance() {
    if (instance == null) {
    synchronized (Singleton.class) {
    if (instance == null) {
    instance = new Singleton();
    }
    }
    }
    return instance;
    }
    }
    • synchronized 的有序性并不是 volatile 的防止指令重排序,因此即使 synchronized 拥有可见性的特点,还是需要使用 volatile 关键字
    • 不加 volatile 可能导致的结果: 第一个线程在初始化对象设置 instance 指向内存地址时,第二个线程进入,有指令重排序,在判断 if(instance == null) 时有可能出错,因为此时 instance 可能还没初始化完成