这里只是将自己对synchronized锁的操作demo做一个记录。
在1.6之前,锁都是重量级锁,即我们不管什么线程来操作资源,都要进行加锁释放锁,如果有多线程,还要等待之类的,很浪费资源,1.6之后引入了偏向锁与轻量锁来减小获取和释放锁所带来的性能消耗。
锁升级其实就是对synchronized的优化,以前用synchronized修饰一个对象或者是方法,方法也等于是锁住对象,直接用一把操作系统层面的大锁,万一只有少量线程的话会大题小作了,如果大量线程的话又会特别消耗时间,划不来,所以要将以前的二话不说用一把大锁进行优化。
锁升级的过程是 无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁
锁的状态看对象的markword
其中 Mark Word 用于存储对象的哈希码(hashCode)、GC分代年龄、锁状态标记、线程持有的锁、偏向线程ID、偏向时间戳等信息,Mark Word 会根据对象的状态复用自身的存储空间,也就是说在运行期间 Mark Word 里存储的数据会随着锁标志位的变化而变化的。详情图如下:
也就是说我们只要查看对象的Mark Word就可以查看到当前是什么锁状态。
demo:可以使用一个工具 JOL 来查看底层的内存标志位变化
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
<scope>compile</scope>
</dependency>
无锁状态:
在没有线程争用的情况下,对象并未被任何锁锁定。此时,所有线程可以自由地对对象进行读写操作,无需经过任何同步控制。
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
class T{
Integer age;
public void setAge(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
}
T o = new T();
System.out.println("无锁状态:" + ClassLayout.parseInstance(o).toPrintable());
}
}
输出结果:状态为001,则为无锁状态
偏向锁:
一个线程反复的去获取/释放一个锁,如果这个锁是轻量级锁或者重量级锁,不断的加解锁显然是没有必要的,造成了资源的浪费。于是引入了偏向锁,偏向锁在获取资源的时候会在资源对象上记录该对象是偏向该线程的,偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断该资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进入同步操作。
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000); //jdk中偏向锁存在4秒启动,也就是说jvm启动后创建的对象才会开启偏向锁
class T{
Integer age;
public void setAge(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
}
T o = new T();
System.out.println("偏向锁");
synchronized (o){
o.setAge(12);
System.out.println("偏向锁状态1 " + ClassLayout.parseInstance(o).toPrintable());
}
synchronized (o){
o.setAge(13);
System.out.println("偏向锁状态2 " + ClassLayout.parseInstance(o).toPrintable());
}
}
输出结果:状态为101,则为偏向锁
轻量级锁:
当两个或以上线程交替获取锁,但并没有在对象上并发的获取锁时,偏向锁升级为轻量级锁。在此阶段,线程采取CAS的自旋方式尝试获取锁,避免阻塞造成的cpu在用户态和内核态间转换的消耗。
public static void main(String[] args) throws InterruptedException {
Thread.sleep(4100); //jdk中偏向锁存在4秒启动,也就是说jvm启动后创建的对象才会开启偏向锁
class T{
Integer age;
public void setAge(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
}
T o = new T();
synchronized (o){
o.setAge(12);
System.out.println("偏向锁状态1 " + ClassLayout.parseInstance(o).toPrintable());
}
synchronized (o){
o.setAge(13);
System.out.println("偏向锁状态2 " + ClassLayout.parseInstance(o).toPrintable());
}
System.out.println("轻量级锁");
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (o){
o.getAge();
System.out.println("轻量级锁状态 " + ClassLayout.parseInstance(o).toPrintable());
}
}
});
thread.start();
thread.join();
System.out.println("无锁状态 " + ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
System.out.println("轻量级锁状态 " + ClassLayout.parseInstance(o).toPrintable());
}
}
主线程首先对O对象进行加锁,首次加锁为101偏向锁,子线程等待主线程释放锁后,对O对象加锁,这时将偏向锁升级为00轻量级锁。
轻量级锁解锁后,o对象无线程竞争,恢复为001无锁状态,并且处于不可偏向状态。如果之后有线程再尝试获取o对象的锁,会直接加轻量级锁,而不是偏向锁。
输出结果:(前面偏向锁就不截图了)状态为00,且后续主线程加锁,直接升级为轻量级锁
重量级锁:
随着竞争加剧,或者自旋尝试达到一定次数后,JVM会将锁升级为重量级锁。此时,线程会调用操作系统互斥量(Mutex)创建一个ObjectMonitor结构体,将线程放入EntryList队列中等待,同时可能触发线程调度和上下文切换。持有锁的线程释放锁时,会唤醒等待队列中的下一个线程,确保临界区资源的有序访问。
两个或以上线程并发的在同一个对象上进行同步时,为了避免无用自旋消耗cpu,轻量级锁会升级成重量级锁。这是mark word中的指针指向的是monitor对象的起始地址。
public static void main(String[] args) throws InterruptedException {
Thread.sleep(4100); //jdk中偏向锁存在4秒启动,也就是说jvm启动后创建的对象才会开启偏向锁
class T{
Integer age;
public void setAge(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
}
T o = new T();
synchronized (o){
o.setAge(12);
System.out.println("偏向锁状态1 " + ClassLayout.parseInstance(o).toPrintable());
}
synchronized (o){
o.setAge(13);
System.out.println("偏向锁状态2 " + ClassLayout.parseInstance(o).toPrintable());
}
System.out.println("轻量级锁");
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (o){
o.getAge();
System.out.println("轻量级锁状态 " + ClassLayout.parseInstance(o).toPrintable());
}
}
});
thread.start();
thread.join();
System.out.println("无锁状态 " + ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
System.out.println("轻量级锁状态 " + ClassLayout.parseInstance(o).toPrintable());
}
//此时主线程还没有结束,但是此时下面的重量级锁1和主线程是交替使用o的,所以认为重量级锁1是一个轻量级锁
//所以我们可以在线程一睡三秒,此时线程一占用着资源,线程二只能等待,
System.out.println("重量级锁");
new Thread(new Runnable() {
@Override
public void run() {
synchronized (o){
try {
System.out.println("睡眠了三秒");
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("轻量级-->重量级锁1 "+ ClassLayout.parseInstance(o).toPrintable());
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (o){
System.out.println("重量级锁2 "+ ClassLayout.parseInstance(o).toPrintable());
}
}
}).start();
}
输出结果:(上面轻量级锁就不截图了)状态为10,由轻量级升级为重量级锁
总结来说,Java中synchronized锁的升级过程是一个动态优化的过程,它根据线程竞争情况调整锁的状态,尽量降低锁的开销,提高系统并发性能。理解这一过程对于编写高效且安全的多线程代码至关重要。随着JDK版本的不断迭代,锁优化机制也在不断完善,例如引入了适应性自旋、锁消除、锁粗化等技术,使得Java的并发环境变得更加智能和高效。
关于锁升级其实挺简单的,但是实操的时候其实蛮多疑问的,就是Thread.sleep()如果放在New T()下面,则本来无锁的会变成偏向锁,偏向锁变成轻量级锁,难道跟创造对象有关?欢迎各位大佬来解答,谢谢!!!
参考文章链接:
深入理解 synchronized 的锁升级 - 掘金 (juejin.cn)
关于状态位倒数第三位的详解: