synchronized使用场景
Synchronized主要两种使用方式:修饰方法与修饰对象
修饰方法
// 非静态方法
// Synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。
public synchronized void funA(){
System.out.println("hello world");
}
// 修饰静态方法
// Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。
public synchronized static void funD(){
System.out.println("hello world");
}
修饰对象
// 修饰对象
public void funB(){
synchronized (this){
System.out.println("hello world");
}
}
// 修饰类对象
public void funC(){
synchronized (SynchronizedDemo.class){
System.out.println("hello world");
}
}
synchronized原理
Synchronized不论是修饰方法还是代码块,都是通过持有修饰对象的锁来实现同步(类本身也是一个对象)
对象内存布局
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:
对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。与synchronized相关的结构主要是对象头。
HotSpot虚拟机对象的对象头部分包括两类信息:
- Mark Word(标记字段):存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特。
- Klass Point(类型指针):对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。
在64位系统中,Mark Word结构如下所示:
锁的升级
在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。
JVM 对此进行了大刀阔斧地改进,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
偏向锁
当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID。
因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;
如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;
如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
轻量级锁
线程1获取轻量级锁时,会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中,创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容更新为线程1存储的锁记录(DisplacedMarkWord)的地址;
如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。
长时间的自旋会消耗大量的CPU资源,在一定条件下,轻量级锁就会膨胀为重量级锁,这里的一定条件指的是:
- 自旋的次数超过限制,这个限制通过JVM参数-XX:PreBlockSpin设置,默认为10。
- 线程2自旋等待期间,有线程3竞争锁对象。
重量级锁
重量级锁是传统意义上的锁,非公平,可重入,所有未获取锁资源的线程都将被阻塞。