synchronized原理、偏向锁、轻量级锁、重量级锁、锁升级

本文详细探讨了Java中的synchronized关键字,包括其概念、基本使用、原理,以及各种锁(偏向锁、轻量级锁、重量级锁)的工作机制和升级场景。还介绍了synchronized的优化策略,如自旋优化、锁粗化和锁消除,旨在提高多线程环境下的性能。
摘要由CSDN通过智能技术生成

Synchronized

概念

自增自减字节码指令

我们知道自增自减操作不是原子性的,一行代码它为四条指令

getstatic i // 获取静态变量i的值 
iconst_1 // 将int常量1压入操作数栈
iadd // 自增    自减指令是isub
putstatic i // 将修改后的值存入静态变量i中

既然不是原子操作,那么就有可能在最后一步取出操作数栈结果之前进行了线程上下文切换,进而导致线程安全问题



临界区

多个线程对共享资源进行读写操作就会有并发安全问题。

临界区:一段代码对共享资源进行读写操作,这段代码称为临界区

临界资源:共享资源称为临界资源

//临界资源
private static int counter = 0;

public static void increment() {
    //临界区
    counter++;
}

public static void decrement() {
   //临界区
    counter--;
}


竞态条件

多个线程对共享资源有竞争,那么也就有竞态条件

竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果也无法预测,称为发生了竞态条件

避免临界区中竞态条件发生:

  • 阻塞式解决方案:加锁
  • 非阻塞式解决方案:CAS原子变量


基本使用

synchronized如果锁对象是类class对象,它是不存在偏向锁的。

在这里插入图片描述

private static String lock = "";

public static void increment() {
   
    synchronized (lock){
   
        counter++;
    }
}

public static void decrement() {
   
    synchronized (lock) {
   
        counter--;
    }
}

接下来的一个执行流程时序图如下所示

在这里插入图片描述



原理

在JDK1.5之前,synchronized是基于Monitor机制实现的,其实就是管程。依赖底层操作系统的互斥原语Mutex互斥量,所以就涉及到用户态到内核态的切换。是重量级锁,性能较低。

在JDK1.5之后,添加了锁粗化、锁消除、轻量级锁、偏向锁、自适应自旋等技术减少锁操作的开销。


同步方法是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;同步代码块是通过monitorenter和monitorexit来实现。

两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。



查看synchronized的字节码指令序列

首先是synchronized添加在方法上,通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现

在这里插入图片描述

在这里插入图片描述


接下来是同步代码块方式,通过monitorenter和monitorexit来实现

在这里插入图片描述



Monitor

monitor,翻译是监视器,在操作系统层面叫管程,管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。


在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在正在广泛使用的是MESA模型

在这里插入图片描述


Java语言的内置管程synchronized

java语言内置的管程(synchronized)参考了MESA管程模型,并对它进行了精简。在MESA中条件变量有多个,而java语言内置的管程只有一个条件变量

在这里插入图片描述


Monitor机制在Java中的实现

java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于ObjectMonitor实现,这是 JVM 内部基于 C++ 实现的一套机制。

ObjectMonitor其主要数据结构如下:

ObjectMonitor() {
   
    _header       = NULL; //对象头  markOop
    _count        = 0;  
    _waiters      = 0,   
    _recursions   = 0;   // 锁的重入次数 
    _object       = NULL;  //存储锁对象
    _owner        = NULL;  // 标识拥有该monitor的线程(当前获取锁的线程) 
    _WaitSet      = NULL;  // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
    _WaitSetLock  = 0 ;    
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
    FreeNext      = NULL ;
    _EntryList    = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
}

ObjectMonitor中有三个阻塞队列:_cxq 、_EntryList、_WaitSet。刚开始多个线程竞争锁,竞争失败的线程进入到_cxq队列中,它是栈结构。

获取到锁的对象执行后续的业务逻辑,调用等待方法后进入_WaitSet队列中,被唤醒后根据相应策略进入_cxq _EntryList队列中。

当持有锁对象的线程释放锁后,会根据相应的策略去唤醒_cxq _EntryList队列中的线程。

