内置锁使用
通常我们说的 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 机制来实现锁的获取、释放。会在代码快前后增加 monitorenter
、monitorexit
指令。
**锁定对象不能是 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 默认延迟加载偏向锁。