为什么95%的Java程序员人,都是用不好Synchronized?

news2024/11/15 17:47:58

Synchronized锁优化

jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
锁主要存在四中状态,依次是:无锁-> 偏向锁 -> 轻量级锁 -> 重量级锁,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

img

锁优化

偏向锁

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。

偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,

因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。

引入偏向锁主要目的是:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。上面提到了轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的。

那么偏向锁是如何来减少不必要的CAS操作呢?我们可以查看Mark work的结构就明白了。只需要检查是否为偏向锁、锁标识为以及ThreadID即可

img

获取锁

  1. 检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
  2. 若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);
  3. 如果线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);
  4. 通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
  5. 执行同步代码块

释放锁
偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:

  1. 暂停拥有偏向锁的线程,判断锁对象石是否还处于被锁定状态;
  2. 撤销偏向苏,恢复到无锁状态(01)或者轻量级锁的状态;

轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。

轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁。

img

获取锁
  1. 判断当前对象是否处于无锁状态(hashcode、0、01),若是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3);
  2. JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指正,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3);
  3. 判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;
释放锁

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  1. 取出在获取轻量级锁保存在Displaced Mark Word中的数据;
  2. 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,否则执行(3);
  3. 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程。

对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢;

自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。

这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,

当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。

如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。

同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。

何谓自旋锁?
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋),和CAS类似。

自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。

如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。

所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。

自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;

如果通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。

于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。

适应自旋锁

JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。

反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译).

通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用

因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。

锁消除的依据是逃逸分析的数据支持。
如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。

变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?

但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。

比如StringBuffer的append()方法,Vector的add()方法:

COPYpublic void vectorTest(){
       Vector<String> vector = new Vector<String>();
       for(int i = 0 ; i < 10 ; i++){
           vector.add(i + "");
       }

       System.out.println(vector);
   }

在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。

逃逸分析

如果证明一个对象不会逃逸方法外或者线程外,则可针对此变量进行优化:

同步消除synchronization Elimination,如果一个对象不会逃逸出线程,则对此变量的同步措施可消除。

重量级锁

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

为什么重量级锁的开销比较大呢

原因是当系统检查到是重量级锁之后,会把等待想要获取锁的线程阻塞,被阻塞的线程不会消耗CPU,但是阻塞或者唤醒一个线程,都需要通过操作系统来实现,也就是相当于从用户态转化到内核态,而转化状态是需要消耗时间的

三种锁的区别

优点缺点使用场景
偏向锁加锁和解锁不需要CAS,没有额外的性能消耗,和执行非同步方法相比,仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗只有一个线程访问同步块或者同步方法的场景
轻量级锁竞争的线程不会阻塞提高响应速度若线程长时间抢不到锁,自旋会消耗CPU性能线程交替执行同步块或者同步方法的场景
重量级锁线程竞争不使用自旋,不消耗CPU线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗追求吞吐量,同步块或者同步方法执行时间较长的场景

锁升级

偏向锁升级轻量级锁:当一个对象持有偏向锁,一旦第二个线程访问这个对象,如果产生竞争,偏向锁升级为轻量级锁。

轻量级锁升级重量级锁:一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

锁粗化

我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
​ 在大多数的情况下,上述观点是正确的,LZ也一直坚持着这个观点。

但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。

如下面的例子,一个方法由两个加锁,因为num = x + y;耗时较短,对比两次锁短的多,就会锁粗化。

COPYprivate int x, y;

   /**
    * 因为一个方法需要两个加锁解锁耗费资源
    * 对于  num = x + y; 耗费时间很短 就会将
    * 代码包裹进去组成一个锁
    * @return
    */
   public int lockCoarsening() {
       int num = 0;
       //对象锁
       synchronized (this) {
           x++;
           //todo 处理部分业务
       }
       num = x + y;
       //对象锁
       synchronized (this) {
           y++;
           //todo 处理部分业务
       }
       return num;
   }

粗化后

COPYprivate int x, y;

  /**
   * 使用一个锁
   *
   * @return
   */
  public int lockCoarsening() {
      int num = 0;
      //只进行一次加锁解锁
      synchronized (this) {
          x++;
          //todo 处理部分业务
          num = x + y;
          y++;
          //todo 处理部分业务
      }
      return num;
  }

