发布时间:2026/9/4 22:54:11
多线程(4) 上一篇我们已经知道了synchronized加锁来解决线程安全问题修改操作不是原子的synchronized修饰普通方法相当于给this进行加锁synchronized修饰静态方法相当于给类对象加锁。synchronized-监视器锁 monitor lockJVM中采用的一个术语使用锁的过程中抛出一些异常可能会看到监视器锁这样的报错信息。1 synchronized的特性1互斥多个线程针对同一个对象加锁才会产生互斥锁冲突/锁竞争这个上一篇已经说过了2可重入要想理解可重入我们先来了解一下死锁的一种情况直接的代码示例在这段代码中第二次synchronized就得阻塞等待等到第一次加锁被释放第二次加锁的阻塞才会继续执行。看起来是两次一样的加锁没有必要但是在实际开发中很容易写出这样的代码比如这样的代码我们稍不注意就会写出来。这样的代码第一次加锁是可以成功的因为锁没有被使用第二次进行加锁锁是被占用的状态就得阻塞等待。要想解除阻塞需要往下执行才可以要想往下执行就需要等到第一次的锁被释放……这样的问题就成为“死锁”。值得注意的是这只是死锁的一种情况一个线程一把锁连续加锁多次。其他的情况稍后再说死锁是一个非常严重的bug会使代码执行到这一块之后就会被卡住。为了解决上述的问题Java的synchronized就引入了可重入的概念。可重入当某个线程针对一个锁加锁成功之后后续该线程再次针对这个锁进行加锁。不会触发阻塞而是继续往下走。但是如果是其他线程尝试加锁就会正常阻塞。可重入锁的实现原理关键在于让锁对象内部保存当前是哪个线程持有的这把锁。后续有线程针对这把锁加锁的时候进行对比看锁持有的线程是否和当前加锁的线程是同一个可以连续加锁多次最外层是真正加锁最外层也是真正解锁。站在JVM的视角看到多个}需要执行那它如何知道哪个}是真正解锁的那个会先引入一个变量计数器。每次触发{计数器每次触发}计数器--当计数器-- 为0的时候就是真正需要解锁的时候。那如何实现一个可重入锁呢1在锁内部记录当前是哪个线程持有的锁后续每次加锁都进行判定2通过计数器记录当前加锁的次数从而确定何时真正解锁2 关于死锁上面已经知道了一种情况一个线程一把锁连续加锁多次第二种情况两个线程两把锁每个线程获取到一把锁之后尝试获取对方的锁代码示例public static void main(String[] args) throws InterruptedException { Object locker1 new Object(); Object locker2 new Object(); Thread t1 new Thread(()-{ synchronized (locker1){ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (locker2){ System.out.println(t1线程两个锁都获取到); } } }); Thread t2 new Thread(()-{ synchronized (locker2){ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (locker1){ System.out.println(t2线程两个锁都获取到); } } }); t1.start(); t2.start(); t1.join(); t2.join(); }值得注意的是必须是拿到第一把锁再拿第二把锁。必须是嵌套的关系这样的代码就构成了死锁那如果不加sleep,是否还会出现一样的现象呢那就得看具体的调度的顺序了。咱们加上sleep是为了确保t1拿到locker1t2拿到locker2等待1秒t1尝试拿locker2t2尝试拿locker1。如果不加sleep很可能t1一口气把locker1和locker2都拿了这个时候t2还没开动呢自然就构不成死锁。第三种情况N个线程M把锁一个经典的模型哲学家就餐问题哲学家相当于线程。筷子相当于锁。大部分情况下这个模型可以很好的运转只有在一些极端情况下会造成死锁。极端情况同一时刻大家都想吃面条同时拿起了左手边的筷子如上图所示此时任何一个线程都无法拿起右手边的筷子任何一个哲学家都吃不成面条那如何避免代码中出现死锁呢要想避免得先知道死锁是如何构成的构成死锁的四个必要条件重点1锁是互斥的一个线程拿到锁之后另一个线程在尝试获取锁就得阻塞等待2锁是不可抢占不可剥夺的线程1拿到锁线程2也尝试获取这个锁线程2必须阻塞等待而不是线程2直接把锁抢过来1和 2是锁的基本特性Java的synchronized是遵守这两点的3请求和保持一个线程拿到锁1之后在不释放锁1的情况下获取锁2在上面的哲学家的极端情况中如果先放下左手的筷子再拿右手的筷子就不会够成死锁那可能就说了代码中加锁的时候不去嵌套不就可以了。但是这种做法不是通用的有些时候就得嵌套4循环等待多个线程多把锁之间的等待过程构成了“循环”。A等待BB也等待A或者A等待BB等待CC等待A解决办法约定好加锁的顺序就可以破除循环等待了比如约定每个线程加锁的时候永远是先获取序号小的锁后获取序号大的锁代码示例运行结果避免死锁上述1和 2的情况是不可避免的3可以把嵌套的锁改为并列的锁4对加锁的顺序作出约定3 内存可见性是造成线程安全问题的原因之一代码示例运行结果我们可以看到虽然输入了非0的值但是此时t1线程循环并没有结束。很明显这是个bug是线程安全问题。一个线程读取一个线程修改修改线程修改的值但并没有被线程读取到这就是内存可见性问题。要想知道内存可见性问题得先谈谈编译器优化我们知道程序员的水平是参差不齐的研究JDK的大佬们就希望通过让编译器 JVM对程序员写的代码自动的进行优化。本来程序员写的代码是xxxx编译器/JVM会在你原有逻辑不变的前提下对你的代码进行调整使你的程序效率更高。值得注意的是编译器虽然声称优化操作是能够保证逻辑不变。但是在多线程的程序中编译器的判断可能会出现失误可能导致优化后的逻辑和优化前的逻辑出现细节上的偏差对于cmp这样的指令条件跳转load是读内存操作cmp是纯cpu寄存器操作load的时间开销可能是cmp的几千倍。短时间之内这个循环就会循环很多次执行过程中JVM就能感知到load反复执行的结果好像是一样的。这时JVM心想我执行这么多次读flag的操作值始终都是0既然都是一样的结果何必要反复执行这么多次呢。这个flag的值取决于用户输入不知道用户过多久才能输入于是就把读内存的操作优化成读取寄存器的操作把内存的值读到寄存器了后续再load就不再重新读内存了而是直接从寄存器里取。于是等到很多秒之后用户真正输入新的值真正修改flag此时t1线程就感知不到了编译器优化使得t1线程的读操作不是真正读内存如果我们微调上述代码就会得到不一样的结果代码示例运行结果本来这个while循环转的飞起1秒钟转几千万次……但是加了sleep(1)之后循环次数大幅度下降了当引入sleep之后sleep消耗的时间相比于上面的load flag的操作就不知道高了多少了。但是针对内存可见性问题不能指望通过sleep来解决因为使用sleep会大大影响到程序的效率。那该如何解决呢在语法中引入了volatile这个关键字通过这个关键字来修饰某个变量此时编译器通过对这个变量的读取操作就不会优化成读寄存器。代码示例运行结果OK啦到此结束

