杠精学synchronized(1)-初探和各种锁的测试验证

322 阅读39分钟

一个简单的并发例子

private static volatile int counter = 0;
public static void increment() {
    counter++;
}
public static void decrement() {
    counter--;
}
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            increment();
        }
    }, "t1");
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            decrement();
        }
    }, "t2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(String.format("counter=%s", counter));
}
运行结果: counter=-1564(不确定, 反正基本不可能是0)

在上面的代码中, 分别对counter+和-了50000次, 但是结果打印出来counter并不是0, 原因很简单, 因为counter++和counter--并不是一个原子操作

对于++操作:

image.png

  1. getstatic i // 获取静态变量i的值
  2. iconst_1 // 将int常量1压入操作数栈
  3. iadd // 自增
  4. putstatic i // 将修改后的值存入静态变量i 对于--操作:

image.png

  1. getstatic i // 获取静态变量i的值
  2. iconst_1 // 将int常量1压入操作数栈
  3. isub // 自减
  4. putstatic i // 将修改后的值存入静态变量i

因此在多个线程同时执行这些代码的时候, 由于线程执行的不确定性, 最终这4步操作会错综复杂的执行, 而且它们操作的也都是counter这个变量, 因此得到的结果就不会是预期的值0

竞态条件(Race Condition)

程在临界区内执行, 由于代码的执行序列不同而导致的结果无法预测, 称之为发生了竞态条件

为了避免临界区竞态条件发生, 有多种手段可以达到目的

  1. 阻塞式的解决方案: synchronized, Lock

  2. 非阻塞式的解决方案: 原子变量

    注意: java中互斥和同步都可以采用synchronized来完成, 但这两个概念还是有区别的, 互斥是保证临界区的竞态条件发生, 同一时刻只有一个线程能执行临界区的代码, 而同步是由于线程执行的先后顺序不同, 需要一个线程等待其它线程运行到某个点

synchronized的使用

synchronized同步块是java内部提供的一种原子性内置锁, java中的每个对象都可以被当做一个同步锁来使用, 这些java内置的使用者看不到的锁被称为内置锁, 也叫做监视器锁

下面是synchronized加在不同的地方的区别

  1. 加在实例方法上 锁对象就是当前的this对象. 例如: public synchronized void doSth(){}

  2. 加在静态方法上 锁对象就是当前的类对象. 例如: public static synchronized void doSth(){}

  3. 代码块中设置this 锁对象就是当前的this对象. 例如: synchronized(this){}

  4. 代码块中设置class对象 锁对象就是class对象. 例如: synchronized(Demo.class){}

  5. 代码块中设置任意对象 锁对象就是那个对象. 例如: synchronized(new Lock()){}

在上面的例子中, 只需要将increment和decrement方法分别都加上synchronized就可以解决并发问题了.

public static synchronized void increment() {
    counter++;
}

public static synchronized void decrement() {
    counter--;
}
运行结果: counter=0

synchronized的底层原理

synchronized简介

synchronized是jvm内置锁, 基于Monitor机制实现, 依赖底层操作系统的互斥原语Mutex(互斥量), 它是一个重量级锁, 性能较低, jdk1.5开始版本做了巨大的优化, 例如锁粗化(Lock Coarsening), 锁消除(Lock Elimination), 轻量级锁(Lightweight Locking), 偏向锁(Biased Locking), 自适应自旋(Adaptive Spinning)等技术手段来减少锁操作的开销, 内置锁的并发性能已经基于和ReentrantLock持平了

Java虚拟机通过一个同步结构支持方法和方法中的指令序列的同步: Monitor.

同步方法是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现; 同步代码块是通过monitorenter和monitorexit来实现. 两个指令的执行是jvm通过调用操作系统的互斥原语Mutex来实现, 被阻塞的线程会被挂起, 等待重新调度, 会导致用户态和内核态两个态之间的来回切换, 对性能有较大的影响.

查看synchronized的字节码指令序列

加在代码块中

image.png

可以看到, 确实是在getstatic之前加了monitorenter, 在aload_0后面加了monitorexit, 之所以有2个monitorexit是因为方法可能抛异常

实际上java也提供了api使用monitor

image.png

加在方法上

image.png

可以看到, access flags变了, 本来正常是0009, 现在变成了0029, 并且后面加了个synchronized.

image.png

这就是java方法的访问标志, 标志一个方法是一个什么样的类型的方法, 比如上面的decrement方法就是0009, 因为它是一个public的, 又是一个static的, 因此0001 + 0008 = 0009. 由于increment方法是synchronized, 就会有一个ACC_SYNCHRONIZED标志是0020, 因此它的访问标志就是0001 + 0008 + 0020 = 0029

Monitor(管程/监视器)

Monitor直译为监视器, 而操作系统领域一般翻译为管程. 管程是指管理共享变量以及对共享变量操作的过程, 让它们支持并发. 在java1.5之前, java语言提供的唯一并发就是管程, java1.5之后也是以此为基础的. synchronized关键字和wait(), notify(), notifyAll()这三个方法是java中实现管程技术的组成部分.

MESA模型

管程模型有多种, 如Hasen, Hoare和MESA, 其中使用最多的就是MESA模型, 而synchronized和ReentrantLock都是使用的MESA模型.

image.png

管程中引入了条件变量的概念, 而且每个条件变量都对应有一个等待队列, 条件变量和等待队列的作用就是解决线程之间的同步问题.

当大量线程过来争抢资源的时候, 如果资源不够, 没抢到资源的线程将会在入口等待队列进行等待. 而抢到资源的线程, 当在做一些业务的时候, 发现条件不足无法继续, 就会使用wait机制去释放锁, 并且进入到条件变量等待队列中. 等待条件满足的时候, 再通过notify机制重新进入入口等待队列争夺资源.

