背景
在java中,经常会用到synchronized关键字来保证线程安全,那么什么时候会存在线程安全呢?
- 共享数据的修改
- 临界资源访问
应用场景
- 修饰普通同步方法:锁
当前实例对象
; - 修饰静态同步方法:锁
当前的类Class对象
; - 修饰同步代码块:锁
Synchronized后面括号里配置的对象
,这个对象可以是任意对象;
synchronized原理
在绝大多数情况下,都只会有一个线程去访问synchronized修饰的代码块,所以synchronized在jdk1.6之后为了提升效率,优化了synchronized的机制,就是所谓的锁升级
。通过对象头
及ObjectMonitor
对象将锁划分了几个类型,其升级顺序为:无锁
->偏向锁
->轻量级锁
->重量级锁
,要了解它的原理,则必须要了解对象头。
对象头
java对象保存在内存中,由3个部分组成:
对象头
实例数据
-
对齐填充字节
这里,我们只对对象头加以说明
1、对象头的存在形式
JVM中的对象头有两种形式,它由三部分组成:
- Mark Word
- Klass Pointer(指向类的指针)
- 数组长度(只有数组对象才有)
1.1 普通对象
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
1.2 数组对象
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
2、对象头组成
2.1 MarkWord
这部分主要存储对象自身的运行时数据,如hashCode,gc分代年龄,锁标记等等。MarkWord的长度根据jvm来确认,32位的JVM的mark word为32bit,64位的mark word为64bit。当一个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
Mark Word在不同的锁状态下,存储不同,在32位JVM中是这样的:
|-------------------------------------------------------|-----------------------------------------|
| Mark Word (32 bits) | 状态 |
|----------------------------------------- -------------|-----------------------------------------|
| identity hashcode:25 | 分代年龄:4 | 是否偏向锁:1 | 锁标志位:2 |无锁 |
|------------------------------------------------------|---------------------------------------- |
| thread id:23 | epoch:2 | 分代年龄:4 | 是否偏向锁:1 | 锁标志位:2 | 偏向锁|
|-----------------------------------------------------|--------------------|
| 指向栈中锁记录的指针:30 | 锁标志位:2 | 轻量级锁|
|-------------------------------------- -------------|-------------------------------------------|
| 指向重量级锁的指针:30 | 锁标志位:2 | 重量级锁 |
|----------------------------------------------------|-------------------------------------------|
| 空 | 锁标志位:2 |GC 标记
注意:hashCode与identity hashcode并非完全是一个东西,identity hashcode是Object的hashCode
2.2 指向类的指针
该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
Java对象的类数据保存在方法区。
2.3 数组长度
只有数组对象保存了这部分数据。
该数据在32位和64位JVM中长度都是32bit。
Monitor
当锁膨胀为重量级锁时,多个线程来访问一段同步代码时,这些线程会被放到一个EntrySet集合中,处于阻塞状态的线程都会被放到该列表中。接下来,当线程获取到对象的Monitor时,Monitor是依赖于底层操作系统的mutex lock来实现互斥的,线程获取mutex成功,则会持有改mutex,这时其他线程就无法再获取到该mutex
如果线程调用了wait方法,那么该线程就会释放掉所持有的mutex,并且该线程会进入到WaitSet集合中,等待下一次被其他线程调用notify/notifyAll唤醒。如果当前线程顺利执行完毕方法,那么它也会释放掉所持有的mutex。
由于这种实现方式,Monitor是依赖底层的操作系统实现,这样存在用户态和内核态之间的切换,所以会增加性能开销。
锁升级的过程
JVM一般是这样使用锁和Mark Word的:
1、当没有被当做锁的时候,这就是个普通对象,锁标志位为01,是否偏向锁为0
2、当对象被当做同步锁时,一个线程A抢到锁时,锁标志位依然是01,是否偏向锁为1,前23位记录A线程的线程ID,此时锁升级为偏向锁
3、当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码,这也是偏向锁的意义
4、当一个线程B尝试获取锁,JVM发现当前的锁处于偏向状态,并且现场ID不是B线程的ID,那么线程B会先用CAS将线程id改为自己的,这里是有可能成功的,因为A线程一般不会释放偏向锁。如果失败,则执行5
5、偏向锁抢锁失败,则说明当前锁存在一定的竞争,偏向锁就升级为轻量级锁。JVM会在当前线程的现场栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁MarkWord中保存指向这片空间的指针。上面的保存都是CAS操作,如果竞争成功,代表线程B抢到了锁,可以执行同步代码。如果抢锁失败,则继续执行6
6、轻量级锁抢锁失败,则JVM会使用自旋锁,自旋锁并非是一个锁,则是一个循环操作,不断的尝试获取锁。从JDK1.7开始,自旋锁默认开启,自旋次数由JVM决定。如果抢锁成功,则执行同步代码;如果抢锁失败,则执行7
7、自旋锁重试之后仍然未抢到锁,同步锁会升级至重量级锁,锁标志位改为10,在这个状态下,未抢到锁的线程都会被阻塞,由Monitor来管理,并会有线程的park与unpark,因为这个存在用户态和内核态的转换,比较消耗资源,故名重量级锁