当前位置: 首页 > article >正文

Java多线程与高并发专题——深入synchronized

类锁、对象锁

synchronized的使用一般就是同步方法和同步代码块

synchronized的锁是基于对象实现的。

如果使用同步方法

  • static:此时使用的是当前类.class作为锁(类锁)
  • 非static:此时使用的是当前对象做为锁(对象锁)

synchronized的优化

在JDK1.5的时候,Doug Lee推出了ReentrantLock,lock的性能远高于synchronized,所以JDK团队就在JDK1.6中,对synchronized做了大量的优化。

主要是以下三个方面:

锁消除

在synchronized修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,你即便写了synchronized,他也不会触发。

public synchronized void method(){
    // 没有操作临界资源
    // 此时这个方法的synchronized会被消除
}

锁膨胀

如果在一个循环中,频繁的获取和释放锁资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗。

public void method(){
    for(int i = 0;i < 999999;i++){
        synchronized(对象){

        }
    }
    // 这是上面的代码会触发锁膨胀
    // 相当于锁往上扩了一层
    synchronized(对象){
        for(int i = 0;i < 999999;i++){

        }
    }
}

锁升级

ReentrantLock的实现,是先基于乐观锁的CAS尝试获取锁资源,如果拿不到锁资源,才会挂起线程。synchronized在JDK1.6之前,完全就是获取不到锁,立即挂起当前线程,所以synchronized性能比较差。synchronized就在JDK1.6做了锁升级的优化。

锁升级的过程从无锁开始,逐步升级为偏向锁、轻量级锁,最后是重量级锁。

  1. 无锁:对象刚创建时处于无锁状态,没有任何线程竞争。

  2. 偏向锁:当一个线程访问同步块并获取锁时,对象头中的锁标志位会变为偏向锁,同时记录获得锁的线程 ID。偏向锁假定在接下来的一段时间内,总是这个线程获取锁,从而减少不必要的同步操作。

  3. 轻量级锁:当有第二个线程来竞争锁时,偏向锁会升级为轻量级锁。此时线程会通过自旋的方式来尝试获取锁,避免直接进入阻塞状态。

  4. 重量级锁:如果自旋一定次数后还没有获取到锁,或者有多个线程同时竞争,轻量级锁就会升级为重量级锁。重量级锁会导致线程阻塞,需要依靠操作系统的互斥量来实现同步。

无锁、匿名偏向:当前对象没有作为锁存在。

偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断,当前指向的线程是否是当前线程 。

  • 如果是,直接拿着锁资源走。

  • 如果当前线程不是我,基于CAS的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁。(偏向锁状态出现了锁竞争的情况)

轻量级锁:会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁

  • 如果成功获取到,拿着锁资源走

  • 如果自旋了一定次数,没拿到锁资源,锁升级。

重量级锁:就是最传统的synchronized方式,拿不到锁资源,就挂起当前线程。(用户态&内核态的切换)

锁升级的目的是为了在没有激烈竞争时,通过偏向锁和轻量级锁来提高性能,减少线程阻塞和唤醒的开销;而在竞争激烈时,再升级为重量级锁来保证线程安全。

不同级别的锁实现是有区别的。

偏向锁是通过在对象头的标记字段中记录线程 ID 等信息来实现的。

轻量级锁是基于 CAS(Compare and Swap)操作来尝试获取锁,通过在对象头中存储指向线程栈中锁记录的指针等信息来实现。

在升级到重量级锁时,会依赖 ObjectMonitor 来进行阻塞和唤醒线程等操作。

关于锁升级

锁默认情况下,开启了偏向锁延迟。

偏向锁在升级为轻量级锁时,会涉及到偏向锁撤销,需要等到一个安全点(STW),才可以做偏向锁撤销,在明知道有并发情况,就可以选择不开启偏向锁,或者是设置偏向锁延迟开启

因为JVM在启动时,需要加载大量的.class文件到内存中,这个操作会涉及到synchronized的使用,为了避免出现偏向锁撤销操作,JVM启动初期,有一个延迟4s开启偏向锁的操作

如果正常开启偏向锁了,那么不会出现无锁状态,对象会直接变为匿名偏向

public static void main(String[] args) throws InterruptedException {
    Thread.sleep(5000);
    Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());

    new Thread(() -> {

        synchronized (o){
            //t1  - 偏向锁
            System.out.println("t1:" + ClassLayout.parseInstance(o).toPrintable());
        }
    }).start();
    //main - 偏向锁 - 轻量级锁CAS - 重量级锁
    synchronized (o){
        System.out.println("main:" + ClassLayout.parseInstance(o).toPrintable());
    }
}