默认策略(QMode=0)是:_EntryList队列中不为空,直接从_EntryList队列中唤醒线程。如果_EntryList队列为空,则将_cxq中的元素插入到_EntryList,并唤醒第一个线程,也就是后来的线程先获取到锁。

在这里插入图片描述



对象的内存布局

一个对象是由三部分组成:对象头、实例数据、对其填充

而对象头由三部分组成:Mark Word标记、元数据指针、数组长度

  • Mark Work标记:用于标记对象hash值、分代年龄、锁状态标记、线程持有的锁、偏向线程ID、偏向时间戳等。32位机器占4字节,64为机器占8字节
  • Klass point指针:指向方法区中类元数据,标识当前对象是哪个类的实例,开启指针压缩后占4字节
  • 数组长度:数组对象才有,占四字节

在这里插入图片描述



Mark Word是如何记录锁状态的

synchronized加锁是加在对象上的,锁对象是如何记录锁状态的嘞?

锁的信息都是记录在每个对象 对象头的Mark Word中的


32位JVM下的对象头Mark Word结构描述

在这里插入图片描述


64位JVM下的对象头Mark Word结构描述

在这里插入图片描述

详情:

  • hashCode:对象的hashCode值

  • age:分代年龄

  • biased_lock:偏向锁标记位。

    因为无锁和偏向锁都是使用的01锁标志位,这样没办法区分所以就有加了1位来标识是否是偏向锁

  • lock:锁标志位

    区分锁的状态,01表示无锁或偏向锁、00表示轻量级锁、10表示重量级锁、11表示对象待GC回收状态

  • JavaThread*:保存持有偏向锁的线程ID,这个不是java中的线程ID,它们不一样

    偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。

  • epoch:保存偏向时间戳

    偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。

  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针

    当锁获取是无竞争时,JVM使用原子操作而不是OS互斥,这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的Mark Word中设置指向锁记录的指针。

  • ptr_to_heavyweight_monitor:重量级锁状态下,会创建一个Monitor对象,指向对象监视器Monitor的指针。

    如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。


Mark Word中锁标记枚举

enum {
    locked_value             = 0,    //00 轻量级锁 
         unlocked_value           = 1,   //001 无锁
         monitor_value            = 2,   //10 监视器锁,也叫膨胀锁,也叫重量级锁
         marked_value             = 3,   //11 GC标记
         biased_lock_pattern      = 5    //101 偏向锁
     }

我们新写一个类,使用JOL工具查看对象的内存布局可以发现,刚开始创建一个对象时,它是001无锁状态,然后用synchronized把这个对象变为锁对象后,它是00轻量级锁状态了。那为什么会跳过偏向锁直接变为了轻量级锁嘞?



偏向锁

什么是偏向锁

偏向锁是一种加锁操作的优化机制。经过研究发现大部分情况下是不存在锁竞争,一直都是一个线程去获取锁,因此为了消除在无竞争情况下重入锁(CAS操作)的开销,而引入了偏向锁。

对于没有竞争的场合,偏向锁有很好的优化效果。

JVM1.6默认开启偏向锁。新创建一个对象,此时给对象的Mark Word中的ThreadID为0,说明该对象处于可偏向但未偏向任何线程,也叫作匿名偏向状态



偏向锁延迟偏向

偏向锁是延迟开启的,这也是为什么我们直接运行一个java类,使用JOL工具查看对象的内存布局时发现对象的锁状态会直接从无锁变为轻量级锁。

之所以有偏向锁延迟的原因是:JVM在启动过程中会有一系列复杂的过程,比如装载配置、系统类初始化等等。在这个过程中会大量使用synchronized来为对象加锁,而且这些锁大多数都不是一个线程用,如果直接使用偏向锁,那么就会存在偏向锁撤销、偏向锁升级等过程。为了减少初始化时间,JVM才默认延迟开启偏向锁。

HotSpot虚拟机默认在启动后4s延迟才会对每个新创建的对象开启偏向锁模式。

相关的JVM启动参数

//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay=0
    
