【多线程】线程安全 问题

线程安全 问题

  • 一. 线程不安全的典型例子
  • 二. 线程安全的概念
  • 三. 线程不安全的原因
    • 1. 线程调度的抢占式执行
    • 2. 修改共享数据
    • 3. 原子性
    • 4. 内存可见性
    • 5. 指令重排序

一. 线程不安全的典型例子

class ThreadDemo {static class Counter {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);}
}

多次执行的结果:

在这里插入图片描述
两个线程各 加了 50000 次, 但最终结果都不是我们预期的 100000, 并且相差甚远。

二. 线程安全的概念

操作系统调度线程是随机的(抢占式),正因为这样的随机性,就可能导致程序的执行出现一些 bug。

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的,否则就是线程不安全的。

三. 线程不安全的原因

1. 线程调度的抢占式执行

线程是抢占式执行,线程间的调度就充满随机性。
这是线程不安全的万恶之源,但是我们无可奈何,无法解决。

2. 修改共享数据

多个线程针对同一变量进行了修改操作,假如说多个线程针对不同变量进行修改则没事,多个线程针对相同变量进行读取也没事。

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

在这里插入图片描述

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

3. 原子性

针对变量的操作不是原子的,通过加锁操作,可以把多个操作打包成一个原子操作。

一条 java 语句不一定是原子的,也不一定只是一条指令
比如上面的 count++,其实是由三步操作组成的:
(1)从内存把数据 count 读到 CPU
(2)进行数据更新 count = count + 1
(3)把更新后的数据 count 写回到 内存

所以说导致上面那段代码线程不安全的原因就是:

在这里插入图片描述

只要 t2 是在 t1 线程 save 之前读的, t2 的自增就会覆盖 t1 的自增, 那么两次加 1 的效果都相当于只加了 1 次.
所以上面的代码的执行结果 在 5w ~ 10w,并且大多数是靠近 5w 的。
(小于 5w 的是非常少见的, 这种情况就是 t2 线程覆盖了 t1 线程的多次 自增操作, 也就是说 t2 线程的 load 与 save 之间跨度很大的情况.)

解决:加锁 ! 打包成原子操作。
最常见的加锁方式就是 使用 synchronized

class ThreadDemo {static class Counter {public int count = 0;synchronized 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);}
}

在这里插入图片描述

在自增之前加锁,自增之后再解锁。
当一个线程加锁成功时,其他线程再尝试加锁就会阻塞,直到占用锁的线程将锁释放。

那这样不就是串行执行了嘛?那多线程又有什么用 ?
实际开发中,一个线程要执行的任务很多,可能只有其中的很少一部分会涉及到线程安全问题,才需要加锁,而其他地方都能并发执行。

注意:
加锁,是要明确指出对哪个对象进行加锁的,如果两个线程对同一个锁对象进行加锁才会产生锁竞争(阻塞等待),如果对不同的对象进行加锁,那么不会产生锁竞争(阻塞等待)。
上面代码中 synchronized 加在方法上,那么就是对 Counter 对象加锁,对应到代码中就是两个线程就是对 counter 这个实例对象加锁。

4. 内存可见性

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

Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型.
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.

在这里插入图片描述

  • 线程之间的共享变量存在 主内存 (Main Memory).
  • 每一个线程都有自己的 “工作内存” (Working Memory) .
  • 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
  • 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

实际并没有这么多 “内存”. 这只是 Java 规范中的一个术语, 是属于 “抽象” 的叫法.
所谓的 “主内存” 才是真正硬件角度的 “内存”. 而所谓的 “工作内存”, 则是指 CPU 的寄存器和高速缓存.

由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 “副本”. 此时修改线程 1 的工作内存中的值, 线程 2 的工作内存不一定会及时变化.

举一个栗子:
针对同一个变量:
一个线程进行循环读取操作,另一个线程在某个时机进行了修改操作。

在这里插入图片描述

比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的比寄存器慢 3-4 个数量级. 
但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问
内存了. 效率就大大提高了. (编译器优化的结果)

