3. Java中的锁

文章目录

  • 乐观锁与悲观锁
    • 乐观锁(无锁编程,版本号机制)
    • 悲观锁
    • 两种锁的伪代码比较
  • 通过 8 种锁运行案例,了解锁
    • 锁相关的 8 种案例演示
      • 场景一
      • 场景二
      • 场景三
      • 场景四
      • 场景五
      • 场景六
      • 场景七
      • 场景八
    • synchronized 有三种应用方式
      • 8 种锁的案例实际体现在 3 个地方
    • 从字节码角度分析 synchronized 实现
      • `javap -c ****.class`文件反编译
      • synchronized 同步代码块
      • synchronized 普通同步方法
      • synchronized 静态同步方法
    • 对于 synchronized 的深入研究
      • 面试题:为什么任何一个对象都可以成为一个锁
      • 什么是管程 monitor
  • 公平锁与非公平锁
    • 为什么会有公平锁和非公平锁的设计?
    • 为什么默认使用非公平锁?
    • 什么时候用公平锁?什么时候用非公平锁?
  • 可重入锁,又叫,递归锁
    • 隐式锁(synchronized 默认是可重入锁)
      • 同步块
      • 同步方法
    • synchronized 的可重入原理(基于 objectMonitor.hpp)
    • 显示锁(Lock,ReentrantLock)
  • 死锁以及排查
    • 死锁是什么
    • 编写一个死锁 case
    • 死锁的故障排查
  • 小结
  • 见后续
    • 自旋锁 SpinLock
    • 无锁->独占锁->读写锁->邮戳锁
    • 无锁->偏向锁->轻量锁->重量锁

乐观锁与悲观锁

乐观锁(无锁编程,版本号机制)

  • 认为自己在使用数据时,不会有别的线程修改数据或资源,所以不会加锁 。
  • 在 Java 中通过使用无锁编程来实现,只在更新数据时去判断,之前是否存在其它线程更新此数据。
    • 如果这个数据没有被更新,当前线程将自己修改的数据成功写入
    • 如果这个线程数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
      • 放弃修改,尝试抢锁等
  • 判断规则
    • 版本号机制 Version
    • 最常采用的是 CAS 算法,Java 原子类的递增操作就通过 CAS 自旋实现的
  • 适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升
  • 乐观锁直接去操作同步资源,是一种无锁算法
  • 乐观锁的两种实现方式
    • 采用 Version 版本号机制
    • CAS(Compare-and-Swap,比较替换算法) 实现

悲观锁

  • 认为自己在使用数据时,必然有别的线程来修改数据,因此在获取到数据的时候,进行操作之前,会先加锁,保证数据不被别的线程所修改
  • synchronized 关键字与 Lock 锁的实现类均为悲观锁
  • 适合写操作多的场景,先加锁可以保证写操作时数据正确,
  • 显示锁定之后再进行同步资源

两种锁的伪代码比较

  • 悲观锁
 //悲观锁基于synchronized关键字public synchronized void m1(){//code logic segment....}//悲观锁基于Lock对象实现ReentrantLock reentrantLock = new ReentrantLock();public void m2(){reentrantLock.lock();try {//code logic segment....} finally {reentrantLock.unlock();}}
  • 乐观锁
        //乐观锁的调用方式,保证多个线程使用的是同一个AtomicIntegerAtomicInteger atomicInteger = new AtomicInteger();atomicInteger.incrementAndGet();

通过 8 种锁运行案例,了解锁

锁相关的 8 种案例演示

场景一

a,b 两个线程分别使用同步监视器修饰
a 线程启动,0.2 秒后 b 线程启动
可以预见,先执行 a 后执行 b

  • 代码