相关新闻

2026/9/4 22:54:11

Linux基础开发工具:从软件安装到程序编译全攻略

🌈个人主页:ꪔ小林Y ✨个人专栏:《C小白闯关日记》,《C语言小白闯关日记》,《数据结构入门——从原理到实战》,《拾光Linux》 🍀代码信条:每一行代码都是成长的脚印👣,每…

2026/9/4 22:54:11

Runway Solaris:界面世界模型如何为GUI Agent提供“预演”能力?

如果你一直在关注最近一两年的 AI Agent,应该见过不少“能自动操作电脑”的演示视频。模型截一张屏幕截图,把按钮和输入框识别出来,然后像人一样移动鼠标、敲键盘、完成任务。但这个方向真正落地时,最大的瓶颈往往不是模型不会点击…

2026/9/4 23:59:41

开源低代码平台ToolJet实战:快速构建企业内部管理后台

企业内部经常会出现这样的需求:运营需要一个数据看板,测试需要一批测试账号管理页面,客服需要查询用户订单,财务需要核对账单状态。这类系统通常功能不算复杂,但“麻雀虽小五脏俱全”,如果每一套都由开发从…

2026/9/4 23:59:41

YOLOv8课堂行为检测系统:小目标识别与边缘部署实战

简介:本资源是一套基于YOLOv8的课堂学生行为检测系统完整实现,面向计算机、人工智能、教育技术等专业的本科生与研究生,适用于课程设计、毕业设计及教学实训场景,解决课堂教学中学生姿态、专注度、异常行为等自动化识别问题。压缩…

2026/9/4 23:59:41

niri 笔记本低功耗调优:3 步把待机功耗从 8W 压到 5W

niri 笔记本低功耗调优:3 步把待机功耗从 8W 压到 5W 【免费下载链接】niri A scrollable-tiling Wayland compositor. 项目地址: https://gitcode.com/GitHub_Trending/ni/niri 在 niri 这款 Wayland 合成器(负责把每个窗口画面拼合成最终屏幕图…

2026/9/4 23:59:41

自托管PaaS实战:Docker Compose+Caddy搭建部署平台

很多开发者刚开始接触 Vercel、Railway 这类平台时,往往会被它们的自动化程度惊艳到:项目推到 Git 仓库,平台自动完成构建、发布、HTTPS 证书签发,甚至还能一键创建数据库和定时任务。这种体验确实流畅,基本把“部署”…

2026/9/4 23:59:41

ToolJet深度解析:开源低代码平台核心能力与实战指南

深入解析 ToolJet:开源低代码平台的核心能力与实战上手指南 1. ToolJet 是什么?为什么低代码平台需要重新被审视 2. ToolJet 的核心功能拆解 2.1 可视化应用构建器 2.2 数据源连接与查询管理 2.3 前端组件与事件交互 2.4 权限管理与协作能力 3. 环…

2026/9/3 18:28:26

vSound小提琴数字处理器实操指南:从接线到演出的完整配置

电小提琴或者原声小提琴插电演出,第一个绕不开的坎就是声音难听。原声琴的共鸣和空气感一旦进了拾音器,出来的往往是一坨干瘪、发尖、带着奇怪塑料味的信号。我当初第一次把琴接上乐队调音台,直接被主唱吐槽"你这声音像在锯钢丝"。…

2026/9/3 14:29:47

传感器接口IC如何攻克生物化学传感的微弱信号难题?

1. 从电极到比特流:为什么生物化学传感必须依赖专用接口IC 做生物化学传感的人都有过类似的经历:明明传感器本身性能很好,信号输出却一塌糊涂——噪声大、漂移明显、重复性差,怎么调都达不到预期。很多时候问题并不在传感器&#…

2026/9/3 14:30:35

STM32F411CEU6多通道ADC采集:扫描模式+DMA实现详解

1. 多通道 ADC 的用武之地把“Multichannel ADC”和“STM32F411CEU6”这两个关键字放在一起,其实就是嵌入式开发里最常遇到的一类需求:用一块不算贵的 MCU,同时采集多路模拟信号。STM32F411CEU6 是 48 引脚的 Cortex-M4F 主控,主频…

2026/9/3 20:43:36

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/3 17:51:43

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/3 21:06:57

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…