一、作用
保证同一时刻只有一个线程可以执行某个方法或代码块(原子性、有序性),同时可保证一个线程对共享变量的操作可以被其他线程及时看到。
二、概念
1. JMM(Java Memory Model)
JMM 用于屏蔽掉各种硬件和操作系统内存的访问差异,以实现 Java 程序在各种平台上都能达到一致的并发效果。JMM 规范了 Java 虚拟内存如何与计算机内存协同工作:规定了一个线程如何和何时可以看到其他线程修改过的共享变量的值,以及在必须时如何同步地访问共享变量。
2. JVM 中对象的内存布局
(1)对象头(Header)
对象头包含2部分信息:1)存储对象自身的运行时数据(Mark Word),如 HashCode、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳灯,这部分数据的长度在32位和64位的虚拟机中分别为
32bit
、64bit
;2)类型指针,即指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。该部分数据不是必须的,因为查找对象的元数据信息不一定要经过对象本身。
以下是32位 JVM 下,Mark Word 的默认存储结构(无锁状态)。
32位 JVM 下,其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的 Mark Word 的存储结构如下。
重量级锁也就是 synchronized 对象锁,锁标志位为 10,其指针指向的是
monitor
对象的起始地址,每个对象都存在一个 Monitor 与之关联,对象与 monitor 之间的关联有多种实现方式,monitor 可以与对象一起创建销毁,或当线程试图获取对象锁时创建。
Monitor
Monitor 的数据结构中,主要包含
_WaitSet
保存 ObjectWaiter 列表(每个等待锁的线程都会被封装成 ObjectWaiter 对象)。当在 synchronized 代码块/方法中调用了wait()
后,线程等待被唤醒,进入 WaitSet_EntryList
保存 ObjectWaiter 列表。当多个线程想要访问锁时,首先会进入 _EntryList。owner
指向持有 ObjectMonitor 对象的线程count
进入计数器
当多个线程想要访问锁时,首先会进入 _EntryList。当线程获取到 Monitor 时,_owner 设置为当前线程,count 加1。若当前线程调用wait()
,将释放 monitor,owner 恢复为 null,count 减1,同时线程进入 WaitSet,等待被唤醒。若当前线程执行完毕,则释放 monitor,owner 恢复为 null,count 置为0。
(2)实例数据(Instance Data)
无论是从父类继承下来的,还是子类中定义的,都需要记录下来。
(3)对齐填充(Padding)
该数据不是必然存在的。JVM 要求对象起始地址必须是8字节的整数倍。
3. 线程安全
线程安全:多个线程同时修改共享变量时,会导致某些线程对数据的修改丢失
所以如何避免线程安全问题:
- 保证共享资源在同一时刻只能由一个线程进行操作(原子性、有序性)
- 将线程操作的结果及时刷新,保证其他线程可以立即获取到修改后的最新数据(可见性)
4. 等待唤醒机制与 synchronized
在使用
notify()/notifyAll()
和wait()
时,必须处于 synchronized 代码块或 synchronized 方法中,否则会抛出IllegalMonitorStateException
异常。
因为,调用这几个方法前要求必须拿到当前对象的 monitor,而 synchronized 关键字可以获取 monitor。
三、使用
1. 修饰实例方法
对当前实例进行加锁
2. 修饰静态方法
对当前类对象进行i加锁
3. 修饰代码块
对当前对象进行加锁
四、synchronized 实现同步的原理
1. 修饰代码块
同步代码块实现使用的是
monitorenter
和monitorexit
指令实现同步,monitorenter
指向代码块开始的位置,monitorexit
指向代码块结束的位置。
当一个线程试图获取对象objectref
的monitor
的持有权时,
若 monitor 的进入计数器为0
成功获取,并将 count=1若当前线程已拥有 monitor 的持有权
则可重入,count 值加1若其他线程正在持有
objectref
的monitor
的持有权
当前线程被阻塞,知道其他线程调用完成(将 monitor 释放,count 设置为0)
编译器会保证每条
monitorenter
指令,都有对应的monitorexit
指令,不论代码块是异常结束还是正常结束。
2. 修饰方法
修饰方法时无需通过字节码指令来控制同步。JVM 从方法常量池的方法表结构中的
ACC_SYNCHRONIZED
标志位来判断该方法是否为同步方法。
当调用方法时
若设置了该标志位,则线程试图去获取 monitor 的持有权当方法返回时
释放 monitor
五、评价
参考文献
深入理解Java并发之synchronized实现原理
https://zhuanlan.zhihu.com/p/29881777