内置锁使用

通常我们说的 java 内置锁默认都是指的 JVM 给我们提供的 synchronized 关键字实现的锁。 下面是一个简单的例子:

public class SynchronizedVariableTest1 {

    public static void main(String[] args) throws InterruptedException {
        SynchronizedVariableTest1 test = new SynchronizedVariableTest1();
        synchronized (test) {
            System.out.println(1);
        }
    }
}
复制代码

对象加锁

我们可以通过 javap命令查看字节码文件,或者通过 idea 的jclasslib Bytecode Viewer插件进行查看字节码指令信息,如下图所示:

我们看到 synchronized 底层是使用 monitor 机制来实现锁的获取、释放。会在代码快前后增加 monitorentermonitorexit 指令。 ​

**锁定对象不能是 null , 如果是 null 程序运行的时候会提示 ****NullPointerException**空指针异常。

Object lock = null;
synchronized(lock) {
    System.out.println(100);
}

// 结果: 
// Exception in thread "main" java.lang.NullPointerException
//	at cn.xyz.juc.synchronized1.status.CleanLockTest.main(CleanLockTest.java:16)
复制代码

方法加锁

同样,如果是在方法上增加 synchronized关键字(由于jclasslib Bytecode Viewer查看方法信息不是很方便,下面我就通过 javap -verbose指令来进行演示),会在方法的 flags 上增加 ACC_SYNCHRONIZED关键字,原方法代码如下:

public synchronized void test() {
}
复制代码

编译后的字节码, 执行指令 javap -verbose xxxx.class

  public synchronized void test();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: iconst_1
         4: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
        line 14: 21
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      22     0  this   Lcn/xyz/juc/synchronized1/SynchronizedVariableTest2;

复制代码

对于 .class字节码文件的解析和分析可以参考这篇掘金文章: JVM 字节码指令解析 。 ​

内置锁状态存储

内置锁的状态是存储到 Java 对象的对象头中,对象头的存储结构,以及对象内存分配可以参考我的这篇文章: 基于 Hostpot 虚拟机的 Java 对象揭秘 。本文不再赘述。

Mark Word (64bit)

为了方便下文阅读,我吧 mark word 再贴一次到本文中

Java 管程

Java 虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是采用管程(Monitor, 更常见的是直接称为 “锁”)来实现的。

Java 采用的是管程技术,synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分。而管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。但是管程利用OOP的封装特性解决了信号量在工程实践上的复杂性问题,因此java采用管理机制。

MESA 模型

管程模型,分别是:Hasen 模型、Hoare 模型和 MESA 模型。其中,现在广泛应用的是 MESA 模型,并且 Java 管程的实现参考的也是 MESA 模型。

在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题,管程都是能够解决的。

ObjectMonitor

ObjectMonitor 在 jvm 中的定义信息如下:

  // 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;      // 存储锁对象
    _owner        = NULL;      // 标识拥有该 monitor 的线程(当前获取锁的线程) 
    _WaitSet      = NULL;      // 等待线程(调用 waite) 组成的双向循环链表 _WaitSet 是第一个节点
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;      
    _succ         = NULL ;
    _cxq          = NULL ;     // 多线程竞争锁会先存储到这个单向链表中(FIFO)结构
    FreeNext      = NULL ;     // 存放在进入或者重新进入时被阻塞(Blocked)的线程(也就是竞争失败的线程)
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }
复制代码

内置锁状态

内置锁分为 4 个状态,分别是:无锁,偏向锁,轻量级锁,重量级锁。在锁竞争的过程中会进行一个正向的锁升级过程。

锁升级过程

说明,锁升级状态是不可逆转的。

无锁状态

实验代码:

public class NoSynchronizedTest {

    public static void main(String[] args) {
        NoSynchronizedTest test = new NoSynchronizedTest();
        System.out.println("无锁状态 +++++++++");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());
    }
}
复制代码

输出结果

无锁状态 +++++++++
cn.xyz.juc.synchronized1.NoSynchronizedTest 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)                           05 c0 00 f8 (00000101 11000000 00000000 11111000) (-134168571)
     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: 对应内存中当前存储的值,二进制 32 ;

指针压缩

打印的结果我们可以看到,对象总大小位 16 字节,前 12 字节为对象头(我本地 jdk 1.8 默认开启指针压缩),后面 4 字节为对齐填充。 ​

可以通过一下参数进行关闭:

-XX:-UseCompressedOops
复制代码

我再执行一次,对象总大小 16 bytes, 前 8 bytes 是 mark word , 后 8 bytes 表示 kclass point

匿名偏向

当 JVM 启用了偏向锁模式(JDK 6默认开启),创建新的 Mark Word 的 Thread Id 为 0, 说明此时处于可偏向但是并未偏向任何线程,也叫做匿名偏向状态(anonymously biased)

偏向锁状态

偏向锁延迟偏向

**偏向锁模式存在偏向锁延迟机制: **Hostpost 虚拟机再启动后有一个几秒(默认 4 秒)的延迟才对妈给新对象进行开启偏向锁模式。 JVM 启动时会进行一系列的对象创建过程。在这个过程中大量 synchronized 关键字对对象加锁,这些锁多数都不是偏向锁。为了减少初始化时间, jvm 默认延迟加载偏向锁。 ​

