深入分析 Synchronized

Synchronized 用法

Synchronized 是 Java 提供的一个并发控制的关键字。主要有两种用法,分别是同步方法和同步代码块。

通过编译可以发现,被 Synchronized 关键字修饰的同步块前后分别生成 monitorentermonitorexit 字节码指令。monitorentermonitorexit 指令是对 lockunlock 指令的高层次实现。

这两个字节码指令都需要一个 引用类型的参数 来指明要锁定和解锁的对象。

lock/unlock 规则

  1. 一个变量同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一个线程执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才能被解锁。
  2. 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值;
  3. 如果一个变量没有被 lock 操作锁定,则不允许对其执行 unlock 操作,也不允许 unlock 一个其它线程锁定的变量;
  4. 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中,即执行 store 和 write 操作。

原子性

原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行

线程是 CPU 调度的基本单位。CPU 有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去 CPU 使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。原子性问题是由于时间片切换导致的。

通过上述规则 1,即被 Synchronized 修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在 Java 中可以使用 Synchronized 来保证方法和代码块内的操作是原子性的。

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

Java 内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程 A 改了某个变量的值,但是线程 B 不可见的情况。可见性问题是由于工作内存和主内存不一致导致的。

通过上述介绍的规则 1、2、4可知,synchronized 关键字锁住的对象,其值是具有可见性的。

有序性

有序性即程序执行的顺序按照代码的先后顺序执行

除了引入了时间片以外,由于处理器优化和指令重排等,CPU 还可能对输入代码进行乱序执行,比如 load->add->save 有可能被优化成 load->save->add 。这就是可能存在有序性问题。

这里需要注意的是,synchronized 是无法禁止指令重排和处理器优化的。也就是说,synchronized 无法避免上述提到的问题。那么,为什么还说 synchronized 也提供了有序性保证呢?如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的。

as-if-serial 语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守 as-if-serial 语义。

再由于上述规则 1、3,同一时间只能被同一线程访问,那么也就是单线程执行的。所以,可以保证其有序性。

Synchronized的实现原理

通过反编译可以发现:synchronized 同步代码块是使用 monitorentermonitorexit 指令实现的,同步方法可以看到 flags: ACC_SYNCHRONIZED 字段,JVM 采用 ACC_SYNCHRONIZED 标记符来实现同步。

无论是 ACC_SYNCHRONIZED 还是 monitorentermonitorexit 都是基于 Monitor 实现的,在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++ 实现的,由 ObjectMonitor 实现。ObjectMonitor 类中提供了几个方法,如 enterexitwaitnotifynotifyAll等。Sychronized 加锁的时候,会调用 objectMonitor 的 enter 方法,解锁的时候会调用 exit 方法。

同步方法

方法级的同步是隐式的。同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。当某个线程要访问某个方法的时候,会检查是否有 ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放

同步代码块

同步代码块使用 monitorentermonitorexit 两个指令实现。monitorenter 指令插入到同步代码块的开始位置,monitorexit 指令插入到同步代码块的结束位置,JVM 需要保证每一个 monitorenter 都有一个 monitorexit 与之相对应。

我们通过 javap 命令反编译同步代码后,发现有两个 monitorexit 指令。原因是:为了保证抛异常的情况下也能释放锁,所以 javac 为同步代码块添加了一个隐式的 try-finally,在 finally 中会调用 monitorexit 命令释放锁。

每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行 monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行 monitorexit 指令)的时候,计数器再自减。当计数器为 0 的时候。锁将被释放,其他线程便可以获得锁。

Java 对象模型

Java 对象保存在堆内存中。在内存中,一个 Java 对象包含三部分:对象头、实例数据和对齐填充。其中对象头是一个很关键的部分,因为对象头中包含锁状态标志、线程持有的锁等标志。

OOP-Klass Model

HotSpot 是基于 C++ 实现,C++ 具备面向对象基本特征的,所以 Java 中的对象表示,最简单的做法是为每个 Java 类生成一个 C++ 类与之对应。

HotSpot JVM 没有根据 Java 实例对象直接通过虚拟机映射到新建的 C++ 对象,而是设计了一个 OOP-Klass Model。OOP 指的是 Ordinary Object Pointer(普通对象指针),用来表示对象的实例信息;而 Klass 则包含元数据和方法信息,用来描述 Java 类

