synchronized看这一篇就够了
关联文章
一、synchronized介绍
synchronized java关键字,用于解决多个线程对同一资源的访问和修改的线程安全问题。
线程安全是并发编程中的主要关注点,主要原因是存在多条线程共同操作共享数据造成成。为了解决这个问题我们需要保障同一资源在同一时刻有且只有一个线程在操作,其他线程必须等到该线程处理完数据后再进行,这种方式就叫做互斥锁,而synchronized本质上就是一个互斥锁。
二、synchronized的三种应用方式
- 在静态方法上加锁;
- 在非静态方法上加锁;
- synchronized代码块
public class Sample {
private static int count = 0;
//非静态方法
public synchronized void noStatic() {
count++;
}
//静态方法
public static synchronized void isStatic() {
count++;
}
public void codeBlock() {
//代码块
synchronized (this) {
count++;
}
}
}
三种作用范围的区别实际是被加锁的对象的区别,请看下表:
作用范围 | 锁对象 |
---|---|
非静态方法 | 当前对象 => this |
静态方法 | 类对象(class对象) => SynchronizedSample.class |
代码块 | 指定对象 |
三、synchronized底层语义原理
了解 synchronized 的实现原理,需要理解二个预备知识:
1、预备知识一:需要知道 Java 对象头,锁的类型和状态和对象头的Mark Word息息相关;
synchronized的实现 和 对象头信息息息相关。我们来看下对象的结构:
-
实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
-
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。
而对象头则是实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构如下:
组成解释:
虚拟机位数 | 头对象结构 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode、锁信息或分代年龄或GC标志等信息 |
32/64bit | Class Metadata Address | 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。 |
对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额 外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。
2、预备知识二:需要了解 Monitor ,每个对象都有一个与之关联的Monitor 对象;Monitor 对象属性如下:
//👇图详细介绍重要变量的作用
ObjectMonitor() {
_header = NULL;
_count = 0; // 重入次数
_waiters = 0, // 等待线程数
_recursions = 0;
_object = NULL;
_owner = NULL; // 当前持有锁的线程
_WaitSet = NULL; // 调用了 wait 方法的线程被阻塞 放置在这里
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 等待锁 处于block的线程 有资格成为候选资源的线程
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
对象关联的 ObjectMonitor 对象有一个线程内部竞争锁的机制,如下图所示:
四、synchronized 是公平锁 or 非公平锁
synchronized是非公平锁。
- Synchronized 在线程竞争锁时,首先做的不是直接进ContentionList 队列排队,而是尝试自旋获取锁(可能ContentionList 有别的线程在等锁),如果获取不到才进入 ContentionList,这明显对于已经进入队列的线程是不公平的;
- 另一个不公平的是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源
五、synchronized 底层实现源码
我们把文章开头的示例代码编译成class 文件,然后通过javap -v Sample.class。
如图:
synchronized 在代码块上是通过 monitorenter 和 monitorexit指令实现,在静态方法和 方法上加锁是在方法的flags 中加入 ACC_SYNCHRONIZED 。JVM 运行方法时检查方法的flags,遇到同步标识开始启动前面的加锁流程,在方法内部遇到monitorenter指令开始加锁。
锁变动描述
synchronized 锁状态从无锁状态到偏向锁
- 假如A 线程访问同步代码块,使用CAS 操作将 Thread ID 放到 Mark Word 当中
- 如果CAS 成功,此时线程A 就获取了锁
- 如果线程CAS 失败,证明有别的线程持有锁,这个时候启动偏向锁撤销
- 偏向锁撤销:
- 让 A线程在全局安全点阻塞(类似于GC前线程在安全点阻塞)
- 遍历线程栈,查看是否有被锁对象的锁记录( Lock Record),如果有Lock Record,需要修复锁记录和Markword,使其变成无锁状态
- 恢复A线程
- 将是否为偏向锁状态置为 0 ,开始进行轻量级加锁流程
轻量级锁怎么升级为重量级锁
当锁升级为轻量级锁之后,如果有新线程过来竞争锁,首先新线程会自旋尝试获取锁,尝试一定次数(默认10次)后依然没有拿到,锁就会升级成重量级锁。
一般来说,同步代码块内的代码应该很快就执行结束,这时候新线程自旋一段时间是很容易拿到锁的,但是如果达到默认次数还没拿到,自旋本身其实就是死循环,很耗CPU的,因此就直接转成重量级锁咯,这样就不用了线程一直自旋了。