//禁止偏向锁
-XX:-UseBiasedLocking 
    
//启用偏向锁
-XX:+UseBiasedLocking 

4s后新创建的对象就开启了偏向锁标识,此时ThreadID还是为0

在这里插入图片描述

当有一个线程使用synchronized给这个对象加锁后,就会记录ThreadID

在这里插入图片描述



偏向锁状态跟踪

上面锁对象是在4s后创建的一个对象,那如果锁对象是某个类的class对象嘞?

这其实就是从无锁01 --> 轻量级锁00 因为类是class对象是在4s之内创建的。

// 锁对象是class对象
public static void main(String[] args) throws InterruptedException {
   

    // 未加锁
    System.out.println(ClassLayout.parseInstance(ObjectTest.class).toPrintable());

    // 加锁后
    new Thread(()->{
   
        synchronized (ObjectTest.class){
   
            System.out.println(ClassLayout.parseInstance(ObjectTest.class).toPrintable());
        }
    },"Thread1").start();
}

在这里插入图片描述



偏向锁加完锁,并释放后的状态,都是101偏向锁状态

public static void main(String[] args) throws InterruptedException {
   
    //jvm延迟偏向
    Thread.sleep(5000);

    // 创建时
    Object obj = new Test();
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());

    new Thread(()->{
   
        // 加锁后
        synchronized (obj){
   
            System.out.println(Thread.currentThread().getName()+"获取锁\n"+ClassLayout.parseInstance(obj).toPrintable());
        }
        
        // 释放后
        System.out.println(Thread.currentThread().getName()+"释放锁\n"+ClassLayout.parseInstance(obj).toPrintable());

    },"Thread1").start();
}

在这里插入图片描述



如果线程1释放偏向锁后,线程2又加锁了,此时偏向锁会升级为轻量级锁,也有可能还是偏向锁

public static void main(String[] args) throws InterruptedException {
   
    //jvm延迟偏向
    Thread.sleep(5000);

    Object obj = new Test();
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());

    // 线程1先加偏向锁,再释放锁
    new Thread(()->{
   
        synchronized (obj){
   
            System.out.println(Thread.currentThread().getName()+"\n"+ClassLayout.parseInstance
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Synchronized 是 Java 中用于实现线程同步的关键字,它可以保证在同一时刻只有一个线程可以访问被 Synchronized 修饰的代码块或方法。在实现线程同步时,Synchronized 使用了不同的,包括偏向、轻量级和重量级偏向是一种针对单线程场景的优化,它会在第一次获取时,将当前线程 ID 记录在的对象头中,之后该线程再次获取时,无需竞争资源,直接获取即可。举个例子: ```java public class BiasLockExample { private static Object lock = new Object(); public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { synchronized (lock) { // do something } } System.out.println("Time cost: " + (System.currentTimeMillis() - start) + "ms"); } } ``` 在以上代码中,由于所有的都是在同一个线程中获取的,因此会使用偏向进行优化,从而提高了程序的执行效率。 轻量级是一种适用于竞争不激烈的场景的,它会在第一次获取时,将对象头中的信息复制到线程栈中,然后通过 CAS 操作来更新对象头中的信息,如果更新成功,则表示该线程获取了,如果失败,则需要升级为重量级。举个例子: ```java public class LightweightLockExample { private static Object lock = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " got the lock"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(() -> { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " got the lock"); } }).start(); } } ``` 在以上代码中,由于第一个线程获取后会休眠 5 秒钟,因此第二个线程需要等待第一个线程释放之后才能获取,而这时就会使用轻量级进行优化。 重量级是一种适用于竞争激烈的场景的,它会导致线程阻塞,从而消耗大量的系统资源。举个例子: ```java public class HeavyweightLockExample { private static Object lock = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " got the lock"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(() -> { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " got the lock"); } }).start(); } } ``` 在以上代码中,由于两个线程同时竞争同一个,因此会使用重量级进行优化,从而导致第二个线程需要等待第一个线程释放之后才能获取,这样会导致线程阻塞,从而影响程序的执行效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值