为什么 HotSpot 要设计 OOP-Klass Model 呢?答案是:HotSopt JVM 的设计者不想让每个对象中都含有一个 virtual table(虚函数表),所以就把对象模型拆成 Klass 和 OOP,其中 OOP 中不含有任何虚函数,而 Klass 就含有虚函数表。

为了实现 C++ 的多态,C++ 使用了一种动态绑定的技术,这个技术的核心是虚函数表。虚函数表属于类,然后类的所有对象通过虚函数表指针共享类的虚函数表。虚函数表的作用是当使用父类指针来操作子类对象时,虚函数表可以指明实际所应该调用的函数。

OOP 体系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//定义了oops共同基类
typedef class oopDesc* oop;
//表示一个Java类型实例
typedef class instanceOopDesc* instanceOop;
//表示一个Java方法
typedef class methodOopDesc* methodOop;
//表示一个Java方法中的不变信息
typedef class constMethodOopDesc* constMethodOop;
//记录性能信息的数据结构
typedef class methodDataOopDesc* methodDataOop;
//定义了数组OOPS的抽象基类
typedef class arrayOopDesc* arrayOop;
//表示持有一个OOPS数组
typedef class objArrayOopDesc* objArrayOop;
//表示容纳基本类型的数组
typedef class typeArrayOopDesc* typeArrayOop;
//表示在Class文件中描述的常量池
typedef class constantPoolOopDesc* constantPoolOop;
//常量池缓存
typedef class constantPoolCacheOopDesc* constantPoolCacheOop;
//描述一个与Java类对等的C++类
typedef class klassOopDesc* klassOop;
//表示对象头
typedef class markOopDesc* markOop;

上面列出的是整个 OOP 模块的组成结构,其中包含多个子模块。每一个子模块对应一个类型,每一个类型的 OOP 都代表一个在 JVM 内部使用的特定对象的类型。

第一个变量 oop 的类型是 oppDesc ,其是OOPS 类的共同基类型。除此之外,还有很多 instanceOopDesc、arrayOopDesc 等类型的实例,都是 oopDesc 的子类。

这些模块在 JVM 内部有着不同的用途,例如,instanceOopDesc 表示类实例,arrayOopDesc 表示数组。也就是说,当我们使用 new 创建一个 Java 对象实例的时候,JVM 会创建一个 instanceOopDesc 对象来表示这个 Java 对象。同理,当我们使用 new 创建一个 Java 数组实例的时候,JVM 会创建一个 arrayOopDesc 对象来表示这个数组对象。

oopDesc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;

private:
// field addresses in oop
void* field_base(int offset) const;

jbyte* byte_field_addr(int offset) const;
jchar* char_field_addr(int offset) const;
jboolean* bool_field_addr(int offset) const;
jint* int_field_addr(int offset) const;
jshort* short_field_addr(int offset) const;
jlong* long_field_addr(int offset) const;
jfloat* float_field_addr(int offset) const;
jdouble* double_field_addr(int offset) const;
address* address_field_addr(int offset) const;
}

在虚拟机内部,一个 Java 对象对应一个 instanceOopDesc 的对象。再来看一下 instanceOopDesc 源码:

1
2
class instanceOopDesc : public oopDesc {
}

通过上面的源码,可知 HotSpot 虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头实例数据对齐填充。其中对象头包含了两部分内容:_mark_metadata ,而实例数据则保存在 oopDesc 中定义的各种 field 中。

  • _markinstanceOopDesc 中的 _mark 成员,允许压缩。它用于存储对象的运行时记录信息,如哈希值、GC 分代年龄(Age)、锁状态标志(偏向锁、轻量级锁、重量级锁)、线程持有的锁、偏向线程 ID、偏向时间戳等。
  • _metadata_metadata 是一个共用体,其中 _klass 是普通指针,_compressed_klass 是压缩类指针。这两个指针都指向 instanceKlass 对象,它用来描述对象的具体类型。
  • 实例数据:实例数据就是在程序代码中所定义的各种类型的字段,包括从父类继承的,这部分的存储顺序会受到虚拟机分配策略和字段在源码中定义顺序的影响。
  • 对齐填充:由于 HotSpot 的自动内存管理要求对象的起始地址必须是 8 字节的整数倍,即对象的大小必须是 8 字节的整数倍,对象头的数据正好是 8 的整数倍,所以当实例数据不够 8 字节整数倍时,需要通过对齐填充进行补全。
  • 数组长度:只有数组对象包含此部分。