wait的正确使用姿势

对于MESA管程来说, 有一个编程范式

while(条件不满足) {
    wait();
}

由于wait的线程迟早会被唤醒继续执行, 而synchronized只提供了notify随机唤醒和notifyAll全部唤醒, 而且synchronized只有一个等待队列, 因此如果想要唤醒特定线程, 只能通过notifyAll去唤醒全部, 这样就很有可能唤醒那些本该不满足条件的线程. 因此当线程被唤醒发现条件又不满足, 还是需要继续进入等待队列中, 因此需要使用while循环. MESA模型的wait方法还有一个超时参数, 为了避免线程进入等待队列永久阻塞.

ObjectMonitor的数据结构

从java的层面, 也就只能分析到上面那部分了, 如果要看Monitor是如何实现的, 只能查看jdk的源码了.

源码目录: /src/share/vm/runtime/objectMonitor.hpp

objectMonitor类

image.png

objectMonitor的构造方法和析构方法 image.png

对于这个类, 有几个属性比较重要

  1. _recursions 锁的重入次数

  2. _object 存储的是锁对象的指针

  3. _owner 哪个线程获取了这个锁, 就记录的那个线程

  4. _WaitSet 等待队列, 调用wait方法等待的线程, 就会陆续进入这个队列中, 这是一个双向链et就是头结点

  5. _cxq 如果有多个线程竞争资源, 则这些线程会先进入到cxq队列中, 这是一个单向链表栈结构, 先进后出

  6. _EntryList 存放进入或重新进入时被阻塞的线程, 也就是竞争锁失败的线程, 是一个双向链表, 先进先出

每一个java对象, 都会对应一个ObjectMonitor对象.

获取锁进入等待队列的策略

monitor提供了2个等待队列, 一个叫做cxq, 一个叫做EntryList, jdk有一个默认的策略: 在获取锁的时候, 将当前线程插入cxq的头部, 而释放锁的时候, 默认策略是如果EntryList为空, 就将cxq中的元素按照FILO的顺序插入到EntryList, 并先进先出地唤醒第一个进入的线程, 也就是说当EntryList为空的时候, 是后来的线程先获取锁. 如果EntryList不为空, 直接从EntryList中唤醒线程.

默认策略代码证明1

