首页 > 编程语言 >Java 并发编程深度解析:synchronized 关键字的内部原理与应用

Java 并发编程深度解析:synchronized 关键字的内部原理与应用

时间:2024-09-13 12:52:06浏览次数:3  
标签:Java Monitor synchronized 对象 void 编程 线程 public

引言

在并发编程中,当多个线程访问同一个共享资源时,我们必须考虑如何维护数据的原子性。Java 是通过 synchronized 关键字实现锁功能来做到这点的,synchronized是 JVM 实现的一种内置锁,锁的获取和释放由 JVM 隐式实现。

锁的本质

在这里插入图片描述
如上图所示,多个线程要访问同一个资源。线程就是一段运行的代码,资源就是一个变量、对象、文件等;而锁就是要实现线程对资源访问的控制,保证同一时刻只能有一个线程去访问某一个资源。

从程序的角度看,锁其实就是一个对象,那么这个对象需要完成以下几个事情:

  1. 对象内部有一个标志位,记录自己是否被某个线程占用;
  2. 如果这个对象被某个线程占用,得记录这个线程的 Thread ID
  3. 这个对象需要维护一个 thread id list,记录其他所有阻塞的、等待获取这个锁的线程,在当前线程释放锁后,从这个 thread id list里面取出一个线程唤醒;

基于上述的描述,来学习一下 synchronized 的使用及原理

基本使用

synchronized 关键字可以作用于方法或方法内的局部代码块。

作用于方法

public synchronized void method1() { // code }

作用于局部代码块

public void method2() { 
	Object o = new Object(); 
	synchronized (o) { 
	 //code
	} 
}

假设现在有一个Counter 类,如下

public class Counter {
  private int increasedSum = 0;
  private int decreasedSum = 0;

  public void add(int value) {
    increasedSum += value;
  }

  public void substract(int value) {
    decreasedSum -= value;
  }
}

尽管add 函数和substract函数是线程不安全的,由于它们访问的共享资源不同,所以它们是可以并发执行的。
我们应该如何使用synchronized加锁,既保证类为线程安全的,又保证两个函数可以并发执行呢?

public class Counter {
  private int increasedSum = 0;
  private int decreasedSum = 0;
  
  private Object obj1 = new Object();
  private Object obj2 = new Object();

  public void add(int value) {
    synchronized (obj1) {
      increasedSum += value;
    }
  }

  public void substract(int value) {
    synchronized (obj2) {
      decreasedSum -= value;
    }
  }
}

synchronized 关键字底层使用的锁是Monitor 锁,每个对象实例都有一个 Monitor 锁,Monitor 锁是寄生于对象存在的,Monitor可以和对象一起创建、销毁。

如果我们想使用一个新的 Monitor 锁,只需新创建一个对象即可。

所以为了让add 函数和substract函数之间能并发执行,可以对这两个函数加不同的锁,即分别使用 obj1 上的锁和 obj2 上的锁。

对象锁和类锁

synchronized 修饰普通方法时,锁为当前实例对象;修饰代码块时,锁为括号里的对象,这些都是对象锁

与对象锁相对应的是类锁

public synchronized static void method3() {
      // code
 }

当用 synchronized 修饰静态方法时,会隐式的使用当前类的类锁;
对于类锁而言,synchronized 使用的也是某个对象上的 Monitor 锁,而这个对象比较特殊,是类的 Class 类对象。
Class 类是所有类的抽象,每个类在 JVM 中都有一个 Class 类对象来表示这个类。

锁的字节码

来看下 synchronized 对应的字节码长什么样子。

public class SynTest {

    //关键字在实例方法上,锁为当前实例
    public synchronized void method1() {
        // code
    }

    //关键字在代码块上,锁为括号里面的对象
    public void method2() {
        Object o = new Object();
        synchronized (o) {
            // code
        }
    }
}

通过反编译看下具体字节码的实现,运行以下反编译命令,就可以输出我们想要的字节码:

javac -encoding UTF-8 SynTest.java  //先运行编译class文件命令
javap -v SynTest.class //再通过javap打印出字节文件

method1对应的字节码如下,从字节码中,我们发现,实际上,编译器只不过是在函数的flags中添加了ACC_SYNCHRONIZED标记而已;

JVM 使用ACC_SYNCHRONIZED标记来区分同步方法,当方法调用时,调用指令先检查该方法是否有ACC_SYNCHRONIZED标志,如果设置了该标志,执行线程将先持有 Monitor 对象,然后再执行方法。在该方法运行期间,其它线程将无法获取到该 Mointor 对象,当方法执行完成后,再释放该 Monitor 对象。

 public synchronized void method1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 13: 0

method2对应字节码如下,会发现:synchronized 在修饰同步代码块时,是由 monitorentermonitorexit 指令来实现同步的。进入 monitorenter 指令后,线程将持有 Monitor 对象,退出 monitorenter 指令后,线程将释放该 Monitor 对象。

 public void method2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1
         8: aload_1
         9: dup
        10: astore_2
        11: monitorenter
        12: aload_2
        13: monitorexit
        14: goto          22
        17: astore_3
        18: aload_2
        19: monitorexit
        20: aload_3
        21: athrow
        22: return

从上述实例可以看出,synchronized语句编译为字节码,只是做了一个简单的翻译而已。我们无法通过synchronized对应的字节码了解其底层实现原理,需要继续深挖。

底层实现原理

在Hotspot JVM中,Monitor锁对应的实现类为ObjectMonitor类,而 ObjectMonitor 是由 C++ 的 ObjectMonitor.hpp 文件实现,如下所示:

ObjectMonitor() {
   _header = NULL;
   _count = 0; //记录个数
   _waiters = 0,
   _recursions = 0;
   _object = NULL; //该 Monitor 锁所属的对象
   _owner = NULL;  //获取到该 Monitor 锁的线程
   _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
   _WaitSetLock = 0 ;
   _Responsible = NULL ;
   _succ = NULL ;
   _cxq = NULL ; //没有获取到锁的线程暂时加入_cxq
   FreeNext = NULL ;
   _EntryList = NULL ; //存储等待被唤醒的线程
   _SpinFreq = 0 ;
   _SpinClock = 0 ;
   OwnerIsThread = 0 ;
}

通过_object成员变量,可以得到 Monitor 锁所属的对象,也可以通过对象查找到对应的 Monitor 锁,对象头中的 Mark Word 字段用来记录对象所对应的 Monitor 锁。
在这里插入图片描述
在这里插入图片描述

Monitor 锁是如何实现加锁、解锁的呢?

竞争锁

多个线程同时请求获取 Monitor 锁时,它们会通过 CAS 来设置ObjectMonitor_owner字段,谁设置成功,谁就获取到了这个 Monitor 锁。

排队等待锁

成功获取到锁的线程去执行代码,没有获取到锁的线程会被放入ObjectMonitor_cxq中等待锁,_cxq是一个单链表,链表节点的定义如下ObjectWaiter类所示。ObjectWaiter类中包含线程的基本信息以及其他一些结构信息,比如_prev指针、_next指针。

class ObjectWaiter : public StackObj {
 public:
  enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ } ;
  enum Sorted  { PREPEND, APPEND, SORTED } ;
  ObjectWaiter * volatile _next;
  ObjectWaiter * volatile _prev;
  Thread*       _thread;
  jlong         _notifier_tid;
  ParkEvent *   _event;
  volatile int  _notified ;
  volatile TStates TState ;
  Sorted        _Sorted ; // List placement disposition
  bool          _active ; // Contention monitoring is enabled
 public:
  ObjectWaiter(Thread* thread);
  void wait_reenter_begin(ObjectMonitor *mon);
  void wait_reenter_end(ObjectMonitor *mon);
};

