当前位置: 首页 > news >正文

【Java】线程安全问题

一.线程安全概念

实际结果和预期结果不一样,就是线程不安全

多线程带给我们效率提升的同时,也为我们带来了风险,因为多线程的抢占式执行,带来的随机性。

count++看起来是一行代码,实际上对应了三个cpu指令

++操作本质上要分为三步
1.load指令:先把内存中的count值读取到CPU的寄存器中
2. add指令:把CPU寄存器里的数值+1操作
3.save指令:把寄存器中的内容保存回内存上


我们想使用两个线程将一个变量同时增加5000次

此时两个线程的并发执行,就相当于两组load   add     save指令并发执行。所以这两组指令的执行顺序存在了许多可能性。

package Thread;
public class Demo1{private static int count=0;public static void main(String[] args)throws InterruptedException{Object obj=new Object();Thread t1=new Thread(()->{for(int i=0;i<5000;i++){count++;}});Thread t2=new Thread(()->{for(int i=0;i<50000;i++){count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

每运行一次,结果都是不同的,如下所示,这是我分别运行三次的结果

推演图(有很多种可能性)

一个线程的load必须在一个线程的save之后

二.线程安全问题原因和如何解决线程安全问题

最根本的原因: 抢占式执行,随机调度

我们上述线程代码之所以不安全,因为涉及到我们两个线程同时去修改一个相同的变量。
一个线程修改一个变量,安全
多个线程读取同一个变量,安全
多个线程修改多个不同的变量,安全

1.根本:操作系统对于线程的调度是随机的,抢占式执行

操作系统的底层设定,咱们左右不了

2.多线程同时修改同一个变量

和代码的结构直接相关,可以通过调整代码结构,规避一些线程不安全的代码

(不够通用,有的情况下,有时需要多线程修改同一个变量)

3.修改操作不是原子的

(java中解决线程安全问题,最主要的方案)

计算机中的锁和生活中的锁,是同样的概念,互斥/排他

加锁,让不是原子的,打包成一个原子的操作

一旦把锁加上了,其他人要想加锁,就得阻塞等待。

(eg:上厕所关门)不允许暴力拆锁

加锁操作,不是把线程锁死到 cpu 上,禁止这个线程被调度,而是禁止其他线程重新加这个锁,避免其他线程的操作在当前线程执行过程中,插队

在代码中如何实现呢?如下所示

用synchronized(){}将count++包裹起来

然后再写一个locker对象,更改后的代码块如下所示

 Thread t1=new Thread(()->{for(int i=0;i<50000;i++){synchronized(locker){count++;}}});Thread t2=new Thread(()->{for(int i=0;i<50000;i++){synchronized(locker){count++;}}});

两个线程,针对同一个对象加锁,才会产生互斥效果,(一个线程加上锁了,另一个线程就得阻塞等待,等到第一个线程释放锁,才有机会)

运行结果为

三.Synchronized监管锁monitor lock

在 Java 中,synchronized 关键字的底层实现依赖于监视器锁(Monitor Lock),它是一种基于 JVM 内置的同步机制,用于保证多线程环境下共享资源的原子性、可见性和有序性。当使用 synchronized 时若出现异常,错误信息中常提到 “监视器锁”(如 IllegalMonitorStateException),这与它的特性和使用规则直接相关。

使用锁的时候抛出一些异常,可能就会看到监视器锁这样的报错信息

监视器锁(Monitor Lock)的核心特性

1. 可重入性(Reentrancy)

  • 定义:一个线程已经获取了某个对象的监视器锁后,再次请求该对象的锁时可以直接获取(无需重新竞争),即允许同一线程多次持有同一把锁。
  • 作用:避免死锁(例如递归调用中,线程不会因重复获取自己已持有的锁而阻塞)。

代码示例

(线程调用 methodA 获取锁后,调用 methodB 时可直接重入,无需等待。)

package Threadtest;
//synichronized 的可重入性代码举例class Counter2 {private int count = 0;public void add() {synchronized (this) { // 第一把锁:thiscount++;}}public int get() {return count;}}public class demo3 {public static void main(String[] args) throws InterruptedException {Counter2 counter = new Counter2();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized(counter){counter.add();}}});t1.start();t1.join();System.out.println("count = " + counter.get());}}

2. 其他关键特性

  • 独占性:同一时间只能有一个线程持有监视器锁,其他线程尝试获取时会被阻塞(进入等待队列)。
  • 释放机制:锁的释放是自动的 —— 当同步代码块执行完毕、抛出未捕获异常或执行 wait() 时,线程会释放锁。
  • 关联对象:监视器锁与具体对象绑定(synchronized 修饰非静态方法时锁是 this,修饰静态方法时锁是类对象 Class)。

四.死锁的出现原因

两次加锁

死锁(间接与监视器锁相关)

死锁代码示例是在synichronized可重入性的代码示例上的做的修改,可以通过观察两者的区别,更好的学习死锁

触发原因:多个线程相互持有对方需要的锁,且都不释放,导致无限阻塞。

本质:监视器锁的独占性导致资源竞争无法化解。

代码触发死锁,线程就卡住了

构成死锁的原因

(1)锁是互斥的

(2)锁不可抢占/剥夺  

注:1/2都是锁的基本特性,所以只能通过破坏3/4打破死锁

(3)请求和保持--保持锁状态的前提下,再请求另一把锁

(4)循环等待

死锁是怎么解决的

(1)加锁的时候,不要嵌套,即避免锁嵌套(打破3

(2)约定加锁顺序(打破4,如何约定,下文中有举例解释

(3)银行家算法

class Counter2 {private int count = 0;// 新增第二把锁private final Object lock2 = new Object();public void add() {synchronized (this) { // 第一把锁:thissynchronized (lock2) { // 第二把锁:lock2count++;}}}public int get() {return count;}
}public class Demo19 {public static void main(String[] args) {Counter2 counter = new Counter2();// 线程1:先拿this锁,再等lock2锁Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.add();}});// 线程2:先拿lock2锁,再等this锁Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (counter.lock2) { // 先获取lock2锁synchronized (counter) { // 再尝试获取this锁// 此处可执行任意操作(如空逻辑),关键是锁的顺序}}}});t1.start();t2.start();// 等待线程执行(实际死锁时此代码不会执行到,但用于演示结构)try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("count = " + counter.get());}
}

死锁经典问题:哲学家就餐问题

描述 5 位哲学家围坐在圆桌旁,每人左右各有一根筷子,思考时放下筷子,就餐时需要同时拿起左右两根筷子。若每位哲学家同时拿起左手筷子,再等待右手筷子,会导致所有人都持有一根筷子并等待另一根,形成死锁。

解决该问题的方案(破坏死锁条件)

死锁的 4 个必要条件:互斥、持有并等待、不可剥夺、循环等待。只需破坏其中一个即可避免死锁,以下是两种经典方案:

方案 1:固定筷子获取顺序(破坏 “循环等待”)

让所有哲学家先拿编号小的筷子,再拿编号大的筷子,避免循环等待。

方案 2:最多允许 4 位哲学家同时拿筷子(破坏 “持有并等待”)

限制同时拿筷子的哲学家数量,保证至少有一位能拿到两根筷子,用完后释放资源。

五.什么是原子性?

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。

不保证原子性会给多线程带来什么问题?

一个线程正在对一个变量操作,中途其他线程插进来了,对这个操作造成了打断,可能会造成结果的错误。这和线程的抢占式调度有关,如果不是抢占式,就算不是原子性,也问题不大。

六.内存可见性


ava内存模型(JMM): java虚拟机规定了java内存模型。
目的: 屏蔽各种硬件和操作系统的内存访问差异,实现java程序在各平台下都达成一致的并发效果

这个时候代码就会出现问题。

七.指令重排序


指令重排序实际上也是编译器优化,简单的来说,就是我们把一个东西写的太烂了,JVM.CPU指令集会对其进行优化。
编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价

八.内存可见性

什么是可见性

可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到

共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量

volatile如何实现内存的可见性

当编译器优化处bug,使用这个关键字修饰的变量,就属于“易失”,必须每次重新读取内存

深入来说:通过加入内存屏障和禁止重排序优化来实现的

对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
对volatile变量执行读操作时,会在读操作前加入一条load屏障指令

总结 volatile 核心作用:

  1. 可见性:一个线程修改 volatile 变量后,其他线程能立即看到最新值。
  2. 有序性:禁止指令重排,保证代码执行顺序与预期一致。
  3. 不保证原子性:不能替代锁解决多线程并发修改问题。
public class VolatileDemo {// 用 volatile 修饰共享变量,保证多线程可见性private static volatile boolean flag = false;public static void main(String[] args) {// 线程1:等待 flag 变为 truenew Thread(() -> {System.out.println("线程1启动,等待 flag 变为 true...");while (!flag) {// 循环等待,直到 flag 被修改}System.out.println("线程1检测到 flag 为 true,退出循环");}).start();// 主线程:休眠1秒后修改 flagtry {Thread.sleep(1000); // 确保线程1先启动} catch (InterruptedException e) {e.printStackTrace();}flag = true;System.out.println("主线程已将 flag 修改为 true");}
}

九.synchronized和volatile比较


volatile不需要加锁,比synchronized更轻量级,不会阻塞线程
从内存可见性角度讲,volatile读相当于加锁,volatile写相当于解锁
synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性;

http://www.dtcms.com/a/520901.html

相关文章:

  • 数据结构算法学习:LeetCode热题100-链表篇(下)(随机链表的复制、排序链表、合并 K 个升序链表、LRU 缓存)
  • 网络营销网站 功能南京微信小程序开发制作
  • 域名打不开原来的网站手机免费网站制作
  • 中药饮片网购是什么?主要的市场特点及未来发展潜力如何?
  • 自己做的网站数据库360优化大师app
  • Python-__init__函数
  • 沈阳网站维护公司网站建设财务怎么入账
  • JavaEE:知识总结(一)
  • 各家高性能MCU的内置Flash逐渐走向MRAM之路,关于嵌入式 MRAM 的性能和能效
  • Leetcode 35
  • GPIO口输出
  • 专教做美食的网站胶州哪里有做网站的
  • 企业网站制作 徐州网站设计原则的历史
  • 隐私保护与数据安全合规(十三)
  • 2025年高真空共晶炉排名
  • 网站做转链接违反版权吗wordpress页面不显示子类
  • 5.3类的构造方法
  • 视频监控系统原理与计量
  • 蓝桥杯高校新生编程赛第一场题解——Java
  • JavaScript 的优势和劣势是什么?
  • 鸿蒙Next的Camera Kit:开启全场景智慧影像开发新纪元
  • 软件开发包含网站开发吗搭建网站成本
  • asp.net 微网站开发教程比较大的建站公司
  • h5游戏免费下载:小猪飞飞
  • 基于单片机的档案库房漏水检测报警labview上位机系统设计
  • 网站开发图标汕头网站建设seo外包
  • DeepSeek-OCR:光学Token:长上下文建模的范式转变
  • Windows 11 24H2内核堆栈保护:系统安全新盾牌
  • 自定义组件(移动端下拉多选)中使用 v-model
  • Android 14 系统启动流程深度解析:AVB流程