public static void main(String[] args) throws InterruptedException {
    SyncQModeDemo demo = new SyncQModeDemo();
    demo.startThreadA();
    //控制线程执行时间
    Thread.sleep(100);
    demo.startThreadB();
    Thread.sleep(100);
    demo.startThreadC();
}
final Object lock = new Object();
public void startThreadA() {
    new Thread(() -> {
        synchronized (lock) {
            System.out.println("A get lock");
            try {
                lock.wait(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A release lock");
        }
    }, "thread-A").start();
}

public void startThreadB() {
    new Thread(() -> {
        synchronized (lock) {
            try {
                System.out.println("B get lock");
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B release lock");
        }
    }, "thread-B").start();
}
public void startThreadC() {
    new Thread(() -> {
        synchronized (lock) {
            System.out.println("C get lock");
        }
    }, "thread-C").start();
}
运行结果: 
A get lock
B get lock
B release lock
A release lock
C get lock

上述代码中, 控制ABC3个线程的执行时间, B比A晚100毫秒, C比B晚100毫秒, 所有业务逻辑都使用synchronized代码块包住, A线程启动后wait300毫秒, 这样B线程就能在A线程启动之后立刻拿到锁, 并休眠了500毫秒, 由于是sleep休眠, 不会释放锁, 因此C线程开始之后, 会因为拿不到锁被阻塞, 注意: C拿不到锁阻塞进入cxq的时候, 是A线程wait第200毫秒的时候, 这时候A线程还在wait, 当A线程再wait100毫秒后, 会被唤醒去拿锁, 拿不到锁, 然后进入cxq中. 根据前面说的默认策略, AC都会进入cxq队列中, 而C比A先进入cxq, B休眠结束出synchronized代码块之后释放了锁, 会唤醒A和C, 由于EntryList为空, 所以A和C会以先A再C的顺序进入EntryList中, 然后唤醒EntryList队列头部的线程, 队列头部线程就是A, 所以A一定会先拿到锁, 然后执行"A release lock"的代码. 等到A释放锁之后, C才能拿到锁.

默认策略代码证明2

将上面的代码startThreadA方法改动一下, 把wait改成sleep

public void startThreadA() {
    new Thread(() -> {
        synchronized (lock) {
            System.out.println("A get lock");
            try {
                Thread.sleep(300);
                //  lock.wait(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A release lock");
        }
    }, "thread-A").start();
}
运行结果:
A get lock
A release lock
C get lock
B get lock
B release lock

根据默认策略, A启动之后sleep了300毫秒, 不会释放锁, 那么接下来200毫秒内启动的B, C线程, 都会因为拿不到锁进入cxq, B比C早了100毫秒进入, 然后A线程释放锁之后, 唤醒了B, C线程, 发现EntryList中没有线程, 于是从cxq中按照先C再B的顺序把线程拿出来, 再压入EntryList栈中, 唤醒队列头部线程, 队列头部线程是C, 因此可以看到一个很有趣的结果, 明明B应该是比C先排队, 然而C竟然比B先获取锁.

加锁的原理

我们知道, 所谓加锁, 实际上锁对象就是所谓的这把锁, 那么锁对象是如何记录锁状态的?

对象内存布局

hotspot虚拟机中, 对象在内存中存储的布局可以分为三块区域: 对象头(Header), 实例数据(Instance data)和对其填充(padding)

  • 对象头 比如hash码, 对象所属的年代, 对象锁, 锁状态标志, 偏向锁(线程)ID, 偏向时间, 数组长度(数组对象才有)
  • 实例数据 存放类的属性数据信息, 包括父类的属性信息
  • 对齐填充 由于虚拟机要求对象起始地址必须是8字节的整数倍, 填充数据不是必须存在的, 仅仅是为了字节对齐

image.png

对象头

对象头包含三部分, mark word, klass pointer, 数组长度

MarkWord

用于存储对象自身的运行时数据, 如哈希码, GC分代年龄, 锁状态标识, 线程持有的锁, 偏向线程ID, 偏向时间戳等, 这部分数据的长度在32位和64位虚拟机中分别为32bit和64bit, 官方称为mark word

Klass Pointer

对象头的另外一部分是klass类型指针, 即对象指向它的类元数据的指针, 虚拟机通过这个指针来确定这个对象是哪个类的实例. 32位4字节, 64位开启指针压缩或最大堆内存<32g时4字节, 否则8字节, jdk8默认开启指针压缩后为4字节, 当在jvm参数中关闭指针压缩(-XX:-UseCompressedOops)后, 长度为8字节.

数组长度(只有数组才有)

如果对象是一个数组, 那在对象头中还必须有一块数据记录数组长度, 为什么数组对象要有这个数据呢? 举个例子. 在C语言中, 创建数组的时候, 只有在数组所在的局部里才能得到数组的总大小, 数组一旦传参, 都是以指针的方式传递, 就已经无法拿到数组的总大小了, 但是通过指针类型可以确定每个元素的大小, 因为内存是无状态的, 内存并不知道你这个区域内放的是什么东西, 也就是说内存根本不关心这块内存存的是一个数组, 内存只知道这块区域是0101的bit, 因此在C语言中传递数组的时候, 一定要传递长度, 否则就会发生数组越界访问的可能, 越界访问很可能会访问到一些操作系统不允许访问的内存, java为了让人们传递数组不会像C语言那么麻烦, 就在对象头中记录了数组的长度, 然后还加入了数组下标溢出的异常, 防止此类现象的发生.

image.png

使用JOL工具查看内存布局

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>
public static void main(String[] args) {
    Object obj = new Object();
    System.out.println(ClassLayout.parseInstance(obj).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

上面两行是mark word, 最下面一行是对其填充, Object对象的总大小是16字节

mark word详细结构

hotspot通过markOopDesc类实现Mark Word, 具体实现位于markOop.hpp文件中.

image.png

enum { 
         age_bits                 = 4, // 分代年龄
         lock_bits                = 2, // 锁位数
         biased_lock_bits         = 1, // 偏向锁位数
         max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits, // 64 - 4 - 2 - 1 = 57 // hashcode最大位数
         hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits, // 三目运算符, 57>31, 因此hashcode位数是31
         cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
         epoch_bits               = 2 // 偏差有效性的时间戳位数
};

下面是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)

32位jvm下的对象结构描述

image.png

64位jvm下的对象结构描述

image.png

hash_bits(64位jvm下)

保存对象的hashcode, 通过上面的图以及代码可以证明, 规定的最大hashcode可以占57位, 但是实际上hash_bits经过计算后, 只有31位. hashcode是延迟计算的, 在运行期间当调用对象原生的native hashcode()方法时, 或者是调用System.identityHashCode()方法时, jvm会计算hashcode值, 然后赋值到这里.

hashcode延迟计算以及取对象头26-56位的证明

上面jol打印是打印的小端模式, 我们把它转成大端模式分析一下hashcode是否真的是延迟计算的.

原本:
01 00 00 00 (00000001 00000000 00000000 00000000) (1)
00 00 00 00 (00000000 00000000 00000000 00000000) (0)
e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)

由于mark word占了64位, 把上面两行转成大端书写
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

可以看到, 从163位都是0, 那么hashcode肯定是0

下面我们调用一下hashcode方法, 然后打印一下对象头
public static void main(String[] args) {
    Object obj = new Object();
    System.out.println(obj.hashCode());
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
输出结果: 
1735600054
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 b6 27 73 (00000001 10110110 00100111 01110011) (1931982337)
      4     4        (object header)                           67 00 00 00 (01100111 00000000 00000000 00000000) (103)
      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

得到前面两行是: 
01 b6 27 73 (00000001 10110110 00100111 01110011) (1931982337)
67 00 00 00 (01100111 00000000 00000000 00000000) (103)

转成大端书写:
00000000 00000000 00000000 01100111 01110011 00100111 10110110 00000001

按照上面的分析, hashcode是从第26位到第56位, 总31位, 也就是
1100111 01110011 00100111 10110110 转成10进制: 1735600054

和上面打印的输出结果的hashcode相同, 由此证明, hashcode只有当调用之后才会生成

age_bits

保存对象分代年龄, 4位, 最大值十进制15, 表示对象被GC的次数. 当该数值达到阈值的时候, 对象将会被移入老年代

biased_lock_bits

偏向锁标志位, 1位, 由于无锁和偏向锁的锁标识都是01, 没办法区分, 因此引入一位用于偏向锁标志位

lock_bits

锁状态标志位, 2位, 区分锁状态. 比如11时表示对象待GC回收状态, 只有最后两位锁标识(11)有效

JavaThread*

保存持有偏向锁的线程的线程ID, 偏向锁模式的时候, 当某个线程持有对象的时候, 对象这里就会被置为该线程的id, 在后面的操作中, 就无须再去进行尝试获取锁的动作了. 这个线程id并不是jvm分配的线程编号, 和Java中的Thread.getId是两个概念.

epoch

保存偏向时间戳, 2位, 偏向锁在cas锁操作过程中, 偏向性标识, 标识对象更偏向哪个锁

ptr_to_lock_record

轻量级锁状态下, 指向栈中锁记录的指针. 当锁获取是无竞争时, jvm使用原子操作而不是使用OS互斥, 这种技术称为轻量级锁定. 在轻量级锁定的情况下, jvm使用cas操作在对象的mark word中设置指向锁记录的指针.

ptr_to_heavyweight_monitor

重量级锁状态下, 指向对象监视器Monitor对象的指针. 如果两个不同的线程同时在一个锁对象上竞争, 则必须将轻量级锁升级到Monitor以管理(objectMonitor中提供的各种队列)等待的线程. 在重量级锁定的情况下, jvm在对象的ptr_to_heavyweight_monitor设置指向Monitor对象的指针.

加锁原理细节

锁状态

在markOop.hpp中定义了5种锁状态

enum {
    locked_value             = 0, //00 轻量级锁
    unlocked_value           = 1, //001 无锁
    monitor_value            = 2, //10 监视器锁,也叫膨胀锁,也叫重量级锁
    marked_value             = 3, //11 GC标记
    biased_lock_pattern      = 5  //101 偏向锁
};

直观来看

锁状态存储内容偏向锁标志位锁标志位
无锁哈希code, 分代年龄001
偏向锁线程id, 时间戳, 分代年龄101
轻量级锁指向栈中锁记录的指针ptr00
重量级锁指向monitor对象的指针ptr10
GC标识11

偏向锁测试

偏向锁存在的意义

类似于局部性原理, 大量的实际情况证明, 在大多数情况下, 锁不仅不存在多线程竞争, 而且总是由同一个线程多次获得, 这是由于我们总会在不经意间在一条调用链上使用大量的锁(如下代码所示), 因此为了消除这种大量无竞争情况下的加锁重入(cas操作)的开销, 引入了偏向锁. 对于没有竞争的场合, 偏向锁的效率是非常高的.

// 不经意间使用的加锁1: StringBuffer内部同步
public synchronized int length() {
    return count;
}
// 不经意间使用的加锁2: sout的内部同步
public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

按照上面的分析, 当创建一个对象之后, 这个对象的mark word的锁状态标志位应该是001, 当进入synchronized代码块之后, 锁状态标识为应该为偏向锁标志位101, 下面通过代码测试看看是不是这样, 为了看的方便, 将只把输出的对象头结果贴出来.

public static void main(String[] args) {
    Object obj = new Object();
    System.out.println("打印obj对象的无锁状态对象头:");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    synchronized (obj){
        System.out.println("打印obj对象的第一次加锁后的对象头:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
    System.out.println("打印obj对象释放锁后的对象头:");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
运行结果:
打印obj对象的无锁状态对象头:
01 00 00 00 (00000001 00000000 00000000 00000000) (1)
00 00 00 00 (00000000 00000000 00000000 00000000) (0)

打印obj对象的第一次加锁后的对象头:
c8 f3 6f f7 (11001000 11110011 01101111 11110111) (-143658040)
45 00 00 00 (01000101 00000000 00000000 00000000) (69)

打印obj对象释放锁后的对象头:
01 00 00 00 (00000001 00000000 00000000 00000000) (1)
00 00 00 00 (00000000 00000000 00000000 00000000) (0)

根据输出结果, 发现对象头锁状态标志在第一次进入synchronized之后, 并不是预期的那样进入偏向锁状态, 而是直接进入了轻量级锁状态, 本该指向hashcode的那一块现在贡献给了指向栈中锁记录的指针了. 这是由于java默认开启了偏向延迟.

偏向延迟

hotspot虚拟机在启动main方法之后, 需要经过大概4-5秒的延迟之后才会给每个新建的对象开启偏向锁模式. 这是由于jvm在启动的时候也会进行一系列的复杂的活动, 比如装载配置, 系统初始化等等, 在这个过程中也会大量使用synchronized进行加锁, 并且这些锁大部分都不会是偏向锁, 因为有非常多的线程去竞争. 因此为了加快系统的启动, jvm默认延迟启动偏向锁.

-XX:BiasedLockingStartupDelay=0 // 关闭延迟开启偏向锁
-XX:-UseBiasedLocking // 禁止使用偏向锁
-XX:+UseBiasedLocking // 启用偏向锁

开启偏向模式后, 再测试

public static void main(String[] args) throws Exception {
    TimeUnit.SECONDS.sleep(5);
    Object obj = new Object();
    System.out.println("打印obj对象的无锁状态对象头:");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    synchronized (obj) {
        System.out.println("打印obj对象的第一次加锁后的对象头:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
    System.out.println("打印obj对象释放锁后的对象头:");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
运行结果:
打印obj对象的无锁状态对象头:
05 00 00 00 (00000101 00000000 00000000 00000000) (5)
00 00 00 00 (00000000 00000000 00000000 00000000) (0)

打印obj对象的第一次加锁后的对象头:
05 70 e1 fc (00000101 01110000 11100001 11111100) (-52334587)
a6 02 00 00 (10100110 00000010 00000000 00000000) (678)

打印obj对象释放锁后的对象头:
05 88 e2 b4 (00000101 10001000 11100010 10110100) (-1260222459)
c5 01 00 00 (11000101 00000001 00000000 00000000) (453)

又发现了一个奇怪的现象, 新new的Object, 在无锁状态下, 锁标识位居然是101, 这明明是轻量级锁的标志. 实际上这个叫做匿名偏向. 匿名偏向是指该对象正处于可偏向状态, 但是由于并未偏向任何线程, 这时候它的偏向线程id为0.

进入synchronized代码块中之后, 对obj对象加了锁, 这时候理所当然的, 锁状态标志位是101, 并且偏向线程id也已经存在于mark word中了.

出synchronized代码块之后, 由于obj对象已经偏向了主线程, 因此mark word中依然会存放主线程的线程id, 锁状态标志依然是101. 说明该对象还是偏向锁模式. 未来使用该obj对象作为锁的线程, 如果还是主线程, 将不会发生改变.

问题:
如果锁一个class对象, 比如我们当前跑的这个类: JolTest.class, 会出现什么情况?

答:
进入synchronized之前是001, 进入synchronized代码块之后是00.
因为JolTest.class是在偏向延迟到点之前创建的(由jvm创建)
等待偏向延迟结束, 只能让延迟结束之后新建的对象享受可偏向待遇, 之前就已经创建的对象是没有这个待遇的.

偏向锁撤销

锁对象调用hashcode()导致偏向锁撤销

前面有提到过, hashcode是延迟加载的, 对于一个对象来说, 只有当调用了它的hashcode()方法, 才会去计算hashcode, 并且存放于该对象的对象头中, 占据31个bit位. 一个对象如果不调用它的hashcode方法, mark word属于hashcode的那部分bit位将会是初始值0.

需要注意的是, 这里所说的hashcode是原生的hashcode方法, 如果类重写了hashcode方法, 并且没有调用super.hashcode()的话, 由于没有走到native那一层hashcode方法, 将不会起到下方代码的作用, 如果有重写hashcode并且并没有使用super.hashcode(), 那么得使用System.identityHashCode(obj)才能达到下面代码的效果.

public static void main(String[] args) throws Exception {
    TimeUnit.SECONDS.sleep(5);
    Object obj = new Object();
    obj.hashCode();
    System.out.println("打印obj对象调用hashcode之后, 还未进入synchronized中的对象头:");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    synchronized (obj) {
        System.out.println("打印obj对象调用hashcode之后, 进入synchronized中之后的对象头:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
    System.out.println("打印obj对象释放锁后的对象头:");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
运行结果:
打印obj对象调用hashcode之后, 还未进入synchronized中的对象头:
01 b6 27 73 (00000001 10110110 00100111 01110011) (1931982337)
67 00 00 00 (01100111 00000000 00000000 00000000) (103)

打印obj对象调用hashcode之后, 进入synchronized中之后的对象头:
c8 f6 6f d0 (11001000 11110110 01101111 11010000) (-797968696)
4f 00 00 00 (01001111 00000000 00000000 00000000) (79)

打印obj对象释放锁后的对象头:
01 b6 27 73 (00000001 10110110 00100111 01110011) (1931982337)
67 00 00 00 (01100111 00000000 00000000 00000000) (103)

从运行结果来看, 调用obj的hashcode方法之后, obj对象头的mark word的锁状态不再是101(可偏向/偏向锁), 而变为001, 表示不可偏向, 并且无锁, mark word中也存在了hashcode值.

进入synchronized之后, obj的mark word锁状态变成了00, 也就是轻量级锁状态, hashcode部分也发生了变化, 存储的是栈中锁记录的指针.

出synchronized之后, obj的mark word锁状态恢复为001, 而hashcode也恢复为原本的值了.

调用obj.hashcode()或者是System.identityHashCode(obj)方法后, 会导致偏向锁撤销, 因为对于一个对象, hashcode只会生成一次并且保存于该对象的mark word中, 偏向锁是没有地方存储hashcode的, 因此为了存储hashcode, 只能牺牲偏向锁了.

  • 轻量级锁会在栈中锁记录保存hashcode
  • 重量级锁会在ObjectMonitor对象中保存hashcode
  • 当对象处于可偏向(101, 并且线程ID为0)/已偏向(101, 线程ID存在)时, 调用hashcode将会使对象再也无法偏向.
  • 当对象可偏向时, 调用hashcode方法, 对象将变为无锁状态, 后续第一次加锁将会是轻量级锁(上述代码已证明).
  • 当对象正处于偏向锁时, 调用hashcode方法, 将会使当前的偏向锁强制升级为重量级锁(下方代码证明).
public static void main(String[] args) throws Exception {
    TimeUnit.SECONDS.sleep(5);
    Object obj = new Object();

    System.out.println("打印obj对象调用hashcode之前, 还未进入synchronized中的对象头:");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    synchronized (obj) {
        System.out.println("打印obj对象调用hashcode之前, 进入synchronized中之后的对象头:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        obj.hashCode();
        System.out.println("打印obj对象调用hashcode之后, 进入synchronized中之后的对象头:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
    System.out.println("打印obj对象调用hashcode之后, 释放锁后的对象头:");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
运行结果:
打印obj对象调用hashcode之前, 还未进入synchronized中的对象头:
05 00 00 00 (00000101 00000000 00000000 00000000) (5)
00 00 00 00 (00000000 00000000 00000000 00000000) (0)

打印obj对象调用hashcode之前, 进入synchronized中之后的对象头:
05 78 46 76 (00000101 01111000 01000110 01110110) (1984329733)
43 02 00 00 (01000011 00000010 00000000 00000000) (579)

打印obj对象调用hashcode之后, 进入synchronized中之后的对象头:
aa 3c 29 7a (10101010 00111100 00101001 01111010) (2049522858)
43 02 00 00 (01000011 00000010 00000000 00000000) (579)

打印obj对象调用hashcode之后, 释放锁后的对象头:
aa 3c 29 7a (10101010 00111100 00101001 01111010) (2049522858)
43 02 00 00 (01000011 00000010 00000000 00000000) (579)

第一次打印: 还没调用hashcode, 还没进入synchronized, 锁状态为101, 线程id为0, 匿名偏向状态.

第二次打印: 已经进入synchronized, 还没调用hashcode, 锁状态为101, 线程id为主线程id, 偏向锁状态.

第三次打印: 还没出synchronized, 在还是处于偏向锁状态的时候调用了hashcode之后, 锁状态为10, mark word为monitor对象指针, 是为重量级锁.

第四次打印: 出了synchronized, 已经释放了锁, 但是锁状态依然为10, mark word没有变化依然还是monitor对象指针. (跟预期有差别, 本该是001无锁状态, 因为释放monitor改状态是一个异步[依靠GC清理]的过程, main线程优先执行了, 如果在打印之前加上一句主线程sleep一秒, 就会是001无锁状态)

锁对象调用wait()/notify()导致偏向锁撤销

1. 偏向锁持有状态下, 调用notify()/notifyAll()升级为轻量级锁

public static void main(String[] args) throws Exception {
    TimeUnit.SECONDS.sleep(5);
    Object obj = new Object();
    System.out.println("打印obj对象调用notify之前, 还未进入synchronized中的对象头:");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    synchronized (obj) {
        System.out.println("打印obj对象调用notify之前, 进入synchronized中之后的对象头:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        obj.notify();
        System.out.println("打印obj对象调用notify之后, 进入synchronized中之后的对象头:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
    System.out.println("打印obj对象调用notify之后, 释放锁后的对象头:");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
运行结果:
打印obj对象调用notify之前, 还未进入synchronized中的对象头:
05 00 00 00 (00000101 00000000 00000000 00000000) (5)
00 00 00 00 (00000000 00000000 00000000 00000000) (0)

打印obj对象调用notify之前, 进入synchronized中之后的对象头:
05 98 b4 5b (00000101 10011000 10110100 01011011) (1538562053)
57 02 00 00 (01010111 00000010 00000000 00000000) (599)

打印obj对象调用notify之后, 进入synchronized中之后的对象头:
28 f8 ef 36 (00101000 11111000 11101111 00110110) (921696296)
e8 00 00 00 (11101000 00000000 00000000 00000000) (232)

打印obj对象调用notify之后, 释放锁后的对象头:
01 00 00 00 (00000001 00000000 00000000 00000000) (1)
00 00 00 00 (00000000 00000000 00000000 00000000) (0)

发现在偏向锁持有(101+线程id)情况下, 调用notify方法后, 锁状态变为了00(轻量级锁+lock record指针), 释放锁之后, 锁状态恢复为001(无锁).

2. 偏向锁持有情况下, 调用对象的wait(long timeout)会导致偏向锁撤销并升级为重量级锁

public static void main(String[] args) throws Exception {
    TimeUnit.SECONDS.sleep(5);
    Object obj = new Object();
    System.out.println("打印obj对象调用wait之前, 还未进入synchronized中的对象头:");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    synchronized (obj) {
        System.out.println("打印obj对象调用wait之前, 进入synchronized中之后的对象头:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        obj.wait(300);
        System.out.println("打印obj对象调用wait之后, 进入synchronized中之后的对象头:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
    // 等待锁释放改完状态
    TimeUnit.SECONDS.sleep(1);
    System.out.println("打印obj对象调用wait之后, 释放锁后的对象头:");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
运行结果:
打印obj对象调用wait之前, 还未进入synchronized中的对象头:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      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

打印obj对象调用wait之前, 进入synchronized中之后的对象头:
05 60 61 86 (00000101 01100000 01100001 10000110) (-2040438779)
40 02 00 00 (01000000 00000010 00000000 00000000) (576)

打印obj对象调用wait之后, 进入synchronized中之后的对象头:
9a 8d a5 aa (10011010 10001101 10100101 10101010) (-1431990886)
40 02 00 00 (01000000 00000010 00000000 00000000) (576)

打印obj对象调用wait之后, 释放锁后的对象头:
01 00 00 00 (00000001 00000000 00000000 00000000) (1)
00 00 00 00 (00000000 00000000 00000000 00000000) (0)

可以看到, 在进入synchronized之后, 调用wait之前, 锁状态和mark word还是101+线程id(偏向锁+线程id), 当调用了wait之后, 锁状态变为10(重量级锁), 并且出了synchronized释放锁之后, 变为无锁状态.

偏向锁撤销总结

  1. 锁对象在可偏向状态(101+线程id为0)时调用hashcode方法, 会将锁状态变为001无锁状态, 后续第一次进入同步块将会对该对象加轻量级锁(00+线程栈lock record指针).

  2. 锁对象在偏向锁持有状态(101+线程id)时调用hashcode方法, 会将当前在该对象上加的偏向锁升级为重量级锁.

  3. 锁对象在偏向锁持有状态(101+线程id)时调用notify/notifyAll方法, 会将当前在该对象上加的偏向锁升级为轻量级锁.

  4. 锁对象在偏向锁持有状态(101+线程id)时调用wait(long timeout)方法, 会将当前在该对象上加的偏向锁升级为重量级锁.

解释

可偏向状态调用hashcode方法会使得偏向锁无处存放hashcode, 因此会牺牲偏向锁, 升级为轻量级锁.

偏向锁持有状态调用hashcode方法, 使得当前偏向锁升级为重量级锁, 这个结果非常颠覆认知, 让我一脸懵逼, 源码能力有限我也没找到原因, 猜测应该就是由于没地方存放hashcode, 又不能在栈中创建lock record(至于为什么, 我不知道, 别问我), 只能关联monitor去存放hashcode了

上面的偏向锁持有状态调用了notify方法导致偏向锁升级为轻量级锁确实有点让人摸不着头脑, 我也不知道为什么要这么设计(源码能力有限, 只能个人猜测, 可能也是由于notify可能需要hashcode, 但是hashcode还没生成, 没必要去存放hashcode而升级为重量级锁, 所以牺牲偏向锁升级为轻量级锁), 但是wait导致偏向锁升级为重量级锁还是比较好想的, wait方法会让当前线程进入WaitSet, WaitSet是哪里来的? 只有objectMonitor才会有这玩意儿, 因此如果想让线程进入WaitSet等待, 只能关联monitor, 既然对象都关联了monitor, 自然是升级为了重量级锁.

轻量级锁和重量级锁测试

轻量级锁

倘若偏向锁失败, 虚拟机一般并不会立刻启动重量级锁(前面示例中偏向锁持有过程中调用hashcode这种情况就会变为重量级锁), 而是启用轻量级锁的优化方案, 此时mark word的结构也变为轻量级锁特有的结构. 轻量级锁出现的场合一般是线程交替执行同步块的场合, 如果存在同一时刻多个线程争抢这个锁对象加锁的时候, 将会膨胀为重量级锁.

偏向锁/轻量级锁/重量级锁区别

偏向锁和轻量级锁的状态更新, 都是操作的mark word, 由于mark word是属于用户态的内存区域, 只要通过cas就可以操作, 并不需要进行系统调用. 而重量级锁操作的是objectMonitor对象, 对于锁状态的更改, 是基于内核态的内存区域, 将会发生系统调用, 导致用户/内核态的切换.

偏向锁升级为轻量级锁

在偏向锁的篇幅中, 已经通过代码证明了有如下几种情况偏向锁会升级为轻量级锁

  1. 在对象锁状态为可偏向的时候, 调用了Object类原生的native hashcode, 或者是System.identityHashCode方法, 那么随后的加锁将会是轻量级锁. (注意: 是可偏向状态下, 在偏向锁持有状态下调用hashcode, 将会升级为重量级锁)
  2. 在偏向锁持有状态下调用notify/notifyAll, 将会导致锁升级为轻量级锁. (注意: 可偏向状态, 也就是未加锁状态, 是无法调用notify/wait的, 会报错)
  3. "轻微竞争"下, 偏向锁升级为轻量级锁.

"轻微竞争"下, 偏向锁升级为轻量级锁

模拟轻微竞争的场景

public static void main(String[] args) throws Exception {
    TimeUnit.SECONDS.sleep(5);
    Object obj = new Object();

    new Thread(() -> {
        synchronized (obj) {
            System.out.println("打印" + Thread.currentThread().getName() + "进入synchronized后的对象头:");
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }
        System.out.println("打印" + Thread.currentThread().getName() + "释放锁之后, 休眠5s前的对象头:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        try {
            // 这里休眠5秒, 防止jvm优化线程复用
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
        }
        System.out.println("打印" + Thread.currentThread().getName() + "释放锁之后, 休眠5s后的对象头:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }, "thread1").start();
    // 控制线程2执行时间
    TimeUnit.SECONDS.sleep(4);

    new Thread(() -> {
        System.out.println("打印" + Thread.currentThread().getName() + "进入synchronized之前的对象头:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        synchronized (obj) {
            System.out.println("打印" + Thread.currentThread().getName() + "进入synchronized后的对象头:");
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }
    }, "thread2").start();

    TimeUnit.SECONDS.sleep(5);
    System.out.println("打印所有线程释放锁之后的对象头:");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
运行结果:
打印thread1进入synchronized后的对象头:
05 f0 24 48 (00000101 11110000 00100100 01001000) (1210380293)
fb 01 00 00 (11111011 00000001 00000000 00000000) (507)

打印thread1释放锁之后, 休眠5s前的对象头:
05 f0 24 48 (00000101 11110000 00100100 01001000) (1210380293)
fb 01 00 00 (11111011 00000001 00000000 00000000) (507)

打印thread2进入synchronized之前的对象头:
05 f0 24 48 (00000101 11110000 00100100 01001000) (1210380293)
fb 01 00 00 (11111011 00000001 00000000 00000000) (507)

打印thread2进入synchronized后的对象头:
a0 f1 cf 3b (10100000 11110001 11001111 00111011) (1003483552)
17 00 00 00 (00010111 00000000 00000000 00000000) (23)

打印thread1释放锁之后, 休眠5s后的对象头:
01 00 00 00 (00000001 00000000 00000000 00000000) (1)
00 00 00 00 (00000000 00000000 00000000 00000000) (0)

打印所有线程释放锁之后的对象头:
01 00 00 00 (00000001 00000000 00000000 00000000) (1)
00 00 00 00 (00000000 00000000 00000000 00000000) (0)

所谓轻微竞争, 也就是说当一个线程释放锁之后, 另一个线程又会拿到锁, 一个资源存在多个线程使用, 但是不会同时有多个线程去竞争它, 这样的情况下, 偏向锁会升级为轻量级锁.

注意: 你可能会问, 为什么thread1要休眠5秒, 如果thread1不休眠5秒, 得到的thread2将会依然是偏向锁, 并且偏向同一个内核线程. 我的理解: jvm做了优化, 如果不对thread1进行休眠保活, thread2会复用之前thread1放弃掉的内核线程.

偏向锁/轻量级锁升级为重量级锁

偏向锁的篇幅中已经通过代码验证了在如下两种情况下偏向锁会升级为重量级锁

  1. 偏向锁持有状态下, 调用了锁对象原生的native hashcode()方法, 或者是System.identifyHashCode(Object obj)方法, 偏向锁将会升级为重量级锁.

  2. 偏向锁持有状态下, 调用了锁对象的wait(long timeout)方法, 偏向锁将会升级为重量级锁. (注意: 以上两种情况都是偏向锁持有状态下才会发生)

  3. 偏向锁/轻量级锁持有状态下, 有其它线程试图加锁, 偏向锁/轻量级锁都会升级为重量级锁. 换言之: 同一时刻有多个线程均想要持有这把锁, 都将会升级为重量级锁. 下面代码给出证明:

偏向锁在激烈竞争下升级为重量级锁的证明

public static void main(String[] args) throws Exception {
    TimeUnit.SECONDS.sleep(5);
    Object obj = new Object();

    new Thread(() -> {
        synchronized (obj) {
            System.out.println("打印" + Thread.currentThread().getName() + "进入synchronized后, 休眠之前的对象头:");
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
            }
            System.out.println("打印" + Thread.currentThread().getName() + "进入synchronized后, 休眠5s后的对象头:");
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }
    }, "thread1").start();
    // 控制线程2执行时间, 因为打印内存布局需要的时间要好几秒
    TimeUnit.SECONDS.sleep(4);

    new Thread(() -> {
        System.out.println("打印" + Thread.currentThread().getName() + "进入synchronized之前的对象头:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        synchronized (obj) {
            System.out.println("打印" + Thread.currentThread().getName() + "进入synchronized后的对象头:");
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }
    }, "thread2").start();

    TimeUnit.SECONDS.sleep(5);
    System.out.println("打印所有线程释放锁之后的对象头:");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
运行结果:
打印thread1进入synchronized后, 休眠之前的对象头:
05 a8 28 21 (00000101 10101000 00101000 00100001) (556312581)
0f 02 00 00 (00001111 00000010 00000000 00000000) (527)

打印thread2进入synchronized之前的对象头:
05 a8 28 21 (00000101 10101000 00101000 00100001) (556312581)
0f 02 00 00 (00001111 00000010 00000000 00000000) (527)

打印thread1进入synchronized后, 休眠5s后的对象头:
da 57 d5 20 (11011010 01010111 11010101 00100000) (550852570)
0f 02 00 00 (00001111 00000010 00000000 00000000) (527)

打印thread2进入synchronized后的对象头:
da 57 d5 20 (11011010 01010111 11010101 00100000) (550852570)
0f 02 00 00 (00001111 00000010 00000000 00000000) (527)

打印所有线程释放锁之后的对象头:
01 00 00 00 (00000001 00000000 00000000 00000000) (1)
00 00 00 00 (00000000 00000000 00000000 00000000) (0)

thread1进入synchronized之后, 休眠之前, 锁状态为101(偏向锁持有), 然后thread1开始休眠, 时间为5秒, 在第四秒之后, thread2启动了, thread2进入synchronized之前, 锁状态依然还是101(偏向锁持有, 持有者是thread1), 然后thread2进入synchronized, 尝试获取锁, 但是获取不到, thread2阻塞. thread1休眠满5秒后继续向下执行, 打印对象头发现, 锁状态已经是010(重量级锁)了, 然后thread1释放锁, thread2进去, 依然还是010(重量级锁), 当所有线程释放锁之后, 锁状态变为001(无锁).

轻量级锁在激烈竞争下升级为重量级锁的证明

很简单, 去掉开始代码的休眠5秒, 在可偏向还没启动的时候执行代码即可.

直接看运行结果:
打印thread1进入synchronized后, 休眠之前的对象头:
28 f3 ef fe (00101000 11110011 11101111 11111110) (-17829080)
f5 00 00 00 (11110101 00000000 00000000 00000000) (245)

打印thread2进入synchronized之前的对象头:
28 f3 ef fe (00101000 11110011 11101111 11111110) (-17829080)
f5 00 00 00 (11110101 00000000 00000000 00000000) (245)

打印thread1进入synchronized后, 休眠5s后的对象头:
1a 76 66 2b (00011010 01110110 01100110 00101011) (728135194)
fb 01 00 00 (11111011 00000001 00000000 00000000) (507)

打印thread2进入synchronized后的对象头:
1a 76 66 2b (00011010 01110110 01100110 00101011) (728135194)
fb 01 00 00 (11111011 00000001 00000000 00000000) (507)

打印所有线程释放锁之后的对象头:
01 00 00 00 (00000001 00000000 00000000 00000000) (1)
00 00 00 00 (00000000 00000000 00000000 00000000) (0)

分析和上面一样, 轻量级锁如预期一样升级为了重量级锁.

锁状态切换总结

  1. 偏向锁: 不存在竞争, 偏向某个线程, 后续同一个线程再次进入同步块的话, 没有加锁解锁的开销.
  2. 轻量级锁: 轻微竞争(也就是多个线程之间友好地交替执行), 通过cas获取锁, 获取成功就是轻量级锁, cas失败就会膨胀.
  3. 重量级锁: 从偏向锁持有状态到轻量级锁cas失败, 升级为重量级锁; 轻量级锁状态下继续轻量级cas失败, 升级为重量级锁. 升级期间将会创建objectMonitor对象(虽然monitor对象也有可能复用, 但是monitor最基本是基于mutex的OS互斥, 因此开销非常大).