ObjectWaiter不仅用来表示单链表的节点(_cxq),还用来表示双向链表的节点(_EntryList_WaitSet),当用来表示单链表的节点时,ObjectWaiter中的_prev指针设置为null。

通知排队等待锁的线程去竞争锁

当持有锁的线程释放锁后,它会从_EntryList中取出一个线程,被取出的线程会再次通过 CAS操作去竞争 Monitor 锁;

如果_EntryList中没有线程,就会先将_cxq中的线程搬移到_EntryList中去,然后再从_EntryList中取出线程。

这里面有几个问题,需要说明一下
1、为什么从_EntryList中取出的线程不直接获取锁而是通过CAS操作去竞争锁?

因为此时有可能存在新来的线程(非_EntryList里的线程)也在竞争锁。

2、为什么不直接从_cxq取线程,而是要将_cxq中的线程倒腾到_EntryList中再取呢?

目的是减少多线程环境下链表存取的冲突,_cxq只负责存操作(往链表中添加节点),_EntryList只负责取操作(从链表中删除节点),冲突减少,线程安全性处理就变得简单。

因为多个线程有可能同时竞争锁失败,同时存入_cxq中,所以,我们需要通过CAS操作来保证往_cxq中添加节点操作的线程安全性。
因为只有释放锁的线程才会从_EntryList中取线程,所以,_EntryList的删除节点操作是单线程操作,不存在线程安全问题。

阻塞

没有获取锁的线程会阻塞,并且对应的内核线程不再分配时间片。

Java线程采用1:1线程模型来实现,一个Java线程会对应一个内核线程。应用程序提交给Java线程执行的代码,会一股脑地交给对应的内核线程来执行。内核线程在执行的过程中,如果遇到synchronized关键字,会执行上述的步骤。

如果没有竞争到锁,则内核线程会调用park()函数将自己阻塞,这样CPU就不再分配时间片给它。

取消阻塞

持有锁的线程在释放锁之后,从_EntryList中取出一个线程时,就会调用unpark()函数,取消对应内核线程的阻塞状态,恢复分配时间片,这样才能让它去执行竞争锁的代码。

流程图

在这里插入图片描述

