JUC并发编程02——锁

一.乐观锁和悲观锁

悲观锁

  • 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
  • synchronized关键字和Lock的实现类都是悲观锁。

适用场景:适合写操作多的场景,先加锁可以保证写操作时数据正确。

乐观锁

  • 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。
  • 如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作。
  • 乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

适用场景:适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

实现方式

乐观锁一般有两种实现方式:

  • 采用版本号机制
  • CAS(Compare-and-Swap,即比较并替换)算法实现

二.synchronized 锁

2.1synchronized 锁的内容

  • 对于普通同步方法锁的是调用当前方法的对象,通常指 this 。并且该对象中所有的普通同步方法用的都是同一把锁。也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。(对象锁)
  • 对于静态同步方法锁的是当前类的Class对象,如 Phone.class 唯一的一个模板。一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。(类锁)
  • 对于同步代码块,锁的是 synchronized 括号内的对象。

注意:具体实例对象 this 和 Class对象,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的

2.2从字节码角度分析 synchronized 实现

synchronized 同步代码块

synchronized的底层实现原理

synchronized作用在代码块时,实现使用的是 monitorenter monitorexit 指令

这里有一个 monitorenter,却有两个 monitorexit 指令的原因是:JVM 要保证每个 monitorenter 必须有与之对应的 monitorexit,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 的所有权。

synchronized 普通同步方法

可以看出,被 synchronized 修饰的方法会有一个 ACC_SYNCHRONIZED 标志。当某个线程要访问某个方法的时候,会首先检查方法是否有 ACC_SYNCHRONIZED 标志,如果有则需要先获得 monitor 锁,然后才能开始执行方法,方法执行之后再释放 monitor 锁。

synchronized 静态同步方法

ACC_STATICACC_SYNCHRONIZED 访问标志区分该方法是否静态同步方法。

总结

同步代码块和同步方法这两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

2.3为什么每个对象都能成为锁

Java中的每个对象都派生自Object类,而每个Java Object在JVM内部都有一个native的C++对象oop/oopDesc进行对应。线程在获取锁的时候,实际上就是获得一个监视器对象 (monitor),monitor可以认为是一个同步对象,所有的Java对象是天生携带monitor。因此,任何一个对象都可以成为一个锁。这也是为什么我们常说的“Java中每个对象都持有一把锁”,这是synchronized实现同步的基础。

三.公平锁和非公平锁

  • 公平锁:是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的。
    Lock lock = new ReentrantLock(true);//true 表示公平锁,先来先得
  • 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)。
    Lock lock = new ReentrantLock(false)://false 表示非公平锁,后来的也可能先获得锁
    Lock lock = new ReentrantLock();//默认非公平锁

为什么线程切换会导致用户态与内核台的切换?

因为线程的调度是在内核态运行的,而线程中的代码是在用户态运行。

公平锁执行流程

  1. 获取锁时,先将线程自己添加到等待队列的队尾并休眠
  2. 当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序

在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。

非公平锁执行流程

当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。

非公平锁因为不用按(顺)序执行,所以后来的锁也可以直接尝试获得锁,没有了阻塞和恢复执行的步骤,所以它的性能会更高。

四.可重入锁

是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。与可重入锁相反,不可重入锁不可递归调用,递归调用就会发生死锁。

隐式锁(synchronized)

在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的

同步代码块

同步方法

显式锁(Lock)

五.死锁及排查

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

纯命令

jps -l
  • jps 是 Java 开发工具包(JDK)提供的一个命令行工具,用于列出当前系统中正在运行的 Java 虚拟机(JVM)进程的信息。
  • -l:显示主类的全名,如果进程是通过 -jar 选项启动的,将显示 JAR 文件的路径。

jstack 进程编号
  • jstack 是 JDK 提供的一个用于生成 Java 线程堆栈跟踪的命令行工具。它通常用于分析 Java 应用程序的线程状态,查找线程死锁、性能问题等。jstack 主要用于定位和解决与多线程相关的问题。
  • 常见的 jstack 选项包括: -l:以长格式输出,显示关于锁的附加信息。

需要注意的是,jstack 可能会在生成堆栈跟踪时暂停目标 Java 进程的部分工作,因此在生产环境中使用时要谨慎,以避免对应用程序性能产生负面影响。

