监视器

java中同步是通过监视器模型来实现的,JAVA中的监视器实际是一个代码块,这段代码块同一时刻只允许被一个线程执行。线程要想执行这段代码块的唯一方式是获得监视器。

监视器有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。什么时候需要协作?比如:一个线程向缓冲区写数据,另一个线程从缓冲区读数据,如果读线程发现缓冲区为空就会等待,当写线程向缓冲区写入数据,就会唤醒读线程,这里读线程和写线程就是一个合作关系。JVM通过Object类的wait方法来使自己等待,在调用wait方法后,该线程会释放它持有的监视器,直到其他线程通知它才有执行的机会。一个线程调用notify方法通知在等待的线程,这个等待的线程并不会马上执行,而是要通知线程释放监视器后,它重新获取监视器才有执行的机会。如果刚好唤醒的这个线程需要的监视器并不是这个通知线程刚释放的监视器,等待线程会继续等待。object类中的notifyAll方法可以解决这个问题,它可以唤醒所有等待的线程,总有一个线程执行。

如上图所示,一个线程通过1号门进入Entry Set(入口区),如果在入口区没有线程等待,那么这个线程就会获取监视器成为监视器的owner,然后执行监视区域的代码。如果在入口区中有其它线程在等待,那么新来的线程也会和这些线程一起等待。线程在持有监视器的过程中,有两个选择,一个是正常执行监视器区域的代码,释放监视器,通过5号门退出监视器;还有可能等待某个条件的出现,于是它会通过3号门到Wait Set(等待区)休息,直到相应的条件满足后再通过4号门进入重新获取监视器再执行。

注意:当一个线程释放监视器时,在入口区和等待区的等待线程都会去竞争监视器,如果入口区的线程赢了,会从2号门进入;如果等待区的线程赢了会从4号门进入。只有通过3号门才能进入等待区,在等待区中的线程只有通过4号门才能退出等待区,也就是说一个线程只有在持有监视器时才能执行wait操作,处于等待的线程只有再次获得监视器才能退出等待状态。

对象锁

JVM中的一些数据,比如堆和方法区会被所有线程共享。JAVA中每个对象和类实际上都一把锁与之相关联,对于对象来说,监视的是这个对象的实例变量,对于类来说,监视的是类变量,如果一个对象没有实例变量,就什么也不监视。当虚拟机装载类时,会创建一个Class类的实例,锁住一个类实际上锁住的是这个类对应的Class类的实例。对象锁是可重入的,也就是说对一个对象或者类上的锁可以累加。

在JAVA中有两种监视区域:同步方法和同步块,这两种监视区域都和一个引入对象相关联,当到达这个监视区域时,JVM就会锁住这个引用对象,不论它是怎么离开的,都会释放这个引用对象上的锁。JAVA程序员不能自己加对象锁,对象锁是JVM内部机制,只需要编写同步方法或者同步块即可,操作监视区域时JVM会自动帮你上锁或者释放锁。

同步语句

要建立一个同步语句,只需要在相关语句加上synchronized关键字就可以,例如下面的incr方法,如果没有获得当前对象(this)的锁,在同步块内的语句是不会执行的,如果不是this引用,而是用另一个对象的引用,需要获得对应对象的锁同步块才会执行,如果用表达式获得对Class对象实例的引用,就需要锁住那个类。

  1. void incr() {
  2. synchronized (this) {
  3. i++;
  4. }
  5. }

以下是incr方法生成的字节码序列:

  1. void incr();
  2. Code:
  3. 0: aload_0            //将this引用压栈
  4. 1: dup                //复制栈顶元素
  5. 2: astore_1           //出栈并将this引用存放在局部变量1中
  6. 3: monitorenter           //出栈并获取对象锁
  7. 4: aload_0            //将this引用压栈
  8. 5: dup                //复制栈顶元素
  9. 6: getfield      #17             //获取i的值
  10. 9: iconst_1           //常数1入栈
  11. 10: iadd               //将i+1的结果入栈
  12. 11: putfield      #17             //将i的值存入this中
  13. 14: aload_1            //将this引用压栈
  14. 15: monitorexit            //弹出this引用释放对象锁
  15. 16: goto          22       //返回
  16. 19: aload_1            //19-22如果抛出,释放对象锁
  17. 20: monitorexit
  18. 21: athrow
  19. 22: return
  20. Exception table:
  21. from    to  target type
  22. 4    16    19   any
  23. 19    21    19   any