解决:

  1. 使用 synchronized
    synchronized 不仅能保证原子性,同时能保证内存可见性,
    被 synchronized 修饰的代码,编译器不会轻易优化。

  2. 使用 volatile 关键字
    volatile 和原子性无关,但是能保证内存可见性,禁止编译器优化,每次都要从内存中读取变量。

5. 指令重排序

什么是代码重排序?
举个栗子:
一段代码是这样的:

1. 去前台取下 U2. 去教室写 10 分钟作业
3. 去前台取下快递

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台,提高效率。这种叫做指令重排序。

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

解决:
使用 volatile 关键字
volatile 除了能保证内存可见性之外还能防止指令重排序。

好啦! 以上就是对 线程安全 问题的讲解,希望能帮到你 !
评论区欢迎指正 !

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

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

相关文章

Matlab之DICOM(数字图像和通信医学)格式图像数据读取函数dicomread

一、DICOM是什么&#xff1f; DICOM是数字图像和通信医学格式的图像数据&#xff0c;在MATLAB中&#xff0c;可以使用dicomread函数读取DICOM格式的图像数据。 二、dicomread函数 使用方法如下&#xff1a; imageData dicomread(filename);其中&#xff0c;filename表示DI…

Axure RP美容美妆医美行业上门服务交互原型图模板源文件

Axure RP美容美妆医美行业上门服务交互原型图模板源文件&#xff0c;原型内容属于电商APP&#xff0c;区别于一般电商&#xff0c;它的内容是‘美容美发美妆等’上门服务等。大致流程是线上买单&#xff0c;线下实体店核销消费。 附上预览演示&#xff1a;axure9.com/mobile/73…

博客程序系统其它功能扩充