class Phone {//资源类public synchronized void sendEmail() {System.out.println(Thread.currentThread().getName() + "-----sendEmail");}public synchronized void sendSMS() {System.out.println(Thread.currentThread().getName() + "-----sendSMS");}
}...
public static void main(String[] args) {//一切程序的入口Phone phone = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone.sendSMS();}, "b").start();}
  • 效果

image.png

场景二

在场景一的资源类中,sendEmail方法中加入暂停3秒钟
由于 synchronized 是悲观锁,并且 sleep 不回释放锁,因此 a 线程先执行,并且执行 sleep 时程序也会阻塞,当 a 线程执行完毕时,b 线程才会执行
可以预见结果和场景一致

  • 代码
    public  synchronized void sendEmail() {try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "-----sendEmail");}
  • 效果

image.png

一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法

场景三

添加一个普通的hello方法,先打印邮件还是hello?
普通方法线程共享

class Phone {//资源类....public void hello() {System.out.println("-------hello");}....
}//main方法public static void main(String[] args) {//一切程序的入口Phone phone = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone.hello();}, "b").start();}
  • 效果

image.png

场景四

有两部手机,请问先打印邮件还是短信
两次方法的调用 synchronized 锁住的对象不同

  • 代码
//资源类不变public static void main(String[] args) {//一切程序的入口Phone phone = new Phone();Phone phone2 = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone2.sendSMS();}, "b").start();}
  • 效果

image.png

场景五

在场景 一的情况下将两个方法均添加 static 修饰,测试代码同场景一
静态同步方法(类锁)

  • 代码
    public static synchronized void sendEmail() {try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "-----sendEmail");}public static synchronized void sendSMS() {System.out.println(Thread.currentThread().getName() + "-----sendSMS");}
  • 效果

image.png

场景六

在场景五的情况下,添加一部手机,用 phone2 调用 sentSMS

  • 代码
//资源类同场景五
//测试代码public static void main(String[] args) {//一切程序的入口Phone phone = new Phone();Phone phone2 = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone2.sendSMS();}, "b").start();}
  • 效果

image.png

对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——>实例对象本身(方法调用者),
对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板(调用者所属的类型)
对于同步方法块,锁的是 synchronized 括号内的对象

场景七