Klass体系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//klassOop的一部分,用来描述语言层的类型
class Klass;
//在虚拟机层面描述一个Java类
class instanceKlass;
//专有instantKlass,表示java.lang.Class的Klass
class instanceMirrorKlass;
//专有instantKlass,表示java.lang.ref.Reference的子类的Klass
class instanceRefKlass;
//表示methodOop的Klass
class methodKlass;
//表示constMethodOop的Klass
class constMethodKlass;
//表示methodDataOop的Klass
class methodDataKlass;
//最为klass链的端点,klassKlass的Klass就是它自身
class klassKlass;
//表示instanceKlass的Klass
class instanceKlassKlass;
//表示arrayKlass的Klass
class arrayKlassKlass;
//表示objArrayKlass的Klass
class objArrayKlassKlass;
//表示typeArrayKlass的Klass
class typeArrayKlassKlass;
//表示array类型的抽象基类
class arrayKlass;
//表示objArrayOop的Klass
class objArrayKlass;
//表示typeArrayOop的Klass
class typeArrayKlass;
//表示constantPoolOop的Klass
class constantPoolKlass;
//表示constantPoolCacheOop的Klass
class constantPoolCacheKlass;

oopDesc 是其他 OOP 类型的父类一样,Klass 类是其他 Klass 类型的父类。

Klass 向 JVM 提供两个功能:

  • 实现语言层面的 Java 类(在 Klass 基类中已经实现);
  • 实现 Java 对象的分发功能(由 Klass 的子类提供虚函数实现)。

总之,HotSopt JVM 的设计者把对象一拆为二,分为 Klass 和 OOP,其中 OOP 的职能主要在于表示对象的实例数据,所以其中不含有任何虚函数。而 Klass 为了实现虚函数多态,所以提供了虚函数表。(这样避免了每个对象中都含有一个虚函数表)

instanceKlass

JVM 在运行时,需要一种用来标识 Java 内部类型的机制。在 HotSpot 中的解决方案是:为每一个已加载的 Java 类创建一个 instanceKlass 对象,用来在 JVM 层面表示 Java 类instanceKlass 内部结构如下,从中可以看出,一个类含有的东西,其内部都包含有对应内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//类拥有的方法列表
objArrayOop _methods;
//描述方法顺序
typeArrayOop _method_ordering;
//实现的接口
objArrayOop _local_interfaces;
//继承的接口
objArrayOop _transitive_interfaces;
//域
typeArrayOop _fields;
//常量
constantPoolOop _constants;
//类加载器
oop _class_loader;
//protected域
oop _protection_domain;
....

重点理解)在 JVM 中,对象在内存中的基本存在形式就是 Oop。那么,对象所属的类,在 JVM 中也是一种对象,因此它们实际上也会被组织成一种 Oop,即 klassOop。同样的,对于 klassOop,也有对应的一个 klass 来描述,它就是 klassKlass,也是 klass 的一个子类。klassKlass 作为 oop 的 klass 链的端点。关于对象和数组的 klass 链大致如下图:

在这种设计下,JVM 对内存的分配和回收,都可以采用统一的方式来管理。oop-klass-klassKlass 关系如图:

内存存储

元数据—— instanceKlass 对象会存在元空间(方法区),而对象实例—— instanceOopDesc 会存在 Java 堆,Java 虚拟机栈中会存有这个对象实例的引用。

1
2
3
4
5
6
7
8
9
10
11
12
class Model{
public static int a = 1;
public int b;

public Model(int b) {
this.b = b;
}}

public static void main(String[] args) {
int c = 10;
Model modelA = new Model(2);
Model modelB = new Model(3);}

从上图中可以看到,在方法区的 instantKlass 中有一个 int a=1 的数据存储。在堆内存中的两个对象的oop中,分别维护着 int b=3,int b=2 的实例数据。和 oopDesc 一样,instantKlass也维护着一些 fields,用来保存类中定义的类数据,比如int a=1

