线程安全问题及其解决

文章目录

  • 一. 线程安全问题
    • 1.1 线程不安全的例子
    • 1.2 线程不安全的原因
      • 1.2.1 随即调度, 抢占式执行
      • 1.2.2 修改共享数据
      • 1.2.3 修改操作非原子性
      • 1.2.4 内存可见性
      • 1.2.5 指令重排序
      • 1.2.6 总结
  • 二. 线程安全问题的解决
    • 2.1 synchronized(解决前三个问题)
      • 2.1.1 synchronized 的锁是什么
      • 2.1.2 synchronized 的特性
    • 2.2 volatile关键字(解决第四,五个问题)
      • 2.2.1 volatile 能保证内存可见性
      • 2.2.2 volatile不保证原子性

一. 线程安全问题

当我们使用多个线程访问同一资源(可以是同一变量, 同一个文件, 同一条记录等) 的时候, 若多个过程只有读操作, 那么不会发生线程安全问题. 但是如果多个线程中对资源有读和写的操作, 就容易出现线程安全问题.

1.1 线程不安全的例子

class Counter {public int count = 0;public void increase() {count+=1;}
}
public class Demo {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread thread1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread thread2 = new Thread(()-> {for (int i = 0; i < 50000; i++) {counter.increase();}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(counter.count);}
}

我们认为count应该是100000, 但是实际输出的却不是, 这就说明上述代码有线程安全问题.

1.2 线程不安全的原因

1.2.1 随即调度, 抢占式执行

线程的调度不是按顺序的, 而是抢占式的, 这是系统规定, 我们无法修改.

1.2.2 修改共享数据

上面线程不安全的代码中, 涉及多个线程对counter.count变量进行修改, 此时这个counter.count是一个多线程都能访问到的"共享数据".

在这里插入图片描述

counter.count这个变量就在堆上, 因此可以被多个线程共享访问.

1.2.3 修改操作非原子性

什么是原子性?

我们把一段代码想象成一个房间, 每个线程就是要进入这个房间的人. 如果没有任何机制保证, A进入房间之后, 还没有出来; B 是不是也可以进入房间, 打断 A 在房间里的隐私. 这个就是不具备原子性的.

那我们应该如何解决这个问题呢? 是不是只要给房间加一把锁, A 进去就把门锁上, 其他人是不是就进不来了. 这样就保证了这段代码的原子性了.

有时也把这个现象叫做同步互斥, 表示操作是互相排斥的.

一条Java语句不一定是原子的, 也不一定只是一条指令.

比如上述代码中的count+=1其实是三个操作组成的:

  • 从内存把数据读到CPU
  • 进行数据更新
  • 把数据写回到CPU
时间t1t2
T1load
T2load
T3add
T4save
T5add
T6save

在这里插入图片描述

不保证原子性会带来的问题: 如果一个线程正在对一个变量操作, 中途其他线程插入进来了, 如果这个操作被打断了, 结果就可能是错误的.

这点也和线程的抢占式调度密切相关. 如果线程不是 “抢占” 的, 就算没有原子性, 也问题不大.

1.2.4 内存可见性

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

我们先来看下面的代码

public class Demo {private static int isQuit = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (isQuit == 0) {;}System.out.println("t1 end");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("其输入isQuit的值");isQuit = scanner.nextInt();});t1.start();t2.start();}
}

我们先输入1, 结果线程t1还未停止.

在这里插入图片描述

在这里插入图片描述

我们想让线程t1在isQuit非零时停止, 但事实并非所愿, 这就是内存可见性引发的线程安全问题.

程序在编译运行的时候, Java编译器和JVM可能会对代码作出一些"优化", 在保持原有逻辑不变的情况下, 提高代码的执行效率, 这就称为 编译器优化.

编译器优化本质是靠代码智能地对代码进行分析判断, 进行调整. 这个调整过程大部分情况下都能保持逻辑不变, 但是如果遇到多线程, 就可能会发生差错, 逻辑改变.