整个锁升级状态的转变:

  • 锁的升级:从无锁状态到偏向锁,再到轻量级锁,最后到重量级锁,锁的状态会根据竞争情况逐步升级。锁的升级是为了在不同的竞争程度下优化性能。
  • 锁的降级:在 JDK 1.8 中,锁的降级是不可逆的。一旦锁升级为重量级锁,就不会再降级为轻量级锁或偏向锁。

实现原理

其底层实现主要包括两部分:

对象头(Object Header):每个 Java 对象在内存中都包含一个对象头,用于存储对象的运行时数据。

  • Mark Word:存储对象的哈希码、GC 分代年龄、锁状态等信息。
  • Class Pointer:指向对象的类元数据。

Monitor:Monitor 是每个对象都有的同步机制,负责管理对象的锁。

  • Owner:当前持有锁的线程。
  • EntryList:等待获取锁的线程队列。
  • WaitSet:等待 notify 或 notifyAll 唤醒的线程队列。

MarkWord

为了可以在Java中看到对象头的MarkWord信息,需要导入依赖

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

MarkWord中标记着四种锁的信息:无锁、偏向锁、轻量级锁、重量级锁

  • 无锁状态:对象刚创建时,Mark Word 中的锁状态为无锁,Owner 为 null。
  • 偏向锁:当一个线程第一次获取锁时,如果锁处于无锁状态,JVM 会尝试将锁升级为偏向锁。偏向锁会将 Mark Word 中的锁状态设置为偏向锁状态,并将当前线程的 ID 存储在 Mark Word 中。如果同一个线程再次获取锁,可以直接使用偏向锁,无需 CAS 操作。
  • 轻量级锁:当多个线程竞争同一个锁时,如果锁处于偏向锁状态,JVM 会将锁升级为轻量级锁。轻量级锁使用 CAS 操作尝试获取锁,如果成功,将 Mark Word 中的锁状态设置为轻量级锁状态,并将当前线程的栈帧中的锁记录地址存储在 Mark Word 中。如果 CAS 操作失败,线程会进入自旋状态,继续尝试获取锁。
  • 重量级锁:当轻量级锁的自旋次数达到一定阈值,或者锁竞争非常激烈时,JVM 会将锁升级为重量级锁。重量级锁会将线程阻塞,进入 EntryList 阻塞队列,等待锁释放。当持有锁的线程释放锁时,会唤醒 EntryList 中的一个线程去获取锁。

Monitor锁

我们先来看下获取和释放 monitor 锁的时机,每个 Java 对象都可以用作一个实现同步的锁,这个锁也被称为内置锁或 monitor 锁,获得 monitor 锁的唯一途径就是进入由这个锁保护的同步代码块或同步方法,线程在进入被 synchronized 保护的代码块之前,会自动获取锁,并且无论是正常路径退出,还是通过抛出异常退出,在退出的时候都会自动释放锁。

同步代码块

我们可以用 javap -verbose 看使用synchronized后的同步代码块的反汇编内容,synchronized 代码块实际上多了 monitorenter 和 monitorexit 指令。可以把执行 monitorenter 理解为加锁,执行 monitorexit 理解为释放锁,每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0,我们来具体看一下 monitorenter 和 monitorexit的含义:

monitorenter

执行 monitorenter 的线程尝试获得 monitor 的所有权,会发生以下这三种情况之一:

  1. 如果该 monitor 的计数为 0,则线程获得该 monitor 并将其计数设置为 1。然后,该线程就是这个monitor 的所有者。
  2. 如果线程已经拥有了这个 monitor ,则它将重新进入,并且累加计数。
  3. 如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 的计数变成为0,代表这个 monitor 已经被释放了,于是当前这个线程就会再次尝试获取这个 monitor。
monitorexit

monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止。代表这个 monitor 已经被释放了,已经没有任何线程拥有它了,也就代表着解锁,所以,其他正在等待这个 monitor 的线程,此
时便可以再次尝试获取这个 monitor 的所有权。

同步方法

同步代码块是使用 monitorenter 和 monitorexit 指令实现的。而对于 synchronized 方法,并不是依靠 monitorenter 和 monitorexit 指令实现的,被 javap 反汇编后可以看到,synchronized 方法和普通方法大部分是一样的,不同在于,这个方法会有一个叫作 ACC_SYNCHRONIZED 的 flag 修饰符,来表明它是同步方法。

当某个线程要访问某个方法的时候,会首先检查方法是否有 ACC_SYNCHRONIZED 标志,如果有则需要先获得 monitor 锁,然后才能开始执行方法,方法执行之后再释放 monitor 锁。其他方面, synchronized 方法和刚才的 synchronized 代码块是很类似的,例如这时如果其他线程来请求执行方法,也会因为无法获得 monitor 锁而被阻塞。

ObjectMonitor

找到ObjectMonitor的两个文件,hpp,cpp

先查看核心属性:OpenJDK Maintenance outage