有1个静态同步方法,有1个普通同步方法,有1部手机,请问先打印邮件还是短信

    //将短信方法还原为普通同步方法public static synchronized void sendSMS() {System.out.println(Thread.currentThread().getName() + "-----sendSMS");}
  • 测试代码同场景一
    public static void main(String[] args) {//一切程序的入口Phone phone = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone.sendSMS();}, "b").start();}
  • 效果

image.png

类锁与对象锁不是同一个,各种执行,a 线程睡眠 3 秒

场景八

有1个静态同步方法,有1个普通同步方法,有2部手机,请问先打印邮件还是短信

//资源类同场景七
//测试代码public static void main(String[] args) {//一切程序的入口Phone phone = new Phone();Phone phone2 = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone2.sendSMS();}, "b").start();}
  • 效果

image.png

当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。
所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this
也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。

synchronized 有三种应用方式

8 种锁的案例实际体现在 3 个地方

  • 作用于实例方法,为当前方法调用者加锁,进入同步代码前需要获得当前实例的锁
  • 作用于代码块,synchronized(obj){},obj 为加锁对象
  • 作用于静态方法,当前类加锁,进入同步代码块之前需要获取类对象的锁

从字节码角度分析 synchronized 实现

javap -c ****.class文件反编译

  • -c 作用: 对代码进行反编译
  • -v (verbose) 作用 输出附加信息(行号,本地变量表,反编译等详细信息)

synchronized 同步代码块

  • 编写测试代码->运行产生 class 文件->进入 class 类路径–>执行 javap -c
Object object = new Object();public void m1() {synchronized (object) {System.out.println("----hello synchronized code block");}}
  • 反编译结果
    • 一般情况下,一个 enter 对应 2 个 exit

image.png

  • 极端情况==>手动抛出一个异常
Object object = new Object();public void m1() {synchronized (object) {System.out.println("----hello synchronized code block");throw new RuntimeException("-----exp");}
}

image.png

  • synchronized 同步代码块的实现
    • 进入锁使用 monitorenter 指令
    • 退出锁使用 monitorexit 指令

synchronized 普通同步方法

    public synchronized void m2() {System.out.println("----hello synchronized m2");}
  • 使用javap -v .\LockSyncDemo.class进行编译

image.png

  • 调用指令时检查方法的 ACC_SYNCHRONIZED 访问标志位是否被设置
    • 若被设置了,则线程会将先持有 monitor 锁,然后再执行方法
    • 最后在方法完成时(无论正常完成还是非正常完成)均会释放 monitor

synchronized 静态同步方法

public static synchronized void m3() {System.out.println("----hello static synchronized m3");
}
  • 执行反编译

image.png

  • ACC_STATIC, ACC_SYNCHRONIZED 访问标志位区分该方法是否为静态同步方法

对于 synchronized 的深入研究

面试题:为什么任何一个对象都可以成为一个锁

什么是管程 monitor

  • HotSport 虚拟机中,monitor 采用 ObjectMonitor 实现
  • C++源码执行过程

  • Object 底层实现基于 ObjectMonitor.cpp,所有类默认继承自 Object 类,因此每个对象天生就带着一个 monitor 对象
  • 每一个锁住的对象均会与 monitor 进行关联
  • objectMonitor.hpp 中的源码片段
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {_header       = NULL;_count        = 0;			//记录该线程获取锁的次数_waiters      = 0,recursions   = 0;			//锁的重入次数_object       = NULL;_owner        = NULL;	    //指向持有ObjectMonitor对象的线程_WaitSet      = NULL;		//存放的处于wait状态的线程队列_WaitSetLock  = 0 ;			_Responsible  = NULL ;_succ         = NULL ;_cxq          = NULL ;FreeNext      = NULL ;_EntryList    = NULL ;		//存放处于等待锁block状态的线程队列_SpinFreq     = 0 ;_SpinClock    = 0 ;OwnerIsThread = 0 ;_previous_owner_tid = 0;
}

公平锁与非公平锁

公平锁是指多线程按照申请锁的顺序来获取锁,先来先得
Lock lock = new ReentrantLock(true); //true表示先来先得


非公平锁是指,获取锁的顺序不是按照申请锁的顺序
存在后申请的线程比先申请的线程优先获取锁
在高并发环境下,存在优先级翻转或者锁饥饿的状态
锁饥饿 : 某个线程长时间得不到锁
Lock lock = new ReentrantLock(false); //false 表示非公平锁,并发抢锁
Lock lock = new ReentrantLock(); //默认非公平锁

为什么会有公平锁和非公平锁的设计?

  • 恢复挂起的线程到真正锁的获取还是有时间差的
  • 对 CPU 而言时间差较为明细,非公平锁能够更充分利用 CPU 的时间片,减少 CPU 空闲时间

为什么默认使用非公平锁?

  • 使用多线程的一个考量点就是线程切换的开销
    • 采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,则刚释放锁的线程在此时再次获得同步状态的概率就变得非常大,因此减少了线程的开销

什么时候用公平锁?什么时候用非公平锁?

  • 为了提高系统的吞吐量,提升性能,减少不必要的时间开销,应选择非公平锁
  • 公平锁需要结合具体场景进行讨论

可重入锁,又叫,递归锁

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

  • Java 中 synchronized 和 ReentrantLock 都是可重入锁
  • 可重入锁可以一定程度上避免死锁
  • 可重入锁,即可多次进入同步域==>同步代码块,同步方法,lock()与 unlock()包裹的区域

隐式锁(synchronized 默认是可重入锁)

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

同步块

    private static void reEntryM1() {final Object object = new Object();new Thread(() -> {synchronized (object) {System.out.println(Thread.currentThread().getName() + "\t ----外层调用");synchronized (object) {System.out.println(Thread.currentThread().getName() + "\t ----中层调用");synchronized (object) {System.out.println(Thread.currentThread().getName() + "\t ----内层调用");}}}}, "t1").start();}
  • 在 main 方法中进行调用,测试结果

image.png

同步方法

     public synchronized void m1() {//指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。System.out.println(Thread.currentThread().getName()+"\t ----come in");m2();System.out.println(Thread.currentThread().getName()+"\t ----end m1");}public synchronized void m2(){System.out.println(Thread.currentThread().getName()+"\t ----come in");m3();}public synchronized void m3(){System.out.println(Thread.currentThread().getName()+"\t ----come in");}//main方法调用public static void main(String[] args){ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();new Thread(() -> {reEntryLockDemo.m1();}, "t1").start();}
  • 效果

image.png

synchronized 的可重入原理(基于 objectMonitor.hpp)

  • 每个锁对象都拥有一个锁计数器和一个指向持有锁的线程指针
    • 当执行 monitorenter 时,如果目标锁的计数器为零,那么没有被其他线程所持有,JVM 会将该锁对象持有的线程设置为当前线程,并将计数器加 1
    • 在目标锁对象的计数器不为零的情况下,如果锁的持有线程是但前线程,那么 JVM 可以将其计数器加 1,否则进入等待,直至持有线程释放该锁
    • 当执行 monitorexit 时,JVM 将锁对象的计数器减 1,计数器为 0 表示锁已被释放

显示锁(Lock,ReentrantLock)

    new Thread(() -> {lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t ----come in内层调用");}finally {lock.unlock();}}finally {// 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。lock.unlock();// 正常情况,加锁几次就要解锁几次}},"t1").start();new Thread(() -> {lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");}finally {lock.unlock();}},"t2").start();
  • 效果