while (isQuit == 0)

本质上是两个指令: 一是读内存; 二是比较并跳转

比较操作是在寄存器上进行的, 速度十分快, 相较之下, 读内存操作就会显得很慢.

此时, JVM就会反应到, 这个代码要反复读取同一个内存值, 读出的结果还都是一样的, 于是编译器就直接把读内存这个指令给优化掉了, 只读一次内存, 后续直接拿寄存器中的数据比较, 大大加快了执行速度.

但是, JVM没有预料到我们会在其他线程修改isQuit的值, 编译器没法准确判定出t2线程会不会执行, 什么时候执行, 因此就出现了误判.

虽然其他线程把值修改了, 但是另一个线程中没有重复读取isQuit的值, 这就引发了内存可见性的问题.

1.2.5 指令重排序

指令重排序也是编译器优化的一种手段, 在保证原有逻辑不发生变化的情况下, 对代码执行的顺序进行调整, 使调整后的执行效率变高.

编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.

1.2.6 总结

线程安全问题的原因:

  • 根本原因: 多个线程之间的调度顺序是"随机的", 操作系统使用"抢占式"执行的策略来调度线程.
    • 当前主流的操作系统都是抢占式执行的.
  • 多个线程同时修改同一个变量.
  • 此处进行的修改不是"原子的".
  • 内存可见性引起的线程安全问题.
  • 指令重排序引起的线程安全问题.

二. 线程安全问题的解决

2.1 synchronized(解决前三个问题)

为了保证每个线程都能正常执行原子操作, Java引入了线程同步机制. 注意: 在任何时候, 最多允许一个线程拥有同步锁, 谁拿到锁就进入代码块, 其他的线程只能在外等着( BLOCKED) .

同步机制的原理, 其实就相当于给某段代码加“锁”, 任何线程想要执行这段代码, 都要先获得“锁”, 我们称它为同步锁.

2.1.1 synchronized 的锁是什么

同步代码块: synchronized 关键字可以用于某个区块前面, 表示只对这个区块的资源实行互斥访问.

synchronized(加锁的对象){//需要同步操作的代码
}public void increase() {synchronized(this) {count+=1;}
}

同步方法: synchronized 关键字直接修饰方法, 表示同一时刻只有一个线程能进入这个方法, 其他线程在外面等着.

public synchronized void method(){//可能会产生线程安全问题的代码
}public synchronized void increase() {count+=1;
}

synchronized 进行加锁解锁, 是以对象为维度进行的. 使用synchronized 的时候, 其实是指定了某个具体对象进行加锁.