一、注册功能 1、约定前后端接口 2、后端代码编写 WebServlet("/register") public class RegisterServlet extends HttpServlet {Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//设置…

LeetCode 3. 无重复字符的最长子串

题目链接 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 题目解析 我们需要找的是含重复元素的最长子串&#xff0c;当然直接暴力求解固然简单。但是可能引发的情况是超时&#xff0c;而且面试官想看到的也不是让你去暴力解决这类问题。因此我们使…

strerror函数

目录 strerror 函数介绍&#xff1a; 举例&#xff1a; 使用案例&#xff1a; 优化&#xff1a; perror&#xff1a; strerror 函数介绍&#xff1a; 函数声明&#xff1a; char * strerror ( int errnum );头 文 件&#xff1a;#include <string.h>返 回 值&a…

SpringBoot+MP操作DM8

1. 达梦数据库介绍 达梦数据库管理系统是达梦公司推出的具有完全自主知识产权的国产高性能数据库管理系统&#xff0c;简称DM。当前最新版本是8.0版本&#xff0c;简称DM8。&#xff08;同时也是一款RDBMS&#xff0c;关系型数据库管理系统&#xff0c;和oracle比较像&#xff…

红米Note12Turbo解锁BL刷入PixelExperience原生ROM系统详细教程

红米Note12Turbo的兄弟是国外POCO F5 机型&#xff0c;并且该机性价比非常高&#xff0c;国内外销量也还可以&#xff0c;自然不缺第三方ROM适配。目前大家心心念念的原生PixelExperience已成功发布&#xff0c;并且相对来说&#xff0c;适配程度较高&#xff0c;已经达到日用的…

python实现adb辅助点击屏幕工具

#!/usr/bin/env python # -*- coding: utf-8 -*-import re import os import time import subprocess import tkinter as tk from tkinter import messagebox from PIL import Image, ImageTk# 设置ADB路径&#xff08;根据你的系统和安装路径进行调整&#xff09; ADB_PATH C…

intellij debug模式提示 : Method breakpoints may dramatically slow down debugging

最近在搞一个搭建一个项目 , 项目搭建完之后发现启动不了 , 一直都是正在加载中 并且提示Method breakpoints may dramatically slow down debugging&#xff0c;百度之后才知道是打了方法断点的原因 , 之前不小心打了一个断点 解决办法 : 点击如下图所示的按钮 然后把有断点…

Linux_VMware_虚拟机磁盘扩容

来源文章 &#xff1a;VMware教学-虚拟机扩容篇_vmware虚拟机扩容_系统免驱动的博客-CSDN博客 由于项目逐步的完善&#xff0c;需要搭建的中间件&#xff0c;软件越来越多&#xff0c;导致以前虚拟机配置20G的内存不够用了&#xff0c;又不想重新创建新的虚拟机&#xff0c;退…

为什么说网络安全是风口行业?是it行业最后的红利?

前言 “没有网络安全就没有国家安全”。当前&#xff0c;网络安全已被提升到国家战略的高度&#xff0c;成为影响国家安全、社会稳定至关重要的因素之一。 网络安全行业特点 1、就业薪资非常高&#xff0c;涨薪快 2021年猎聘网发布网络安全行业就业薪资行业最高人均33.77万&…

ucosii任务切换及任务同步

任务的切换 一、 运行态&#xff1a;占用CPU 二、 等待&#xff1a;调用Pend或延时函数后&#xff0c;释放CUP使用权。 三、 就绪&#xff1a;Pend条件满足&#xff08;消息到来、等待超时&#xff09;&#xff0c;延时时间完毕后&#xff08;由等待进入就绪&#xff09; 四、 …

LeetCode 297. Serialize and Deserialize Binary Tree【树,DFS,BFS,设计,二叉树,字符串】困难

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…

机器学习入门教学——决策树

1、简介 决策树算法是一种归纳分类算法&#xff0c;它通过对训练集的学习&#xff0c;挖掘出有用的规则&#xff0c;用于对新数据进行预测。决策树算法属于监督学习方法。决策树归纳的基本算法是贪心算法&#xff0c;自顶向下来构建决策树。 贪心算法&#xff1a;在每一步选择…

MR源码解析和join案例

MR源码解析 new Job(): 读取本地文件, xml配置job.start(): 启动线程job的run():线程方法 runTasks(): 传入对应的接口&#xff0c;启动map或者reduceMapTask类的run(): 设置map阶段的参数&#xff0c;初始化任务&#xff0c;创建上下文对象 创建读取器LineRecordReader判断是…

Visual Studio 新建类从默认internal改为public

前言 之前一直用的Resharp辅助编写C#代码&#xff0c;Resharp用起来的确方便不少&#xff0c;但是太消耗开发机内存了。重装电脑后&#xff0c;还是决定使用Visual Studio内置的功能。 默认情况下&#xff0c;Visual Studio 中生成一个类或接口是internal类型的&#xff0c;而…

EagleSDR USB HAT FT600

给EagleSDR做了个USB 3.0的子卡&#xff0c;采用FT600方案&#xff0c;实物如下&#xff1a; 用FT600DataStreamerDemoApp测试&#xff0c;速度如下&#xff1a; 由于FT600是16bit的接口&#xff0c;如果用FT601的32bit接口&#xff0c;性能应该还会有大幅提升。 测试代码很简…

Notepad++下载安装

自己在 找Notepad发现网上的网址参差不齐&#xff0c;自己找到的一个不错下载链接见文末&#xff01; Notepad 是一个免费的代码编辑器&#xff0c;专为在微软 Windows 环境下使用。它是一个开源项目&#xff0c;采用 GPL 许可证&#xff0c;并使用 C 编程语言结合 Win32 API 和…

数据驱动的数字营销与消费者运营

引言&#xff1a;基于海洋馆文旅企业在推广宣传中&#xff0c;如何通过指标体系量化分析广告收益对业务带来的收益价值的思考&#xff1f; 第一部分:前链路引流投放的策略与实战 1.1 动态广告的实现: 偶然与必然 动态广告是一种基于实时数据和用户行为的广告形式&#xff0c;它…

Mysql--事务

事务 开始之前&#xff0c;让我们先想一个场景&#xff0c;有的时候&#xff0c;为了完成某个工作&#xff0c;需要完成多种sql操作 比如转账 再比如下单 第一步 我的账户余额减少 第二步 商品的库存要减少 第三步 订单表中要新增一项 事务的本质&#xff0c;就是为了把多个操…