总结

JVM 在 JDK1.6 中引入了分级锁机制来优化 synchronized,当一个线程获取锁时,首先对象锁将成为一个偏向锁,这样做是为了优化同一线程重复获取导致的用户态与内核态的切换问题;其次如果有多个线程竞争锁资源,锁将会升级为轻量级锁,它适用于在短时间内持有锁,且分锁有交替切换的场景;轻量级锁还使用了自旋锁来避免线程用户态与内核态的频繁切换,大大地提高了系统性能;但如果锁竞争太激烈了,那么同步锁将会升级为重量级锁。

关于锁升级的文章,后续将再研究研究…

参考文献
《Java 编程之美》
《Java 性能调优实战》
《Java 并发编程实战》

标签:Java,Monitor,synchronized,对象,void,编程,线程,public
From: https://blog.csdn.net/yqq962464/article/details/142093414

相关文章

  • 【转行必看】Java到AI,程序员的逆袭秘籍!
    随着技术的不断进步,AI大模型已经成为当今科技领域最热门的话题之一。许多开发者开始考虑从传统的软件开发领域,如Java,转向AI大模型领域,今天小编和大家一起来探讨Java开发者是否可以转型到AI大模型、转行需要补齐哪些知识?,以及在大模型时代我们如何有效的去学习大模型?01Java......
  • Java中的锁
    Java中的锁公平锁/非公平锁可重入锁独享锁/共享锁互斥锁/读写锁乐观锁/悲观锁分段锁偏向锁/轻量级锁/重量级锁自旋锁上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。公平锁/非公平锁公平锁......
  • 网络套接字编程(二)
    socket常见API创建套接字:(TCP/UDP,客户端+服务器)intsocket(intdomain,inttype,intprotocol);绑定端口号:(TCP/UDP,服务器)intbind(intsockfd,conststructsockaddr*addr,socklen_taddrlen);监听套接字:(TCP,服务器)intlisten(intsockfd,intbacklog);接收请......
  • 基于Java+Springboot+Mysql实现智能物业信息化管理系统功能设计与实现三
    一、前言介绍:1.1项目摘要现代社会对物业管理效率和服务质量不断提升的需求。随着城市化进程的加速和房地产市场的蓬勃发展,物业管理行业面临着越来越多的挑战和机遇。传统的物业管理方式往往依赖于人工操作和经验判断,效率低下且难以满足现代社会的需求。因此,借助信息化技......
  • 基于Java+Springboot+Mysql实现智能物业信息化管理系统功能设计与实现四
    一、前言介绍:1.1项目摘要现代社会对物业管理效率和服务质量不断提升的需求。随着城市化进程的加速和房地产市场的蓬勃发展,物业管理行业面临着越来越多的挑战和机遇。传统的物业管理方式往往依赖于人工操作和经验判断,效率低下且难以满足现代社会的需求。因此,借助信息化技......
  • JVM-详解Java虚拟机
    jvm概述Java上层技术与jvm的层次关系图Java生态圈Java不是最厉害的语音,但jvm是最强大的虚拟机jvm的位置Java代码执行流程对上图举例jvm的生命周期Sun(被Oracle收购)的HotSpot:第一商用虚拟机JRockit:第二商用IBM的J9:第三特定硬件环境中的虚拟机(即应用场......
  • Java常见报错
    NoSuchElementException:一般都是数组或者集合的索引越界ConCurrentCheck(并发修改异常):因为集合中有自己的修改次数记录的变量,还有另一个记录地变量,一般这2个变量不一致,则会报错!mapkeyisrequired怎么解决:说明:MyBatis查询一些记录,数据涉及到两个表里的数据,需要连表查......
  • 适合初学者的[JAVA]:Redis(2:I/O多路复用模型与事件派发)
    目录说明前言I/O多路复用模型备注:用户空间和内核空间:备注:阻塞IO:(了解)非阻塞IO:(了解)IO多路复用:(重点)常见的方式有:差异:事件派发说明:Redis网络模型总结: 说明本文适合刚刚学习Java的初学者,也可以当成阿岩~的随手笔记.接下来就请道友们和我一起来......
  • 【Java】Ruoyi(若依)——6.微服务版项目启动
    http://doc.ruoyi.vip/ruoyi-cloud/document/hjbs.html#%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C最早的时候,并没有打算写ruoyi框架的微服务版的安装和部署,原因如下:1.当时的项目中并没有用到微服务版。2.虽然微服务很有名,也是未来的发展趋势。但是我对微服务了解知之甚少,学起来......
  • Java成神之路-踩坑篇: SpringBoot2.7.0版本整合Swagger3.0.0。解决:项目启动报错与swa
    话不多说先上报错信息Causedby:java.lang.NullPointerException:null atspringfox.documentation.spring.web.WebMvcPatternsRequestConditionWrapper.getPatterns(WebMvcPatternsRequestConditionWrapper.java:56)~[springfox-spring-webmvc-3.0.0.jar:3.0.0] atspri......