对于同步代码块来说, 同步锁对象是由程序员手动指定的 ; 但是对于同步方法来说, 同步锁对象只能是默认的:

  • 静态方法: 默认加锁对象是当前类的Class对象(类名.class)

    public synchronized static void method(){//可能会产生线程安全问题的代码
    }
    //相当于
    public static void method(){synchronized(类名.class) {//可能会产生线程安全问题的代码}
    }
    
  • 非静态方法: 默认加锁对象是this

如果多个线程对同一个对象进行加锁, 就会出现锁竞争; 如果是多个对象针对不同的对象进行加锁, 不会产生锁竞争.

class Counter {public int count = 0;public void increase() {synchronized(this) {//对this加锁count+=1;}}
}
public class Demo {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread thread1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread thread2 = new Thread(()-> {for (int i = 0; i < 50000; i++) {counter.increase();}});// 两个线程对同一个对象(this -> counter)加锁, 那么结果就是100000thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(counter.count);}
}
class Counter {public int count = 0;private Object locker = new Object();public void increase() {synchronized(this) {//对this加锁count+=1;}}public void increase2() {synchronized(locker) {//对locker加锁count+=1;}}
}
public class Demo {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread thread1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread thread2 = new Thread(()-> {for (int i = 0; i < 50000; i++) {counter.increase2();}});// 两个线程对不同对象(this -> counter / locker)加锁, 那么结果就不是100000.thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(counter.count);}
}
class Counter {public int count = 0;private Object locker = new Object();public void increase() {synchronized(locker) {//对locker加锁count+=1;}}public void increase2() {synchronized(locker) {//对locker加锁count+=1;}}
}
public class Demo {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread thread1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread thread2 = new Thread(()-> {for (int i = 0; i < 50000; i++) {counter.increase2();}});// 两个线程对同一对象(locker)加锁, 那么结果就是100000.thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(counter.count);}
}

总结:

同步锁对象可以是任意类型, 但是必须保证 竞争"同一个共享资源"的多个线程必须针对同一个对象进行加锁。

2.1.2 synchronized 的特性

  1. 互斥

    synchronized 会起到互斥效果, 某个线程执行到 synchronized 修饰的代码块中时, 其他线程如果也执行到同一个对象的此代码块, 就会阻塞等待.

    • 进入 synchronized 修饰的代码块, 相当于加锁.
    • 退出 synchronized 修饰的代码块, 相当于解锁.

    可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态(类似于厕所的 “有人/无人”).

    如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.

    如果当前是 “有人” 状态, 那么其他人无法使用, 只能排队

    理解"阻塞等待"

    针对每一把锁, 操作系统内部都维护了一个等待队列, 当这个索贝某个线程栈有的时候, 其他线程尝试进行加锁, 就加不上了, 会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁.

    注意:

    • 上一个线程解锁之后, 下一个线程并不会立即就能获得到锁, 而是要靠操作系统来"唤醒", 这也是操作系统调度的一部分工作.
    • 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.
  2. 刷新内存

    synchronized 的工作过程:

    • 获得互斥锁
    • 从主内存拷贝变量的最新副本到工作的内存
    • 执行代码
    • 将更改后的共享变量的值刷新到主内存
    • 释放互斥锁

2.2 volatile关键字(解决第四,五个问题)

2.2.1 volatile 能保证内存可见性

volatile 修饰的变量, 编译器就不会把读操作优化到都寄存器中, 于是就能保证在循环过程中, 始终能读取内存中的数据, 保证 “内存可见性”.

在这里插入图片描述

代码在 写入 volatile修饰的变量的时候:

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存
    • 工作内存(work memory) : 是指存储区, 包括cpu寄存器和cpu缓存.

代码在 读取 volatile 修饰的变量的时候:

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况.

加上volatile, 强制读写内存, 速度虽然慢了, 但是数据更准确了.

代码示例

在这个代码中

  • 创建两个线程 t1 和 t2

  • t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.

  • t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.

  • 预期当用户输入非 0 的值的时候, t1 线程结束.

static class Counter {public int flag = 0;
}
public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {while (counter.flag == 0) {// do nothing}System.out.println("循环结束!");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("输入一个整数:");counter.flag = scanner.nextInt();});t1.start();t2.start();
}
// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)

t1读的是自己工作内存中的数据

当t2对flag变量进行修改, 此时t1感知不到flag的变化

如果给 flag 加上 volatile

static class Counter {public volatile int flag = 0;
}
// 执行效果
// 当用户输入非0值时, t1 线程循环能够立即结束.

2.2.2 volatile不保证原子性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.

代码示例

static class Counter {volatile public int count = 0;void increase() {count++;}
}
public static void main(String[] args) throws InterruptedException {final Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);
}

此时可以看到, 最终 count 的值仍然无法保证是 100000.

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

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

相关文章

247:vue+openlayers 根据坐标显示多边形(3857投影),计算出最大幅宽

第247个 点击查看专栏目录 本示例是演示如何在vue+openlayers项目中根据坐标显示多边形(3857投影),计算出最大幅宽。这里先通过Polygon来显示出多边形,利用getExtent() 获取3857坐标下的最大最小x,y值,通过ransformExtent转换坐标为4326, 通过turf的turf.distance和计算…

社区论坛小程序系统源码+自定义设置+活动奖励 自带流量主 带完整的搭建教程

大家好啊&#xff0c;又到了罗峰来给大家分享好用的源码的时间了。今天罗峰要给大家分享的是一款社区论坛小程序系统。社区论坛已经成为人们交流、学习、分享的重要平台。然而&#xff0c;传统的社区论坛往往功能单一、缺乏个性化设置&#xff0c;无法满足用户多样化的需求。而…

rabbitmq 集群搭建

RabbitMQ集群介绍 RabbitMQ集群是一组RabbitMQ节点&#xff08;broker&#xff09;的集合&#xff0c;它们一起工作以提供高可用性和可伸缩性服务。 RabbitMQ集群中的节点可以在同一物理服务器或不同的物理服务器上运行。 RabbitMQ集群的工作原理是&#xff0c;每个节点在一个…

C++编写的多线程自动爬虫程序

以下是一个使用C编写的爬虫程序&#xff0c;用于爬取Python进行多线程跑数据的内容。本示例使用了Python的requests库来发送HTTP请求&#xff0c;并使用cheeseboy的爬虫ipIP库来设置爬虫ip信息。以下是详细代码和步骤&#xff1a; #include <iostream> #include <stri…

StyleGAN:彻底改变生成对抗网络的艺术

一、介绍 多年来&#xff0c;人工智能领域取得了显着的进步&#xff0c;其中最令人兴奋的领域之一是生成模型的发展。这些模型旨在生成与人类创作没有区别的内容&#xff0c;例如图像和文本。其中&#xff0c;StyleGAN&#xff08;即风格生成对抗网络&#xff09;因其创建高度逼…

12-使用vue2实现todolist待办事项

个人名片&#xff1a; &#x1f60a;作者简介&#xff1a;一名大二在校生 &#x1f921; 个人主页&#xff1a;坠入暮云间x &#x1f43c;座右铭&#xff1a;懒惰受到的惩罚不仅仅是自己的失败&#xff0c;还有别人的成功。 &#x1f385;**学习目标: 坚持每一次的学习打卡 文章…

【Spring Boot】035-Spring Boot 整合 MyBatis Plus

【Spring Boot】035-Spring Boot 整合 MyBatis Plus 【Spring Boot】010-Spring Boot整合Mybatis https://blog.csdn.net/qq_29689343/article/details/108621835 文章目录 【Spring Boot】035-Spring Boot 整合 MyBatis Plus一、MyBatis Plus 概述1、简介2、特性3、结构图4、相…

LeetCode - 27. 移除元素 (C语言,快慢指针,配图)

力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 思路一&#xff1a;新开辟一个数组&#xff0c;空间复杂度O(N) 因为本题要求是空间复杂度O(1),所以这里只是列出思路1的思路和配图&#xff0c;并没有具体的实现代码&#xff0c;想必这对大家一定很简单…

使用postman测试

第一步&#xff1a; 第二步&#xff1a; 第三步&#xff1a;添加请求 第四步&#xff1a;填写请求 代码实现自动关联的位置&#xff1a; 为相关联的接口设置环境&#xff1a; 使用设置的环境变量&#xff1a; 参数化实现测试&#xff1a;测试脚本中仅测试数据不一样&#xff…

Promise 重写 (第一部分)

学习关键语句&#xff1a; promise 重写 写在前面 重新学习了怎么重写 promise &#xff0c; 我觉得最重要的就是要有思路&#xff0c;不然有些 A 规范是完全想不到的 开始 重写函数的过程中, 最重要的是有思路 我们从哪里获取重写思路? 从正常的代码中 我们先看正常的代码…

『GitHub项目圈选02』一款可实现视频自动翻译配音为其他语言的开源项目

&#x1f525;&#x1f525;&#x1f525;本周GitHub项目圈选****: 主要包含视频翻译、正则填字游戏、敏感词检测、聊天机器人框架、AI 换脸、分布式数据集成平台等热点项目。 1、pyvideotrans pyvideotrans 是一个视频翻译工具&#xff0c;可将一种语言的视频翻译为另一种语…

学习c#的第十三天

目录 C# 多态性 静态多态性 函数重载 运算符重载 动态多态性 virtual 和 abstract 抽象方法和虚方法的区别 重载(overload)和重写(override) 隐藏方法 C# 多态性 多态是同一个行为具有多个不同表现形式或形态的能力。 多态性意味着有多重形式。在面向对象编程范式中…

Postman的Cookie鉴权

近期在复习Postman的基础知识&#xff0c;在小破站上跟着百里老师系统复习了一遍&#xff0c;也做了一些笔记&#xff0c;希望可以给大家一点点启发。 一&#xff09;什么是Cookie 定义&#xff1a;存储在客户端的一小段文本信息&#xff0c;格式为键值对的形式. 二&#xff09…

Leetcode刷题详解——岛屿数量

1. 题目链接&#xff1a;200. 岛屿数量 2. 题目描述&#xff1a; 给你一个由 1&#xff08;陆地&#xff09;和 0&#xff08;水&#xff09;组成的的二维网格&#xff0c;请你计算网格中岛屿的数量。 岛屿总是被水包围&#xff0c;并且每座岛屿只能由水平方向和/或竖直方向上…

汽车OBD2蓝牙诊断仪解决方案程序开发

1、因TL718已经为你建立了物理层、数据链层和部分应用层的协议&#xff0c;所以只要OBD2标准应用层协议文本&#xff0c;ISO15031-5 或 SAE J1979&#xff08;这两个协议是相同的内容&#xff09;。 2、TL718诊断接口 1 套或用TL718芯片自建电路。3、家用PC机电脑一台。4、安…

计算机网络——物理层-编码与调制(数字基带信号、模拟基带信号、码元、常用编码、基本调制方法、混合调制)

目录 编码与调制 数字基带信号 模拟基带信号 码元 常用编码 不归零编码 归零编码 曼彻斯特编码 差分曼彻斯特编码 编码习题 基本调制方法 调幅 调频 调相 混合调制 QAM-16 编码与调制 在计算机网络中&#xff0c;计算机需要处理和传输用户的文字、图片、音频…

深度学习AI识别人脸年龄

以下链接来自 落痕的寒假 GitHub - luohenyueji/OpenCV-Practical-Exercise: OpenCV practical exercise https://download.csdn.net/download/luohenyj/10993309 import cv2 as cv import time import argparsedef getFaceBox(net, frame, conf_threshold0.7):frameOpencvDn…

结构工程师软件 Naviate Core MEP for Revit 3.4 Crk

Naviate Fabrication - 先进的建模和制造命令&#xff0c;可提高 VDC 设计师、细节设计师和承包商的生产力和收入。 Naviate MEP - 通过 MEP 工程师和设计师的建模和参数提高效率 导航架构 Naviate Architecture 完全集成到 Revit 平台中&#xff0c;增强了 BIM 提供的协作可能…

C++标准模板(STL)- 类型支持 (属性查询,获取类型的对齐要求)

类型特性 类型特性定义一个编译时基于模板的结构&#xff0c;以查询或修改类型的属性。 试图特化定义于 <type_traits> 头文件的模板导致未定义行为&#xff0c;除了 std::common_type 可依照其所描述特化。 定义于<type_traits>头文件的模板可以用不完整类型实例…

使用JAVA pdf转word

使用spire.pdf 非常简单。 查看 https://mvnrepository.com/artifact/e-iceblue/spire.pdf 注意&#xff0c;这个包在 e-iceblue 下。 下面开始撸代码 先来pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://mav…