Java Synchronized 锁的实现原理与应用 (偏向锁,轻量锁,重量锁)
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java Synchronized 锁的实现原理与应用 (偏向锁,轻量锁,重量锁)相关的知识,希望对你有一定的参考价值。
- 简介
在Java SE 1.6之前,Synchronized被称为重量级锁.在SE 1.6之后进行了各种优化,就出现了偏向锁,轻量锁,目的是为了减少获得锁和释放锁带来的性能消耗.
-
Synchroized的使用(三种形式)
(1) 对于普通同步方法,锁是当前实例对象.如下代码示例:
解释:对于set和get方法来说,都是在方法上使用了同步关键字,所以他们是同步方法,锁的就是当前的实例对象,怎么理解了,看下面的main方法,就是这个new的实例对象.所以他们的锁对象都是synchronizedMethod 这个实例.private int i = 0; public synchronized void setNum (int number) { this.i = number; } public synchronized int getNum () { return i; } public static void main (String[] args) { // 启动两个线程调用get和set方法 SynchronizedMethod synchronizedMethod = new SynchronizedMethod(); new Thread(() -> { synchronizedMethod.setNum(5); },"set").start(); new Thread(() -> { int num = synchronizedMethod.getNum(); System.out.println(num); },"get").start(); }
(2) 对于静态同步方法,锁是当前类的Class对象.看代码示例:
解释:如下两个方法都是静态同步方法.所以锁是当前类的class对象,这么理解吧,静态方法是类调用的,所以锁就是这个类对象.如下代码运行结果,只有当类的第一个静态同步方法执行完毕,第二个才能执行./** * synchronized 静态方法 */ public class SynchroizedStaticMethod { private static int i = 0; public static synchronized void addNum () { for (;;) { i++; System.out.println(Thread.currentThread().getName()+"----"+i); if(i >= 100){ break; } } } public static synchronized void getNum () { System.out.println(Thread.currentThread().getName()+"----"+i); } public static void main (String[] args) { new Thread(() -> { SynchroizedStaticMethod.addNum(); },"addNum").start(); new Thread(() -> { SynchroizedStaticMethod.getNum(); },"getNum").start(); } }
一部分执行结果
addNum----92
addNum----93
addNum----94
addNum----95
addNum----96
addNum----97
addNum----98
addNum----99
addNum----100
getNum----100
Process finished with exit code 0
(3) 对于同步代码块,锁就是Synchronized括号里面配置的对象.如下代码实例:
解释:通过如下代码可以证明锁就是括号里面的对象,当两个方法是一个对象时,只能是获取到对象锁的方法 执行,但是是两个锁对象时,那么两个方法获取的就是不同的锁对象,所以结果不一样了.
/**
* 代码块
*/
public class SynchroizedCodeBlock {
private Object object = new Object();
public void printOne () {
synchronized (object) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---" + 1);
}
}
}
public void printTwo () {
synchronized (object) {
System.out.println(Thread.currentThread().getName()+"---"+2);
}
}
public static void main (String[] args) {
SynchroizedCodeBlock codeBlock = new SynchroizedCodeBlock();
new Thread(() -> {
codeBlock.printOne();
},"printOne").start();
new Thread(() -> {
codeBlock.printTwo();
},"printTwo").start();
}
}
执行结果
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printTwo---2
Process finished with exit code 0
改变下括号里面的对象
public void printTwo () {
synchronized (this) {
System.out.println(Thread.currentThread().getName()+"---"+2);
}
}
执行结果(与第一次不一样了)
printTwo---2
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
printOne---1
Process finished with exit code 0
3.锁在什么地方(Java 对象头)
Synchronized用的锁是存在Java的对象头里的.如果对象时数组类型,则虚拟机用3个字宽存储对象头..Java对象头里的Mark Word里默认储存对象的HashCode.分代年龄和锁标记位
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashcode或锁信息等 |
32/64bit | Class Metadata Address | 存储对象数据类型的指针 |
32/64bit | Array length | 数组的长度(如果当前对象时数组) |
Mark Word 的状态变化表
4.JSE1.6对锁的优化(锁的升级与对比)
在Java SE1.6中,锁一共有4中状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
(1)偏向锁
why:在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁.
what:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里储存偏向的线程ID,以后该线程在进入和退出同步代码块时不需要进行cas操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否储存着指向当前线程的偏向锁。如果测试成功,表示该线程获得了锁。如果测试失败,则需要在测试一下Mark Word中偏向锁的表示是否设置成1(表示当前是偏向锁):如果没有设置,则使用cas竞争锁;如果设置了,则尝试使用cas将对象头的偏向锁指向当前线程。
偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其它线程尝试竞争偏向锁时,持有线程才会释放锁。偏向锁的撤销,需要等待全局安全节点(在这个时间点上没有正在执行的字节码)。
偏向锁的升级:如果有线程来竞争偏向锁,那么就需要判断对象头的Mark Word的线程ID和当前线程ID是否一致,如果不一致说明发送了竞争,那么就需要检查拥有偏向锁的线程是否还存活;如果没有存活,那么将对象头设置为无锁状态,当前线程和其它线程都可以去竞争偏向锁;如果存活,暂停拥有偏向锁的线程,遍历栈帧信息,判断这个线程是否还要使用这个锁对象,如果还需要,就撤销偏向锁,升级为轻量锁,如果不要继续使用,标记为无锁状态,重新偏向其它线程。如果升级为轻量锁后,应该还是拥有锁的线程先去执行。
(2) 轻量锁
why:轻量锁是为线程竞争不是很多,每个线程的执行时间不长而准备的,因为轻量锁发生竞争时,不阻塞线程,而是采用的自旋;如果竞争时就阻塞线程,而锁很快就释放了,这个线程的状态切换也是很大的消耗。
waht:线程在执行同步代码块前,jvm会先在当前线程的栈帧中创建一个用于存储锁记录的空间,并将对象头中的Mark Word替换为为指向锁记录的指针。如果成功,当前线程获取锁,如果失败,表示其它线程竞争锁,当前线程尝试使用自旋来获取锁。这一块其实有些绕,就是怎么判断锁这一块具体参考这篇文档
轻量锁的解锁:轻量级解锁时,会使用cas操作将disolaced Mark Word替换回到对象头,如果成功,则表示没有发生竞争。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。过程如下图所示:
(3) 锁的优缺点对比
锁 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额为的消耗,和执行非同步方法相比,仅存在纳秒级别的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问的同步块场景 |
轻量锁 | 竞争线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗cpu | 追求响应时间,同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不消耗cpu | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较长 |
以上是关于Java Synchronized 锁的实现原理与应用 (偏向锁,轻量锁,重量锁)的主要内容,如果未能解决你的问题,请参考以下文章
Java多线程系列:深入详解Synchronized同步锁的底层实现
打通JAVA与内核系列之一ReentrantLock锁的实现原理