问题:JVM 中,InstanceKlass、java.lang.Class 的关系?
ClassFileParser 将 class 文件在 runtime 解析成一个个 InstanceKlass 对象,这个对象是静态字节码文件在运行时 Metaspace 空间的一个映射。Java支持反射,为了能在 Java 层实现对定义类型的解构,JVM 实现了 InstanceKlass 的一个 java mirror 的概念—— java.lang.Class 对象。
InstanceKlass类继承自Klass类,在Klass类中有一个成员变量,并且提供了相应的Setter/Getter函数实现:

在 java.lang.Class 类中,也提供了 Class 对象与 Klass 对象的转化函数:

1
2
static Klass* as_Klass(oop java_class);
static void set_klass(oop java_class, Klass* klass);

Class 类所提供的反射机制,最终都是通过 JNI 接口,调用相应的 native 方法,然后通过 as_Klass 函数转换成 InstanceKlass 对象,得到定义类型的元数据信息。

JDK 1.8 变化部分

注意,由于 Java 8 引入了元空间(Metaspace),OpenJDK 1.8 里对象模型的实现与 1.7 有很大的不同。原先存于永久代的类信息移至元空间,因此它们的 C++ 类型都继承于 MetaspaceObj 类(定义见 vm/memory/allocation.hpp),表示元空间的数据。

OOP 体系也发生变化,如下:

1
2
3
4
5
typedef class oopDesc* oop;
typedef class instanceOopDesc* instanceOop;
typedef class arrayOopDesc* arrayOop;
typedef class objArrayOopDesc* objArrayOop;
typedef class typeArrayOopDesc* typeArrayOop;

MetaspaceObj 体系如下:

1
2
3
4
5
6
7
8
9
10
11
// The metadata hierarchy is separate from the oop hierarchy

// class MetaspaceObj
class ConstMethod;
class ConstantPoolCache;
class MethodData;
// class Metadata
class Method;
class ConstantPool;
// class CHeapObj
class CompiledICHolder;

Klass 体系:

1
2
3
4
5
6
7
8
9
10
// The klass hierarchy is separate from the oop hierarchy.

class Klass;
class InstanceKlass;
class InstanceMirrorKlass;
class InstanceClassLoaderKlass;
class InstanceRefKlass;
class ArrayKlass;
class ObjArrayKlass;
class TypeArrayKlass;

注意 Klass 代表元数据,继承自 Metadata 类,因此像 Method、ConstantPool 都会以成员变量(或指针)的形式存在于 Klass 体系中。
以下是 JDK 1.7 中的类在 JDK 1.8 中的存在形式:

  • klassOop -> Klass*
  • klassKlass 不再需要
  • methodOop -> Method*
  • methodDataOop -> MethodData*
  • constMethodOop -> ConstMethod*
  • constantPoolOop -> ConstantPool*
  • constantPoolCacheOop -> ConstantPoolCache*

其中,Klass 对象的继承关系:xxxKlass <:< Klass <:< Metadata <:< MetaspaceObj。

总结

执行 new A() 的时候,JVM native 层里会发生什么。首先,如果这个类没有被加载过,JVM 就会进行类的加载,并在 JVM 内部创建一个 instanceKlass 对象表示这个类的运行时元数据(相当于 Java 层的 Class 对象)。到初始化的时候(执行 invokespecial A::<init>),JVM 就会创建一个 instanceOopDesc 对象表示这个对象的实例,然后进行 Mark Word 的填充,将元数据指针指向 Klass 对象,并填充实例变量。

参考

  1. 深入理解多线程(二)—— Java的对象模型
  2. 深入探究 JVM | klass-oop对象模型研究

对象头

oopDesc 中的 _mark_metadata 其实就是对象头的定义。-metadata前面已经做过介绍,下面重点介绍 _mark

对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。

对 Mark Word 的设计方式上,非常像网络协议报文头:将 Mark Word 划分为多个比特位区间,并在不同的对象状态下赋予比特位不同的含义。下图描述了在 32 位虚拟机上,在对象不同状态时 Mark Word 各个比特位区间的含义。

从上图中可以看出,对象的状态一共有五种,分别是无锁态轻量级锁重量级锁GC标记偏向锁。在 32 位的虚拟机中有 2 个Bits是用来存储锁的标记为的,但是我们都知道,2 个 bits 最多只能表示四种状态:00、01、10、11,那么第五种状态如何表示呢 ,就要额外依赖 1Bit 的空间,使用 0 和 1 来区分。