JVM 参数:

//关闭延迟开启偏向锁 
‐XX:BiasedLockingStartupDelay=0 

//禁止偏向锁
‐XX:‐UseBiasedLocking

//启用偏向锁
‐XX:+UseBiasedLocking
复制代码

测试代码(注意,这里需要注意的是需要创建两个对象,因为对象头信息的初始化是在 new关键字执行的时候初始化,笔者之前就遇到了这样的问题,导致实验失败):

public class BiasedLockDelayTest {

    public static void main(String[] args) throws InterruptedException {
        Object obj1 = new Object();
        System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
        System.out.println();
        Thread.sleep(5000);
        Object obj2 = new Object();
        System.out.println(ClassLayout.parseInstance(obj2).toPrintable());
    }
}
复制代码

结果打印如下,延迟 5 秒后新创建的都默认开启匿名偏向, threadid = 0 。

偏向锁偏向状态跟踪

下面代码主要演示了对象从无锁,到偏向锁的过程。代码如下:

log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
Thread.sleep(4000);
Object obj1 = new Object();
log.debug(ClassLayout.parseInstance(obj1).toPrintable());
new Thread(new Runnable() {
    @Override
    public void run() {
        log.debug("start ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
        synchronized (obj1) {
            log.debug("lock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
        }
        log.debug("unlock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
    }
}, "thread0").start();

Thread.sleep(5000);
log.debug(ClassLayout.parseInstance(obj1).toPrintable());
复制代码

打印结果如下:

偏向锁状态调用 hashcode 方法

调用锁对兑现的 obj.hashCode() 或者 System.identityHashCode(obj) 方法时候会导致该对象的偏向锁被撤销。因为一个对象,其 hashcode 只会被生成一次并且保存,偏向锁是没有地方存储 hashcode 的

  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode

当对象处于可偏向(也就是线程 ID 为0)和已偏向的状态下,调用 hashCode 计算会将使对象也无法偏向:

  • 当对象可偏向时, MarkWord 将编程未锁定状态,并且只能升级成轻量级锁
  • 当对象正处于偏向锁时, 调用 hashCode 使偏向锁强制升级为重量级锁

实验代码:

偏向锁内调用,共享对象 obj1 的 hashCode 方法,锁被升级为重量级锁。

偏向锁状态调用 wait/notify

偏向锁状态执行 obj.notify 会升级为轻量级锁。

调用 obj.wait(timeout) 会升级为重量级锁。

轻量级锁状态

如果偏向锁失败, 虚拟机并不会升级为重量级锁,它还会尝试使用一种称为轻量锁的优化手段,此时 Mark Word 的结构也会变成轻量级锁的结构,轻量级锁所适应的场景就是线程交替执行同步块的场合,如果存在同一个时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。

偏向锁升级为轻量级锁

@Slf4j
public class LightweightLockTest {

    public static void main(String[] args) throws InterruptedException {
        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        Thread.sleep(5000);
        Object obj1 = new Object();
        log.debug(ClassLayout.parseInstance(obj1).toPrintable());
        new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                log.debug("start ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                synchronized (obj1) {
                    log.debug("lock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                }
                log.debug("unlock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
            }
        }, "thread0").start();

        Thread.sleep(1000);

        new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                log.debug("start ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                synchronized (obj1) {
                    log.debug("lock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                }
                log.debug("unlock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
            }
        }, "thread1").start();

        Thread.sleep(5000);
        log.debug(ClassLayout.parseInstance(obj1).toPrintable());
    }
}
复制代码

打印日志:

由于日志过长无法标记,我就简单的画一个流程图吧

轻量级锁升级为重量级锁

需要制造竞争激烈的场景,我们可以通过线程池的方式来模拟,代码如下:

@Slf4j
public class LightweightLockTest {

    public static void main(String[] args) throws InterruptedException {
        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        Thread.sleep(5000);
        Object obj1 = new Object();
        log.debug(ClassLayout.parseInstance(obj1).toPrintable());

        ExecutorService executeService = Executors.newFixedThreadPool(2);
        executeService.submit(new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                log.debug("start ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                synchronized (obj1) {
                    log.debug("lock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                }
                log.debug("unlock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
            }
        }, "thread0"));

        //Thread.sleep(1000);

        executeService.submit(new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                log.debug("start ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                synchronized (obj1) {
                    log.debug("lock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
                }
                log.debug("unlock ... \r\n" + ClassLayout.parseInstance(obj1).toPrintable());
            }
        }, "thread1"));

        Thread.sleep(5000);
        log.debug(ClassLayout.parseInstance(obj1).toPrintable());
    }
}
复制代码

我们再来看看结果: 第一个线程获取锁过后,锁由偏向锁升级为轻量级锁

第二个线程已经采用的是重量级锁。 �说明:为什么有时候没有升级成重量级锁?这个可能是当是 CPU 资源比较空闲,计算逻辑处理能力比较强,不需要进行锁升级,大家可以尝试多增加几个线程参与锁的争抢或者该程序多运行几次。

锁的状态转化

偏向锁、轻量级锁的状态转化及对象 Mark Word 的关系转换入下图所示:

原文链接:https://juejin.cn/post/7055665818176061453