图形化

jconsole

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

用Java实现一对一聊天

目录 服务端 客户端 服务端 package 一对一用户; import java.awt.BorderLayout; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; imp…

Pytorch CIFAR10图像分类 Swin Transformer篇

Pytorch CIFAR10图像分类 Swin Transformer篇 文章目录 Pytorch CIFAR10图像分类 Swin Transformer篇4. 定义网络(Swin Transformer)Swin Transformer整体架构Patch MergingW-MSASW-MSARelative position biasSwin Transformer 网络结构Patch EmbeddingP…

NMOS管的工作状态及漏极电流与漏源电压、栅源电压之间的关系

NMOS管的工作状态及漏极电流与漏源电压、栅源电压之间的关系 以NMOS为例进行说明,PMOS管与NMOS管类似,只是正负极符号需要做些调整。 工作区域 对于NMOS管,根据 V g s V_{gs} Vgs​和 V d s V_{ds} Vds​大小的不同,MOS管可工作…

【牛牛送书 | 第三期】《一本书讲透Java线程:原理与实践》带你深入JAVA多线程

目录 摘要: 多线程对于Java的意义 为什么Java工程师必须掌握多线程 Java多线程使用方式 如何学好Java多线程 参与方式🥇 摘要: 互联网的每一个角落,无论是大型电商平台的秒杀活动,社交平台的实时消息推送&#x…

Kubernetes实战(九)-kubeadm安装k8s集群

1 环境准备 1.1 主机信息 iphostname10.220.43.203master10.220.43.204node1 1.2 系统信息 $ cat /etc/redhat-release Alibaba Cloud Linux (Aliyun Linux) release 2.1903 LTS (Hunting Beagle) 2 部署准备 master/与slave主机均需要设置。 2.1 设置主机名 # master h…

layui日历插件

layui日历插件: 在已开源的layui日历插件的基础上的改版(原版插件地址:https://gitee.com/smalldragen/lay-calender-mark)https://gitee.com/tangmaozizi/layui-calendar-plugin.gitjava后台代码并没有把项目完整结构上传上去,因…

解决electron修改主进程后需要重启才生效

nodemon 是一种工具,可在检测到目录中的文件更改时通过自动重新启动节点应用程序来帮助开发基于 node.js 的应用程序 nodemon 特性 自动重新启动应用程序。检测要监视的默认文件扩展名。默认支持 node,但易于运行任何可执行文件,如 python、…

3DMAX关于显示驱动问题的解决方法大全

3DMAX与显卡驱动有关的问题主要有以下几种情况: 1.3DMAX启动弹出这样的界面: 2.主工具栏按钮不显示,或者鼠标移上去才显示(刷新问题)。 3.视口菜单不显示或显示不全。 问题分析: 首先&#x…

【解密考研英语:Python数据分析与可视化】

解密考研英语:Python数据分析与可视化 背景数据集技术选型功能实现创新点 大家好,欢迎阅读我的CSDN博客!今天我将分享一项有关考研英语真题的数据分析与可视化项目,希望对考研学子提供更有针对性的复习帮助。 背景 作为考研学子…

cache教程 4.一致性哈希(hash)

本章节是单节点走向分布式节点的一个重要部分。 一致性哈希(consistent hashing)的原理以及为什么要使用一致性哈希。实现一致性哈希代码,添加相应的测试用例 1.多节点部署遇到的问题 上一章节完成了一个单节点的缓存服务器。那对于一个单节点来说,读…

【MATLAB】基于EEMD分解的信号去噪算法(基础版)

代码操作 【MATLAB】基于EEMD分解的信号去噪算法(基础版) 代码的主要内容 基于EEMD(集合经验模态分解)的信号去噪算法通常可以结合相关系数、信号的熵值或者方差贡献率来完成去噪处理。这些指标可以用于确定阈值,从而…

【视频笔记】古人智慧与修行

古人的智慧 相由心生、老子悟道、佛祖成佛 多一些思考,多一些精神修炼。 除非我们今天能够产生与人类科技发展相并行的精神变革,否则永远可能也无法跳脱出历史的轮回。 视频来源