wait和notify的原理

调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁。

当其他线程调用notify后,会选择从等待队列中唤醒任意一个线程,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了。

wait和notify为什么需要在synchronized里面?

wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列,而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。

而对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里,所以就必须要找到这个对象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程。

本文由传智教育博学谷狂野架构师教研团队发布。

如果本文对您有帮助,欢迎关注点赞;如果您有任何建议也可留言评论私信,您的支持是我坚持创作的动力。

转载请注明出处!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/381665.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

upload-labs通关详细教程

文章目录文件上传要点1、前端验证绕过做题步骤源码分析2、Content-Type方式绕过做题步骤源码分析3、黑名单绕过做题步骤源码分析4、.htaccess文件绕过简介做题步骤源码分析5、后缀大小写绕过简介做题步骤源码分析6、文件后缀&#xff08;空&#xff09;绕过简介做题步骤源码分析…

CAN工具-VSpy(ValueCAN) - Panel面板(续)

继续上次的Panel面板介绍&#xff01;&#xff01;&#xff01;7. LED 控件LED控件可以用作指示器。在Graphical Panels中选中该控件&#xff0c;在界面右侧侧拉框的Properties中可设置LED控件的通用属性。LED控制属性LED On Color&#xff1a;双击打开标准的Windows颜色选择器…

Vue 3.0 学习笔记之基础知识

系列文章目录 提示&#xff1a;阅读本章之前&#xff0c;请先阅读目录 文章目录系列文章目录前言Vue 3.0 创建与Vue2.0对比的变化关闭语法检查setup 组合式函数compositions前言 Vue 3.0 创建 与Vue2.0对比的变化 关闭语法检查 lintOnSave: false setup 组合式函数compositions…

Git 初体验 (处理 .ssh 文件的相关问题)

目录前沿环境入门教程出现的问题问题 1 &#xff1a;C 盘用户文件中找不到 .ssh 文件问题 2 : 在 clone 远程仓库文件时报错问题3 : 第一次从 Gitee clone 项目时会弹出一个 Git 窗口,要你输入用户和密码获取 credential (可以信任的证明)自我实践总结引用前沿 很早前安装了 G…

二叉树——删除二叉搜索树中的节点

删除二叉搜索树中的节点 链接 给定一个二叉搜索树的根节点 root 和一个值 key&#xff0c;删除二叉搜索树中的 key 对应的节点&#xff0c;并保证二叉搜索树的性质不变。返回二叉搜索树&#xff08;有可能被更新&#xff09;的根节点的引用。 一般来说&#xff0c;删除节点可…

2022_SPL_CMINet

Cross-Stage Multi-Scale Interaction Network for RGB-D Salient Object Detection 用于rgb-d显着目标检测的跨阶段多尺度交互网络 目录 文章目录 前言 一、引言 二、提出的方法 A.概述 感觉有点乱&#xff0c;没看太懂&#xff0c;没关系&#xff0c;我们接着往下看 …

Postman 实现 UI 自动化测试

看到这篇文章的标题&#xff0c;是不是有小伙伴会感到惊讶呢&#xff1f; Postman 不是做接口测试的吗&#xff1f;为什么还能做 UI 自动化测试呢&#xff1f; 其实&#xff0c;只要你了解 Selenium 的运行原理&#xff0c;就可以理解为什么 Postman 也能实现 UI 自动化测试了…

EPICS motor模块

一、概要 1&#xff09; 在EPICS motor模块中的是什么并且它为了什么&#xff1f; 2&#xff09; 支持的电机控制器和模型 3&#xff09;电机记录特性 4&#xff09;配置示例 5&#xff09;反馈 6&#xff09; 重试 7&#xff09; 回程差矫正 8&#xff09;发行 二、术…

Linux上的校验和验证

校验和&#xff08;checksum&#xff09;程序用来从文件中生成相对较小的唯一密钥。我们可以重新计算该密钥&#xff0c;用以检查文件是否发生改变。修改文件可能是有意为之&#xff08;添加新用户会改变密码文件&#xff09;&#xff0c;也可能是无意而为&#xff08;从CD-ROM…

Java:Java仍然处于领先地位?