image.png

  • lock()与 unlock()未一一匹对

image.png

  • 效果

image.png

  • t1 外层未释放锁,t2 陷入持续等待…

死锁以及排查

死锁是什么

参考往期文章

编写一个死锁 case

 public static void main(String[] args) {final Object objectA = new Object();final Object objectB = new Object();new Thread(() -> {synchronized (objectA) {System.out.println(Thread.currentThread().getName() + "\t 自己持有A锁,希望获得B锁");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (objectB) {System.out.println(Thread.currentThread().getName() + "\t 成功获得B锁");}}}, "A").start();new Thread(() -> {synchronized (objectB) {System.out.println(Thread.currentThread().getName() + "\t 自己持有B锁,希望获得A锁");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (objectA) {System.out.println(Thread.currentThread().getName() + "\t 成功获得A锁");}}}, "B").start();}

死锁的故障排查

  • jps -l
  • jstack pid

image.png
image.png

  • jconsole

image.png

小结


见后续

自旋锁 SpinLock

无锁->独占锁->读写锁->邮戳锁

无锁->偏向锁->轻量锁->重量锁


在这里插入图片描述

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

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

相关文章

移动端学习:如何把exe转换成apk

exe转换成apk是怎么实现的呢?-电脑端-一门科技将exe文件转换成apk文件是一个比较常见的需求,尤其是对于一些开发者和用户来说。但是,这个过程并不是简单的复制和粘贴。在本文中,我们将介绍exe转换成apk的原理和详细介绍。首先,我们需要了解什么https://www.yimenapp.net/k…

前端sql条件拼接js工具

因为项目原因&#xff0c;需要前端写sql&#xff0c;所以弄了一套sql条件拼接的js工具 ​ /*常量 LT : " < ", LE : " < ", GT : " > ", GE : " > ", NE : " ! ", EQ : " ", LIKE : " like &qu…

【Excel PDF 系列】EasyExcel + iText 库