在 32 位的 HotSpot 虚拟机 中对象未被锁定的状态下,Mark Word 的 32 个 Bits 空间中的 25Bits 用于存储对象哈希码(HashCode),4Bits 用于存储对象分代年龄,2Bits 用于存储锁标志位,1Bit 固定为0,表示非偏向锁。

Moniter

Monitor 是一种同步工具,也可以说是一种同步机制,主要特点是:

同一个时刻,只有一个 进程/线程 能进入 Monitor 中定义的临界区,这使得 Monitor 能够达到互斥的效果。但仅仅有互斥的作用是不够的,无法进入 Monitor 临界区的 进程/线程应该被阻塞,并且在必要的时候会被唤醒。显然,Monitor 作为一个同步工具,也应该提供这样的管理 进程/线程 状态的机制。

在 Java 中每个对象都有一个 Monitor 对象与之对应,在重量级锁的状态下,对象的 mark word 存放的是一个指针,指向了与之对应的 monitor 对象。这个 Monitor 对象就是实现重量锁的关键。注意这里说的是实现重量锁的关键,所以偏向锁、轻量锁在实现上和 Monitor 是没有关系的。

无论是 ACC_SYNCHRONIZED 还是 monitorentermonitorexit 都是基于 Monitor 机制实现的,在 Java 虚拟机(HotSpot)中,Monitor 机制是基于 C++ 的,由 ObjectMonitor 类具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

ObjectMonitor 中有几个关键属性:

  • _owner:指向持有 ObjectMonitor 对象的线程。初始时为 NULL,当有线程占有该 monitor 时,_owner 标记为该线程的唯一标识。当线程释放 monitor 时,_owner 又恢复为 NULL_owner 是一个临界资源,JVM 是通过 CAS 操作来保证其线程安全的。
  • _cxq:ContentionList,所有请求锁的线程首先会被放在这个集合中。_cxq 是一个临界资源,JVM 通过 CAS 来修改 _cxq 。修改前 _cxq 的旧值填入了 node 的 next 字段,_cxq 指向新值(新线程),因此 _cxq 是一个后进先出的 stack(栈)
  • _WaitSet:存放调用 wait() 方法的线程队列
  • _EntryList:存放处于等待锁 block 状态的线程队列_cxq 队列中有资格成为候选资源的线程会被移动到该队列中;
  • _recursions:锁的重入次数;
  • _count:用来记录该线程获取锁的次数。

其中 cxq、EntryList、WaitSet 都是由 ObjectWaiter 组成的链表结构。

  1. 假设有 A、B 两个线程竞争被 synchronized 锁住的资源,A 线程抢先拿到了锁。拿到锁的步骤为:

    • 将 MonitorObject 中的 _owner 设置成 A线程;
    • 将 mark word 设置为 Monitor 对象地址,锁标志位改为 10;
    • 将 B 线程阻塞放到 ContentionList 队列;
  2. JVM 每次从 Waiting Queue 的尾部取出一个线程放到 OnDeck 作为候选者,但是如果并发比较高,Waiting Queue 会被大量线程执行 CAS 操作,为了降低对尾部元素的竞争,将Waiting Queue 拆分成 ContentionList 和 EntryList 二个队列,JVM 将一部分线程移到 EntryList 作为准备进 OnDeck 的预备线程。

    • 所有请求锁的线程首先被放在 ContentionList 队列中;
    • ContentionList 中那些有资格成为候选资源的线程被移动到 Entry List 中;
    • 任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
    • 当前已经获取到锁资源的线程被称为 owner;
    • 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 下采用 pthread_mutex_lock 内核函数实现的)。
  3. 作为 owner 的 A 线程执行过程中,可能调用 wait() 释放锁,这个时候 A 线程进入 Wait Set , 等待被唤醒。

当线程释放锁时,会从 cxq 或 EntryList 中挑选一个线程唤醒,被选中的线程叫做 Heir presumptive,即图中的 Ready Thread,该线程被唤醒后会尝试获得锁,但 synchronized 是非公平的,所以该线程不一定能获得锁。