字节码的第3行从栈顶中获取对象锁,对象锁获取成功后才后执行后面的add操作,第15行释放获取的对象锁。注意:字节码中出现了异常表,是用于确保加锁的对象被释放,即使从同步语句块中抛出异常,也会释放对象锁,不然有可能导致死锁。

同步方法

要建立同步方法,只需要在方法修饰符前加上synchronized关键字,类似代码如下:

  1. synchronized void incr() {
  2. i++;
  3. }

生成的字节码序列如下:

  1. synchronized void incr();
  2. Code:
  3. 0: aload_0           //this引用压栈
  4. 1: dup               //复制栈顶元素
  5. 2: getfield      #2              //获取i的值
  6. 5: iconst_1          //将常量1入栈
  7. 6: iadd              //i+1入栈
  8. 7: putfield      #2              //将i的值存入this中
  9. 10: return            //返回

可见,JVM并没有使用moniterenter和moniterexit等指令,查看class文件在方法表中可以看到有0020出现,这是incr方法的访问标志(access flag):ACC_SYNCHRONIZED,顾名思义在说incr是一个线程同步方法。当JVM发现这是一个同步方法时,就会在这个对象或者类上获取锁,退出方法时会释放这个锁。两段字段码除了调用指令不同,还有一个区别是同步方法可以没有异常表,实际上JVM隐式地做了异常处理。

优缺点:

synchronized是通过软件(JVM)实现的,简单易用,即使在JDK5之后有了Lock,仍然被广泛地使用。

synchronized实际上是非公平的,新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待,不过这种抢占的方式可以预防饥饿。

synchronized只有锁只与一个条件(是否获取锁)相关联,不灵活,后来Condition与Lock的结合解决了这个问题。

多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待。有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。

参考资料:

Java多线程总结之由synchronized说开去

深入JVM锁机制1-synchronized

深入JVM锁机制2-Lock

The JavaTM Virtual Machine Specification

Inside the Java Virtual Machine