你知道的越多&#xff0c;你不知道的越多 点赞再看&#xff0c;养成习惯 如果您有疑问或者见解&#xff0c;欢迎指教&#xff1a; 企鹅&#xff1a;869192208 文章目录 前言转换前后效果引入 pom 配置代码实现定义 ExcelDataVo 对象主方法EasyExcel 监听器 前言 最近遇到生成 …

2024程序员容器化上云之旅-第4集-Ubuntu-WSL2-Windows11版:夺取宝剑

故事梗概 Java程序员马意浓在互联网公司维护老旧电商后台系统。 渴望学习新技术的他在工作中无缘Docker和K8s。 他开始自学Vue3并使用SpringBoot3完成了一个前后端分离的Web应用系统&#xff0c;并打算将其用Docker容器化后用K8s上云。 6 夺取宝剑 &#x1f525;阅读Nigel…

FullCalendar日历组件:进行任务增删改,参考gitee例子修改

效果 参考路径 zxj/FullCalendar开发示例 - 码云 - 开源中国 (gitee.com) 代码 主页面&#xff1a;index.php <?php ob_start(); include(includes/session.inc); ?> <!DOCTYPE html> <html><head><title>日程管理</title><meta …

Vue-Cropper头像裁剪插件使用

1. 效果预览 2. 插件介绍 官网地址&#xff1a;[GitHub - xyxiao001/vue-cropper: A simple picture clipping plugin for vue](https://github.com/xyxiao001/vue-cropper?fromthosefree.com) 3 . 插件使用 下载插件 npm install vue-croppernext3 . 封装好的代码&#x…

华为云软件开发生产线CodeArts前端DevOps实践

原文链接&#xff1a;CodeArts前端DevOps实践_软件开发生产线 CodeArts_理论实践_DevOps概览 本文主要以CodeArts产品自身为背景&#xff0c;简要介绍一些在前端性能优化方面的优秀实践方法和常见问题。 在开始本文的内容之前&#xff0c;先简单介绍一下华为云CodeArts。Code…

Vue 实现页面导出A4标准大小的PDF文件,以及处理图片跨域不能正常展示的问题等

效果预览&#xff1a; 代码流程&#xff1a;首先在utils文件夹下创建htmlToPdf的js工具文件&#xff0c;然后在main.js中注册引用 htmlToPdf.js // 导出页面为PDF格式 import html2Canvas from html2canvas import JsPDF from jspdfexport default {install(Vue, options) {V…

【底层解读】ArrayList源码学习

成员变量 学习源码前&#xff0c;我们还是先看一下ArrayList中成员变量有哪些 构造函数 ArrayList一共有三个构造函数。 第一个&#xff1a;带有指定初始容量的构造函数 第二个&#xff1a;空参构造 第三个&#xff1a;包含指定集合的构造函数 OK&#xff0c;看完构造函数&a…

[算法沉淀记录] 排序算法 —— 堆排序

排序算法 —— 堆排序 算法基础介绍 堆排序&#xff08;Heap Sort&#xff09;是一种基于比较的排序算法&#xff0c;它利用堆这种数据结构来实现排序。堆是一种特殊的完全二叉树&#xff0c;其中每个节点的值都必须大于或等于&#xff08;最大堆&#xff09;或小于或等于&am…

The Grapes NFT 概览与数据分析

作者&#xff1a;stellafootprint.network 编译&#xff1a;cicifootprint.network 数据源&#xff1a;The Grapes NFT Collection Dashboard The Grapes 是一个有趣且具有吸引力的 NFT 收藏集合&#xff0c;包含 3,333 个精心制作的 NFT。这个 NFT 项目会在 2024 年再创高…

Docker Volume

"Ice in my vein" Docker Volume(存储卷) 什么是存储卷? 存储卷就是: “将宿主机的本地文件系统中存在的某个目录&#xff0c;与容器内部的文件系统上的某一目录建立绑定关系”。 存储卷与容器本身的联合文件系统&#xff1f; 在宿主机上的这个与容器形成绑定关系…