如果线程获得锁后调用 Object.wait 方法,则会将线程加入到 WaitSet 中,当被 Object.notify 唤醒后,会将线程从 WaitSet 移动到 cxq 或 EntryList 中。需要注意的是,当调用一个锁对象的 wait 或 notify 方法时,如当前锁的状态是偏向锁或轻量级锁则会先升级成重量级锁。

ObjectMonitor 类的重要方法

synchronized 关键字在使用的时候,往往需要指定一个对象与之关联,例如 synchronized(this),或者 synchronized(ANOTHER_LOCK),synchronized 如果修饰的是实例方法,那么其关联的对象实际上是 this,如果修饰的是类方法,那么其关联的对象是 this.class。总之,synchronzied 需要关联一个对象,而这个对象就是 ObjectMonitor。

Java 语言中的 java.lang.Object 类,便是满足这个要求的对象,任何一个 Java 对象都可以作为 monitor 机制的 ObjectMonitor。

enter
exit
wait
notify
notifyAll

总结

通过这篇文章我们知道了 sychronized 加锁的时候,会调用 objectMonitor 的 enter 方法,解锁的时候会调用 exit 方法。事实上,只有在 JDK1.6 之前,synchronized 的实现才会直接调用 ObjectMonitor 的 enterexit,这种锁被称之为重量级锁。为什么说这种方式操作锁很重呢?

  • Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到核心态,因此状态转换需要花费很多的处理器时间,对于代码简单的同步块(如被 synchronized 修饰的getset 方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说 synchronized 是 Java 语言中一个重量级锁。

所以,在 JDK1.6 中出现对锁进行了很多的优化,进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化(自旋锁在 JDK1.4 就有,只不过默认的是关闭的,JDK1.6 是默认开启的),这些操作都是为了在线程之间更高效的共享数据 ,解决竞争问题。

Java虚拟机的锁优化技术

线程状态

自旋锁

自旋锁在 JDK 1.4 中已经引入,在 JDK 1.6 中默认开启。

自旋锁指线程不放弃处理器的执行时间,等待持有锁的线程释放锁

自旋锁和阻塞锁最大的区别就是,要不要放弃处理器的执行时间。对于阻塞锁和自旋锁来说,都是要等待获得共享资源。但是阻塞锁是放弃了 CPU 时间,进入了等待区,等待被唤醒。而自旋锁是一直“自旋”在那里,时刻的检查共享资源是否可以被访问。
由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用 CPU 时间。如果线程竞争不激烈,并且保持锁的时间短,适合使用自旋锁。

锁消除

锁消除,是 JIT 编译器对内部锁的具体实现所做的一种优化。在动态编译同步块的时候,JIT 编译器可以借助一种被称为逃逸分析(Escape Analysis)的技术来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么 JIT 编译器在编译这个同步块的时候就会取消对这部分代码的同步。

锁粗化

在代码中,需要加锁的时候,我们提倡尽量减小锁的粒度,这样可以避免不必要的阻塞。但是,如果在一段代码中连续的对同一个对象反复加锁解锁,其实是相对耗费资源的,这种情况可以适当放宽加锁的范围,减少性能消耗。
当 JIT 发现一系列连续的操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的时候,会将加锁同步的范围扩散(粗化)到整个操作序列的外部。

偏向锁

大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。引入偏向锁是为了在多线程竞争的情况下尽量减少不必要的轻量级锁执行,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。上面说过,轻
量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

轻量级锁

“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,其目的是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

Synchronized和ReentrantLock的区别

  1. Synchronized 是 JVM 层次的锁实现,ReentrantLock 是 JDK 层次的锁实现;
  2. Synchronized 的锁状态是无法在代码中直接判断的,但是 ReentrantLock 可以通过 ReentrantLock#isLocked 判断;
  3. Synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的;
  4. Synchronized 是不可以被中断的,而 ReentrantLock#lockInterruptibly 方法是可以被中断的;
  5. 在发生异常时Synchronized会自动释放锁(由javac编译时自动实现),而ReentrantLock需要开发者在finally块中显示释放锁;
  6. ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;
  7. Synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁(上文有说),而ReentrantLock对于已经在等待的线程一定是先来的线程先获得锁。

参考

  1. Hollis-深入理解多线程系列
  2. farmerjohngit/myblog
  3. 就一个Synchronized,也能跟面试官扯了半个小时?