在打开这篇博客以前,我相信你对synchronized关键字是有一定的知识储备,本文主要带你强化记忆整个锁升级的过程,希望对你有帮助。当然我也相信,如果你真的理解了这些内容,你又会发现自己要学的东西还有很多!!!
文章目录
- 1、锁升级基本描述
- 2、关于锁的疑问
- 2、结合代码进行分析测试
- 2.1、测试对象头的锁情况
- 2.2、测试hashCode值
- 3、概念补充
1、锁升级基本描述
synchronized关键字是Java里面用于防止多线程下资源冲突所带来的一系列问题。使用synchronized关键字以后,我们就可以将所有的线程排好队,先拿到锁的先执行。这就可以很好的解决多线程下的资源冲突问题。
可是随着技术的发展,人们对于性能的要求越来越高,传统的排队的方式,已经无法满足我们的需求。
于是在JDK1.6以后,synchronized关键字进行了优化,且引入了无锁、偏向锁、轻量级锁、重量级锁等概念。由无锁变为重量级锁的一个过程叫做锁升级。
无锁:我们刚实例化一个对象
偏向锁:单个线程的时候,会开启偏向锁。可以使用-XX:-UseBiasedLocking来禁用偏向锁。
轻量级锁:当多个线程来竞争的时候,偏向锁会进行一个升级,升级为轻量级锁(内部是自旋锁),因为轻量级锁认为,我马上就会拿到锁,所以以自旋的方式,等待线程释放锁
重量级锁:由于轻量级锁过于乐观,结果迟迟拿不到锁,所以就不断地自旋,自旋到一定的次数,为了避免资源的浪费,就升级为我们最终的自旋锁。
2、关于锁的疑问
我们不妨大胆的猜测一下,他升级的整个过程。然后提出疑问:
- 我怎么知道我现在是什么锁?
- 锁的内部结构是怎么样的?
- 锁可以跳级吗?
回答:
1、2问:当我们实例化一个对象的时候,对象 = 对象头 + 对象实例数据(instance data) + 对齐填充(padding),对象头 = 对象运行时数据(mark word 8个字节) + 对象类型指针(class pointer 由8个字节压缩为4个字节)+ (如果是数组就还需要4个字节)。其实我们不难发现,无论对象类型的指针是否压缩,在不是数组的情况下,对象头都是占16个字节,因为8+4的时候,为了数据对齐会自动填充4位。我们的锁对应的标识,是存放在mark word里面。mark word的8个字节的详细信息,如下图所示:
3问:我们可以进行对应的参数,使我们的对象直接跳过偏向锁,进入轻量级锁。如果我们在一个实现就知道存在多线程竞争的环境中,我们使用偏向锁的意义就不是很大,因为注定了它会生成轻量级锁,甚至更高。网上也有一种说法是:一次偏向锁的撤销操作带来的性能损耗一定要小于轻量级锁自旋一次的性能损耗,不然我们为什么不在没有线程竞争的环境下,直接使用偏向锁,反正也要升级。具体的锁变化的过程,如下图所示:
这两张图建议记住
2、结合代码进行分析测试
其实前面的讲解,我们对锁升级已经有了一个比较深刻的理解了,但是为了强化记忆,我们还是实际上手一下代码,毕竟空洞的文字容易忘记
此处我们需要引入Java Object Layout这个工具,它是用于打印对象的布局的,使用方式,直接引入对应的maven依赖:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
2.1、测试对象头的锁情况
测试代码:
package pers.mobian.synchronizedtest;
import org.openjdk.jol.info.ClassLayout;
public class Test01 {
public static void main(String[] args) {
Object o = new Object();
//打印加锁之前的字节信息
System.out.println(ClassLayout.parseInstance(o).toPrintable());
//为了控制变量,我们再打印一次信息
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
//打印加锁之后的字节信息
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
测试结果:
通过图片,我们不难发现,加锁以前的字节信息和加锁之后的字节信息是发生了改变的。并且变化是在前8个字节,8-11位的字节码没有变化,那么就可以确定我们前面说的,加锁实际上是改变了我们对象头中对象运行时数据(mark word)那8个字节
2.2、测试hashCode值
根据前面的测试,如果我们不显式的调用hachCode,该空间的值是不会发生变化的
测试代码:
package pers.mobian.synchronizedtest;
import org.openjdk.jol.info.ClassLayout;
public class Test01 {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
//显式调用hashCode方法
o.hashCode();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
执行结果:
我们的8-11位的字节依然没有变化。0-7在没有加锁的情况下,还是变化了,这是由于我们显式的调用了hashCode方法,继而修改了相应的值。通过这个实验我们也能够确定hashCode的计算也是放在对象头的mark word中,且需要显式调用才能有变化
3、概念补充
1、为什么有自旋锁还需要重量级锁?
自旋会不断的消耗CPU资源,如果锁的时间长,或者自旋线程多,CPU会被大量消耗。到了重量级锁以后,由于重量级锁有等待队列,拿不到锁的进入等待队列,不需要消耗CPU资源
2、偏向锁是否一定比自旋锁效率高?
不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁,JVM启动过程,会有很多线程竞争,所以默认情况启动时不打开偏向锁,过一段儿时间再打开。
3、不同锁的不同标志形式
锁状态 | 偏向锁位 | 锁标志位 |
无锁状态 | 0 | 0 1 |
偏向锁 | 1 | 0 1 |
轻量级锁 | 无 | 0 0 |
重量级锁 | 无 | 1 0 |
4、轻量级锁也叫自旋锁和无锁
自旋锁我们很好理解,轻量级锁内部不断地自旋,以求获取到对应的锁资源。这里的无锁,我们可以理解为,因为这个锁它在不断的自旋,没有真正的拿到锁,所以也可以叫做无锁。
5、轻量级锁的相关补充
自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
适应性自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
6、synchronized重量级锁的相关补充
synchronized包含6个核心组件:Wait Set、Contention List、Entry List、OnDeck、Owner、!Owner
- Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
- Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
- OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
- Owner:当前已经获取到所资源的线程被称为 Owner;
- !Owner:当前释放锁的线程。
- JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程。
- Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
- Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”。
- OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify或者 notifyAll 唤醒,会重新进去 EntryList 中。
- 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。
- Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁
- 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
- synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的(程序在操作系统的内核态与用户态之间切换),有可能给线程加锁消耗的时间比有用操作消耗的时间更多。
- JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。
这个过程也完完全全说明了synchronized是一个非公平的锁