没有多少编程语言能够自吹自擂并持续流行20多年&#xff0c;但Java就是其中之一。Java应用程序不仅局限于web和移动开发&#xff0c;而且给大数据和人工智能留下了深刻的印象。不用多说&#xff0c;让我们讨论一下Java流行的几个原因!!1.实用性根据JamesGosling的说法&#xff…

QT+OpenGL 面剔除和帧缓冲

QTOpenGL 面剔除和帧缓冲 本篇完整工程见gitee:QtOpenGL 对应点的tag&#xff0c;由turbolove提供技术支持&#xff0c;您可以关注博主或者私信博主 面剔除 OpenGL能够检查所有面向&#xff08;Front Facing&#xff09;观察者的面&#xff0c;并且渲染他们&#xff0c;而丢…

springboot logback日志+异常+阿里云日志 aliyun-log-logback-appender

前言最近有个新项目用了&#xff0c;springboot3.0&#xff0c;以前项目日志保存得方式是阿里云云服务自动读取日志文件&#xff0c;最近项目部署得方式可能有变化&#xff0c;所以新项目用logbackaliyun-log-logback-appender得方式保存到阿里云日志服务。用logback得原因主要…

《OpenGL宝典》--统一变量

统一变量 [layout (location 0)] uniform float f 1.0f;若设置layout&#xff0c;则不需要使用glGetUniformLocation来获取统一变量的位置 使用glUniform*传递值&#xff0c;glUniformMatrix*()设置矩阵统一变量。 glUseProgram(myShader); glUniform1f(0,45.2f);//0为loc…

思迅软锁安装配置说明

思迅软锁安装配置说明 一、软锁安装、申请及配置流程 1.软件安装环境要求 2.软件安装配置流程 步骤1: 在平台下载软锁程序并安装。在安装了总部数据库的服务器上&#xff0c;运行“思迅软锁服务.exe”程序&#xff0c;按照指引进行安装&#xff0c;安装完成后将在电脑的桌面上…

微服务之Gateway服务网关

&#x1f3e0;个人主页&#xff1a;阿杰的博客 &#x1f4aa;个人简介&#xff1a;大家好&#xff0c;我是阿杰&#xff0c;一个正在努力让自己变得更好的男人&#x1f468; 目前状况&#x1f389;&#xff1a;24届毕业生&#xff0c;奋斗在找实习的路上&#x1f31f; &#x1…

「亲测」0成本考证填报个税纳税额减免3600

「亲测」0成本考证填报个税纳税额减免3600 今天开始2022综合所得的年度汇算就开始办理了&#xff0c;刚刚步入工作的同学&#xff0c;对个税的填报有些苦恼&#xff0c;好像除了房租就没有能减税的政策了。 别急别急&#xff0c;其实个⼈所得税⾥⾯包含⼀个叫“专项附加扣除”的…

C++ 认识和了解C++

1.在使用C语言写代码的时候开头要用到的是&#xff1a; #include<iostream> using namespace std;不可以写成这样&#xff1a; #include iostream.h&#xff08;1&#xff09;iostream是输入输出流类&#xff0c; istream输入流类 cin >> ostream输出流类 cout &…

40系笔记本(可不联网激活)深度学习生产力(环境配置和简单训练测试)

40系笔记本深度学习、转码生产力&#xff08;环境配置和简单训练测试&#xff09;这里写自定义目录标题深度学习环境准备CUDA、CUDNN版本问题torch版本问题其他软件版本的安装命令训练测试代码地址关于Linux还是Windows的问题结果博主首发购买了枪神7超竞4080的版本&#xff0c…

git开发流程

分支介绍 dev&#xff1a;开发环境&#xff0c;从feature去mr test: 测试环境&#xff0c;从feature去mr pre&#xff1a; 预生产环境&#xff0c;从master去mr&#xff0c;为了验证master代码 master: 生产环境&#xff0c;从feature去mr feature&#xff1a; 开发分支----小…

4种方法教你如何隐藏电脑磁盘分区?

磁盘分区是电脑的重要组成部分&#xff0c;我们能够在电脑中保存众多数据&#xff0c;就离不开它。那么你知道该如何隐藏磁盘分区吗&#xff1f;下面小编就教你4个方法隐藏电脑磁盘分区。方法一&#xff1a;使用磁盘管理隐藏硬盘分区1、按下“WinR”键&#xff0c;输入“diskmg…