JAVA并发编程学习笔记之synchronized的更多相关文章

  1. 并发编程学习笔记(3)----synchronized关键字以及单例模式与线程安全问题

    再说synchronized关键字之前,我们首先先小小的了解一个概念-内置锁. 什么是内置锁? 在java中,每个java对象都可以用作synchronized关键字的锁,这些锁就被称为内置锁,每个对 ...

  2. Java并发编程学习笔记

    Java编程思想,并发编程学习笔记. 一.基本的线程机制 1.定义任务:Runnable接口 线程可以驱动任务,因此需要一种描述任务的方式,这可以由Runnable接口来提供.要想定义任务,只需实现R ...

  3. Java并发编程学习笔记 深入理解volatile关键字的作用

    引言:以前只是看过介绍volatile的文章,对其的理解也只是停留在理论的层面上,由于最近在项目当中用到了关于并发方面的技术,所以下定决心深入研究一下java并发方面的知识.网上关于volatile的 ...

  4. Java 并发编程学习笔记 理解CLH队列锁算法

    CLH算法实现 CLH队列中的结点QNode中含有一个locked字段,该字段若为true表示该线程需要获取锁,且不释放锁,为false表示线程释放了锁.结点之间是通过隐形的链表相连,之所以叫隐形的链 ...

  5. Java并发编程学习笔记(一)——线程安全性

    主要概念:线程安全性.原子性.原子变量.原子操作.竟态条件.复合操作.加锁机制.重入.活跃性与性能. 1.当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变 ...

  6. JAVA并发编程学习笔记------多线程调优

    1. 多线程场景下尽量使用并发容器代替同步容器 (如ConcurrentHashMap代替同步且基于散列的Map, 遍历操作为主要操作的情况下用CopyOnWriteArrayList代替同步的Lis ...

  7. JAVA并发编程学习笔记------协作对象之间发生的死锁

    一. 如果在持有锁时调用某个外部方法,那么将出现活跃性问题.在这个外部方法中可能会获取其他锁(这可能会产生死锁),或者阻塞时间过长,导致其他线程无法及时获得当前被持有的锁.如下代码: public c ...

  8. [转]JAVA并发编程学习笔记之Unsafe类

    1.通过Unsafe类可以分配内存,可以释放内存:类中提供的3个本地方法allocateMemory.reallocateMemory.freeMemory分别用于分配内存,扩充内存和释放内存,与C语 ...

  9. Java并发编程学习笔记(三)——对象的组合

    重要概念: 1.在设计线程安全类的过程中,需要包含以下三个基本要素: (1)找出构成对象状态的所有变量. (2)找出约束状态变量的不变性条件. (3)建立对象状态的并发访问管理策略. 2.

  10. java并发编程学习笔记(一)初识并发原子性

    1.并发的意义 现在是一个多核的时代,并发的存在意义就是为了能够充分利用多核计算机的优势,提高程序的运行效率: 2.并发的风险 竞争-----多个线程对内存数据数据进行读写操作时,对数据处理结果的一个 ...

随机推荐

  1. Seata 1.3.0 Oracle 回滚测试验证 报错 ORA-02289: 序列不存在

    使用Seata 1.3.0版本,测试A服务调用B服务,且A方法中,手动写了一个异常,测试是否正常回滚(Mysql已经测试过) 发现报错:ORA-02289: 序列不存在 一看就是undo_log这张表 ...

  2. 13 Python面向对象编程:装饰器

    本篇是 Python 系列教程第 13 篇,更多内容敬请访问我的 Python 合集 Python 装饰器是一种强大的工具,用于修改或增强函数或方法的行为,而无需更改其源代码.装饰器本质上是一个接收函 ...

  3. 像 Mysql 和 MongoDB 这种大型软件在设计上都是精益求精的,它们为什么选择B树,B+树这些数据结构?

    为什么 MongoDB (索引)使用B-树而 Mysql 使用 B+树? B 树与 B+ 树,其比较大的特点是:B 树对于特定记录的查询,其时间复杂度更低.而 B+ 树对于范围查询则更加方便,另外 B ...

  4. SimCLR: 一种视觉表征对比学习的简单框架《A Simple Framework for Contrastive Learning of Visual Representations》(对比学习、数据增强算子组合,二次增强、投影头、实验细节很nice),好文章,值得反复看

    现在是2024年5月18日,好久没好好地看论文了,最近在学在写代码+各种乱七八糟的事情,感觉要和学术前沿脱轨了(虽然本身也没在轨道上,太菜了),今天把师兄推荐的一个框架的论文看看(视觉CV领域的). ...

  5. 工具 – Cypress

    介绍 Cypress 是一款 e2e 测试工具.每当我们写好一个组件或者一个页面之后,我们会想对整体做一个测试. 在不使用工具的情况下,我们会开启 browser,然后做一系列点击.滚动.填 form ...

  6. Spring —— 集合注入

    数组注入    List集合注入    set集合注入    Map集合注入    Properties集合注入   

  7. 五,MyBatis-Plus 当中的 “ActiveRecord模式”和“SimpleQuery工具类”(详细实操)

    五,MyBatis-Plus 当中的 "ActiveRecord模式"和"SimpleQuery工具类"(详细实操) @ 目录 五,MyBatis-Plus 当 ...

  8. U179915 关于分级火箭的一点理想化的计算

    题目地址 本题是一道疯狂推式子的玄学复杂度sb题. 解题思路 1.数学部分 ​ 首先假定已经将火箭分成了 \(n+1\) 级,记使用了 \(n\) 个分级器.记各级的开始时间点为: \[0=t_0&l ...

  9. HarmonyOS NEXT 底部选项卡功能

    在HarmonyOS NEXT中使用ArkTS实现一个完整的底部选项卡功能,可以通过以下几个步骤来完成: 创建Tabs组件:使用Tabs组件来创建底部导航栏,并通过barPosition属性设置其位置 ...

  10. 使用c++ onnxruntime构建项目出现的bug

    bug1:The given version [11] is not supported, only version 1 to 7 is supported in this build. 应该是加载了 ...