ObjectMonitor() {
    _header       = NULL;   // header存储着MarkWord
    _count        = 0;      // 竞争锁的线程个数
    _waiters      = 0,      // wait的线程个数
    _recursions   = 0;      // 标识当前synchronized锁重入的次数
    _object       = NULL;
    _owner        = NULL;   // 持有锁的线程
    _WaitSet      = NULL;   // 保存wait的线程信息,是一个双向链表
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;  // 获取锁资源失败后,线程要放到当前的单向链表中
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // _cxq以及被唤醒的WaitSet中的线程,在一定机制下,会放到EntryList中
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }
TryLock
int ObjectMonitor::TryLock (Thread * Self) {
   for (;;) {
      // 拿到持有锁的线程
      void * own = _owner ;
      // 如果有线程持有锁,告辞
      if (own != NULL) return 0 ;
      // 说明没有线程持有锁,own是null,cmpxchg指令就是底层的CAS实现。
      if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
         // 成功获取锁资源
         return 1 ;
      }
      // 这里其实重试操作没什么意义,直接返回-1
      // 因为到这个地方,就说明是竞争锁失败了,立马重试是没意义的
      if (true) return -1 ;
   }
}
try_entry
bool ObjectMonitor::try_enter(Thread* THREAD) {
  // 在判断_owner是不是当前线程
  if (THREAD != _owner) {
    // 判断当前持有锁的线程是否是当前线程,说明轻量级锁刚刚升级过来的情况
    if (THREAD->is_lock_owned ((address)_owner)) {
       _owner = THREAD ;
       _recursions = 1 ;
       OwnerIsThread = 1 ;
       return true;
    }
    // CAS操作,尝试获取锁资源
    if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
      // 没拿到锁资源,告辞
      return false;
    }
    // 拿到锁资源
    return true;
  } else {
    // 将_recursions + 1,代表锁重入操作。
    _recursions++;
    return true;
  }
}
enter

(想方设法拿到锁资源,如果没拿到,挂起扔到_cxq单向链表中)

void ATTR ObjectMonitor::enter(TRAPS) {
  // 拿到当前线程
  Thread * const Self = THREAD ;
  void * cur ;
  // CAS走你,
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  if (cur == NULL) {
     // 拿锁成功
     return ;
  }
  // 锁重入操作
  if (cur == Self) {
     // TODO-FIXME: check for integer overflow!  BUGID 6557169.
     _recursions ++ ;
     return ;
  }
  //轻量级锁过来的。
  if (Self->is_lock_owned ((address)cur)) {
    _recursions = 1 ;
    _owner = Self ;
    OwnerIsThread = 1 ;
    return ;
  }


  // 走到这了,没拿到锁资源,count++
  // count就是记录竞争锁资源的个数
  Atomic::inc_ptr(&_count);

  
    for (;;) {
      jt->set_suspend_equivalent();
      // 入队操作,进到cxq中
      EnterI (THREAD) ;
      if (!ExitSuspendEquivalent(jt)) break ;
      _recursions = 0 ;
      _succ = NULL ;
      exit (false, Self) ;
      jt->java_suspend_self();
    }
  }
  // count--
  Atomic::dec_ptr(&_count);
  
}
EnterI
for (;;) {
    // 入队
    node._next = nxt = _cxq ;
    // CAS的方式入队。
    if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;

    // 重新尝试获取锁资源
    if (TryLock (Self) ]]> 0) {
        assert (_succ != Self         , "invariant") ;
        assert (_owner == Self        , "invariant") ;
        assert (_Responsible != Self  , "invariant") ;
        return ;
    }
}

 


http://www.kler.cn/a/560018.html

相关文章:

  • PythonWeb开发框架—Django之DRF框架的使用详解
  • ai-1、人工智能概念与学习方向
  • 商业化运作的“日记”
  • system运行进程以及应用场景
  • 【Python爬虫(61)】Python金融数据挖掘之旅:从爬取到预测
  • 【odoo18-文件管理】在uniapp上访问odoo系统上的图片
  • 第二个接口-分页查询
  • 网站快速收录:如何优化网站图片Alt标签?
  • 如何安装vm和centos
  • 基于 IMX6ULL 的环境监测自主调控系统
  • github如何创建空文件夹
  • 图像处理篇---图像处理中常见参数
  • 基础学科与职业教育“101计划”:推动教育创新与人才培养
  • Windows逆向工程入门之逻辑运算指令解析与应用
  • 湖北中医药大学谱度众合(武汉)生命科技有限公司研究生工作站揭牌
  • 异常(1)
  • 如何在java中用httpclient实现rpc post 请求
  • linux-多进程基础(1) 程序、进程、多道程序、并发与并行、进程相关命令,fork
  • 瑞幸咖啡×动漫IP:精选联名案例,解锁品牌营销新玩法
  • Python生成器2-250224