05. 就该这么学并发 - 线程安全

浅谈SimpleDateFormat的线程安全问题 浅谈SimpleDateFormat的线程安全问题在实际项目中,我们经常需要将日期在String和Date之间做转化,此时需要使用SimpleDateFormat类。使用SimpleDateFormat类的parse方法,可以将满足格式要求的字符串转换成Date对象,使用SimpleDateFormat类的format方法,可以将Date类型的对象转换成一定格式的字符串!但是有一点需要特别注意,S... 阅读详情

前言

通过前几章对“线程生命周期”的讲解, 相信大家对于线程有了基本的了解.

本章,我们就来聊聊**线程安全**问题.

线程安全

一般我们说到线程安全, 都是**针对多线程的(单线程不存在线程安全问题**)

多线程之所以会出现安全问题, 主要是因为

存在同时访问同一个共享、可变资源的情况
这种资源可以是:一个变量、一个对象、一个文件等

  • 共享
意味着该资源可以由多个线程同时访问
  • 可变
意味着该资源可以在其生命周期内被修改

简而言之,如果

代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么代码就是线程安全的

为了多线程编程的安全, 我们就得先了解线程安全的3个特性.

线程安全特性

安全的线程应该有如下3个特性

  • 原子性
一个操作或者多个操作,
要么全部执行(执行的过程不会被任何因素打断);
要么就都不执行

举个经典的银行转账例子

两个人 A 和 B , 各有1000的存款,
A 转给 B 1000, 逻辑应该是

A - 1000 //步骤1
B + 1000 //步骤2

试想,如果, 步骤1执行完后, 步骤2被打断了,
那么1000元就不翼而飞了!
所以,步骤1和步骤2操作应该是“捆绑”的,也就是我们说的“原子”的

  • 可见性
当多个线程访问同一个变量时,
一个线程修改了这个变量的值,
其他线程能够立即看得到修改的值

可见性主要由内存模型决定.

JMM(Java内存模型)规定如下

  • java 的所有变量都存储在主内存中
  • 每个线程有自己独立的工作内存,保存了该线程使用到的变量副本,是对主内存中变量的一份拷贝
  • 每个线程不能访问其他线程的工作内存,线程间变量传递需要通过主内存来完成
  • 每个线程不能直接操作主存,只能把主存的内容拷贝到本地内存后再做操作(这是线程不安全的本质),然后写回主存

所以, 针对JMM特性, 就会存在如下案例:

public class Test {
    private static boolean isRuning = false;
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isRuning) {
                }
            }
        });
        thread.start();
        Thread.sleep(100);
        isRuning = true;
        System.out.println("主线程运行完毕");
    }
}

我们的程序很简单,
两个线程, 主线程和子线程;
isRuning来控制子线程是否继续执行,
然后主线程
isRuning = true;

按道理说, 子线程会立刻结束执行;
但是你运行这段代码会发现,
image.png
程序根本不会结束!
原因在于:
子线程一直读取的是缓存的 isRuning=false, 而没有读取主存中的最新值true!

  • 有序性
程序执行的顺序按照代码的先后顺序执行

同样,举个例子

int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a * a; //语句4

可以看出, 代码上语句2是在语句1后执行的,
但其实, JVM执行时却不一定是按这个顺序执行的!
为啥?
因为JVM执行时可能会进行 “指令重排序”

指令重排序是处理器为了提高程序运行效率, 可能会对代码进行优化,
它遵循**as-if-serial**语义(后续单独介绍)

它不保证程序中各个语句的执行先后顺序同代码中的顺序一致, 但是它会保证(单线程)程序最终执行结果和代码顺序执行的结果是一致的.

如上代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,所以顺序可以调整;

而语句3和语句4,对语句1和语句2有依赖,他们的执行顺序不能打乱.
所以, JVM真正执行时的顺序可能是
语句1--->语句2--->语句3--->语句4
语句2--->语句1--->语句3--->语句4
而不可能有其它执行顺序

要想并发程序正确地执行,必须要保证 原子性、可见性以及有序性, 只要有一个没有被保证,就有可能会导致程序运行不正确.

重排序

介绍线程安全特性的“有序性”时,提出了个“重排序”的概念,

重排序是编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段

重排序分**3种**类型:

  • 编译器优化的重排序
编译器在不改变单线程程序语义的前提下,可以重新安排语义.
  • 指令级并行的重排序
现代处理器采用了指令级并行技术来将多条指令重叠执行.
如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序.
  • 内存系统的重排序
由于处理器使用缓存和读/写缓冲区,
这使得加载和存储操作看上去可能是在乱序执行.

JMM(Java Memory Model, Java内存模型)中,允许编译器和处理器对指令进行重排序.

从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序
重排序过程

重排序遵循**as-if-serial**语义.

as-if-serial

as-if-serial语义是

不管怎么重排序, 单线程程序的执行结果不能被改变.
编译器、runtime和处理器都必须遵守as-if-serial语义.
为了遵守as-if-serial语义,
编译器和处理器不会对存在数据依赖关系的操作做重排序, 因为这种重排序会改变执行结果.
但是, 如果操作之间不存在数据依赖关系, 这些操作就可以被编译器和处理器重排序.

这里所说的**数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑**.

这也是“单线程不存在线程安全问题”的原因!

另外, 我们可以通过插入特定类型的内存屏障来禁止特定类型的编译器重排序 和 处理器重排序.

内存屏障(Memory Barrier)

内存屏障,又称内存栅栏,是一个CPU指令,
通过内存屏障可以禁止特定类型处理器的重排序,从而让程序按我们预想的流程去执行.

基本上它是一条这样的指令

  • 保证特定操作的执行顺序

  • 影响某些数据(或则是某条指令的执行结果)的内存可见性

编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化.

插入一个内存屏障, 相当于告诉CPU和编译器

先于这个命令的必须先执行, 后于这个命令的必须后执行.

内存屏障另一个作用是

强制更新一次不同CPU的缓存

例如

一个写屏障会把这个屏障前写入的数据刷新到缓存,
这样任何试图读取该数据的线程将得到最新值,
而不用考虑到底是被哪个CPU核心或者哪颗CPU执行的.

Java内存模型中,

  • volatile是基于内存屏障实现的

如果一个变量是volatile修饰的,
JMM会在写入这个变量之后插进一个写屏障指令,
并在读这个字段之前插入一个读屏障指令,

这意味着:
一个线程写入变量X后,任何线程访问该变量都会拿到最新值.
在写入变量X之前的写入操作,
其更新的数据对于其他线程也是可见的.
因为内存屏障会刷出cache中的所有先前的写入.

  • synchronized底层也是通过释放屏障和获取屏障的配对使用保障有序性, 加载屏障和存储屏障的配对使用保障可见性

通过synchronized关键字包住的代码区域,当线程进入到该区域读取变量信息时,保证读到的是最新的值

内存屏障分类

CPU层面的内存屏障

CPU内存屏障主要分为以下3类:

  • 写屏障(Store Memory Barrier)
告诉处理器
在写屏障之前的所有已经在缓存(store bufferes)中的数据同步到主内存,
简单来说
就是使得写屏障之前的指令的结果对写屏障之后的读或者写是可见的
  • 读屏障(Load Memory Barrier)
处理器在读屏障之后的读操作,都在读屏障之后执行.
配合写屏障,使得写屏障之前的内存更新, 对于读屏障之后的读操作是可见的
  • 全屏障(Full Memory Barrier)
确保屏障前的内存读写操作的结果提交到内存之后,
再执行屏障后的读写操作
JMM层面的内存屏障

JMM层面的内存屏障主要分为以下4类:

  • LoadLoad(LL)屏障
对于这样的语句
Load1; 
LoadLoad; 
Load2;
在Load2及后续读取操作要读取的数据被访问前,
保证Load1要读取的数据被读取完毕
  • StoreStore(SS)屏障
对于这样的语句
Store1; 
StoreStore; 
Store2;
在Store2及后续写入操作执行前,
保证Store1的写入操作对其它处理器可见
  • LoadStore(LS)屏障
对于这样的语句
Load1; 
LoadStore; 
Store2;
在Store2及后续写入操作被刷出前,
保证Load1要读取的数据被读取完毕
  • StoreLoad(SL)屏障
对于这样的语句
Store1; 
StoreLoad; 
Load2;
在Load2及后续所有读取操作执行前,
保证Store1的写入对所有处理器可见.
它的开销是四种屏障中最大的.
在大多数处理器的实现中, 这个屏障是个万能屏障,兼具其它三种内存屏障的功能

请关注我的订阅号

订阅号.png

Java并发编程系列05:CyclicBarrier+Runnable,实现线程安全 CyclicBarrier CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier),它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法。 CountDownLatch是一个同步的辅助类,允许一个或多个线程,等待其他一组线程完成操作,再继续执行。 CyclicBarrier是一个同步的辅助类,允许一组线程相互之间等待, 阅读详情

相关推荐

土地利用数据从1972到2023:如何利用Landsat和Sentinel数据构建长期变化监测模型

本文详细解析了如何利用1972至2023年的Landsat和Sentinel遥感数据构建土地利用长期变化监测模型。通过多源数据融合策略、时间序列预处理流程和深度学习算法,实现高精度的土地利用分类与变化检测,为生态评估和城市规划提供科学依据。文章还探讨了验证方法和实际应用案例,展示了长期遥感数据在环境建模中的独特价值。

weixin_28720573的博客 443

【Java 并发编程】【05】线程安全问题与线程同步

5. 线程安全问题与线程同步 多线程编程是有趣且复杂的事情,它常常容易突然出现“错误情况”,这是由于系统的线程调度具有一定的随机性。即使程序在运行过程中偶尔会出现问题,那也是由于我们的代码有问题导致的。当多个线程访问同一个数据时,非常容易出现线程安全问题。 5.1 线程安全问题 所谓线程安全问题,其实就是多线程在并发访问的时候,对共享内存中的共享对象的属性进行修改所导致的数据冲突问题 Keyword: 并发访问 共享内存 共享对象 (共享内存中的共享对象) 属性的修改 (方法是在栈中,不会有并发问题)

weixin_43589563的博客 3219

手把手教你用OPA运放和MOS管搭建恒流源电路(附完整电路图)

本文详细介绍了如何使用OPA运放和MOS管搭建高精度恒流源电路,涵盖核心元器件选型、电路设计、工作原理及实际搭建步骤。通过负反馈控制机制和MOS管工作区设置技巧,实现稳定的电流输出,适用于LED驱动、电池充电等场景。附完整电路图和调试技巧,助您快速掌握恒流源设计。

weixin_29239279的博客 534

【JUC并发编程05】集合的线程安全

文章目录5 集合的线程安全5.1 集合线程不安全演示5.2 解决方案-Vector5.3 解决方案-Collections5.4 解决方案-CopyOnWriteArrayList5.5 对上述三种方式的总结5.6 HashSet的线程不安全5.7 HashMap的线程不安全 5 集合的线程安全 5.1 集合线程不安全演示 在学习集合线程安全之前,先来看一下为什么在多线程中会出现不安全。 以 ArrayList 为例,我们进入 ArrayList 源码,找到 add() 方法,源码如下 public boo

为了远大的理想,冲锋! 3708

AT32串口打印全攻略:从零搭建到printf重定向(GCC版)

本文详细介绍了在GCC编译环境下为AT32系列MCU实现串口打印与printf重定向的完整方案。针对从Keil MDK迁移到GCC工具链时常见的串口无输出问题,文章从环境搭建、USART配置入手,重点剖析了GCC下printf重定向的核心原理——通过重写_write()函数,并提供了经过验证的代码实现、常见问题排查及性能优化建议。

weixin_29163797的博客 263

Minecraft Forge模组开发入门:从环境搭建到事件监听实战

事件驱动编程是一种基于事件和事件监听器的软件架构模式,其核心原理是通过事件总线实现组件间的松耦合通信。在游戏开发领域,这种模式能够有效处理玩家交互、状态变更等异步操作,具有高度的灵活性和可扩展性。Java作为面向对象编程语言,通过注解和反射机制为事件驱动提供了天然支持。Minecraft模组开发正是事件驱动编程的典型应用场景,开发者可以利用Forge框架提供的事件总线监听游戏内各种行为。本文以Minecraft Forge模组开发为例,详细讲解如何通过Gradle构建工具搭建开发环境,并实现自定义方块注册和

culi3182的博客 395

java多线程并发之旅-05-java 设计线程安全的类-实例封闭

问题 如何保证一个类是线程安全的?怎么验证? 线程安全的类可以成为组件,更加方便的创建线程安全的类吗? 线程安全的类有哪些条件 在设计线程安全类的过程中,需要包含以下三个基本要素: 1、找出构成对象状态的所有变量 2、找出约束状态变量的不变性条件 3、建立对象状态的并发访问管理策略 ps: 一言以蔽之,让所有变量都被对象并发管理。 1. 收集同步需求 要确保类的线程安全型,就要确保它的...

1万+

鲁棒优化入门(6)—Matlab+Yalmip两阶段鲁棒优化通用编程指南(上)

Matlab+Yalmip两阶段鲁棒优化通用编程指南(上篇),内容包括: ①.拿到一个复杂的两阶段鲁棒优化问题的分析步骤和方法。 ②.采用Yalmip工具箱中的uncertain函数和鲁棒优化模块求解两阶段鲁棒优化的子问题。 ③.Yalmip工具箱中的鲁棒优化模块和常规的求解思路有什么异同。 ④.使用KKT条件求解两阶段鲁棒优化的子问题,并使用C&CG算法进行迭代求解。

配电网和matlab的博客 1万+

Java并发编程(一)—Java内存模型以及线程安全

线程安全:在多线程环境下,多个线程可以安全地访问和操作共享数据,而不会引发数据不一致或程序错误的问题JMM 解决并发程序中最关键的两个问题:线程间的可见性和指令重排序。线程间的可见性:确保当一个线程修改了共享变量的值时,其他线程可以立即看到这一改变。没有良好的可见性保证,一个线程对共享变量的修改可能对其他线程不可见,导致数据不一致。指令重排序:为了提高性能,编译器和处理器常常会改变指令的执行顺序(只要这种改变不影响单线程内的程序逻辑)。然而,在多线程环境下,这种重排序可能导致严重问题。

echola_mendes的博客 1307

AutoSAR系列讲解(实践篇)7.3-AppL在Dev中的配置(下)

接着讲解Dev中剩下的几个定义项

雪云飞星的博客 1万+

【SA8155P】AIS Camera相关内容的简单介绍

【SA8155P】AIS Camera相关内容的简单介绍

静谧、淡雅 1696

Java并发编程札记-(一)基础-05线程安全问题

在多线程编程中,可能会出现多个线程访问一个资源的情况,资源可以是同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件等等。如果不对这样的访问做控制,就可能出现不可预知的结果。这就是线程安全问题,常见的情况是“丢失修改”、“不可重复读”、“读‘脏’数据”等等。 目录 线程安全问题 线程安全的实现 线程安全问题 上面简单介绍了什么是线程安全问题,下面具体说

潘威威的博客 1532

#systemverilog# 之 event region 和 timeslot 仿真调度(三)Verdi 辅助查看

通过今天的学习,我们知道:event region 是event sequence 的全集。我们打开了 event region feature 也就开启了event sequence 的功能。

那么菜的博客 1365

深入理解Java中的ConcurrentHashMap:高效线程安全的并发容器

是Java集合框架中的一个类,它实现了线程安全的哈希映射(类似于HashMap与传统的HashMap相比,允许多个线程并发地读取和写入数据,而不会导致数据不一致或并发冲突。通过巧妙的设计,避免了全表锁(类似于Hashtable),从而在高并发环境下表现出色。作为Java中强大且高效的线程安全集合类,在多线程编程中发挥着至关重要的作用。通过巧妙的分段锁机制、CAS 操作以及延迟初始化等技术,能够在高并发环境下提供出色的性能。在实际项目中,可以用于各种需要并发访问的场景,例如缓存系统、计数器、状态管理等。

私聊前往站内信:https://i.youkuaiyun.com/#/msg/chat/weixin_44976692 1万+

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

JUC高并发编程(05) -- 集合的线程安全 -- ArrayList集合

集合的线程安全 -- ArrayList集合

姜皓的博客 467

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

JavaWeb笔记05-解决线程安全问题

线程安全问题: Servlet的service方法,每次被请求是,调用. 这个调用很特殊,是在新的子线程中调用的,当service方法执行完毕,子线程死亡了. 可以简单的理解为:service方法每次执行都是一个新的线程. package cn.xdl.demo1; import javax.servlet.ServletException; import javax.servlet.annot...

秋叶夏风的博客 466

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

并发编程——05 并发锁机制之深入理解synchronized

`i++`/`i--`引起的线程安全问题、synchronized、附录:JVM指令集

念头通达 864

Java 并发集合:高效线程安全的数据结构

虽然并发集合提供了线程安全,但它们可能在某些情况下比非线程安全的集合性能低。Java提供了多种并发集合,它们是为线程安全而设计的高性能数据结构。并发集合虽然提供了线程安全,但在某些高并发场景下可能成为性能瓶颈,需要根据具体情况进行优化。大家好,我是微赚淘客返利系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!并发集合中的许多操作都是原子的,这意味着它们在执行过程中不会被其他线程中断。并发集合的设计遵循Java内存模型,确保在多线程环境中数据的一致性和可见性。

微赚开发者技术分享博客 512

线程安全的集合类和并发数据结构

Java提供了多种线程安全的集合类和并发数据结构,以满足不同场景下的并发需求。在选择合适的集合或数据结构时,需要根据实际的应用场景(如读写比例、并发量、是否需要阻塞等)来综合考虑。

qq_51321722的博客 547

Java并发包提供了哪些并发工具类?

蓝苍星2019-05-27 12:33:01 我们通常所说的并发包也就是 java.util.concurrent 及其子包,集中了 Java 并发的各种基础工具类,具体主要包括几个方面: 1) 提供了比 synchronized 更加高级的各种同步结构,包括 CountDownLatch、CyclicBarrier、Sempahore 等,可以实现更加丰富的多线程操作,比如利用 Semap...

u011277123的博客 7642

Java高级编程:深入探索并发编程与线程安全

Java的并发编程是一个庞大而复杂的领域,本文仅从基础概念、关键技术、线程安全问题和解决方案等方面进行了简要介绍,并通过一个使用实现线程池的代码案例展示了如何在实际项目中应用这些技术。要成为一名优秀的Java并发编程专家,还需要不断学习和实践,深入理解JVM的内存模型、锁机制、并发集合等高级特性,以及掌握性能调优和故障排查的技巧。

运维人生 560

【JUC —— 05 线程安全集合类】

在Java中,线程安全的集合类通过多种机制确保在多线程环境下操作集合时不会发生数据竞争或不一致的情况。Vector。

m0_72328778的博客 433

[6-04-05].第05节:并发编程 - 线程的同步

1.在线程同步的过程中,有时会出现死锁问题1.死锁的含义就是:不同的线程分别占用对方需要的同步资源不放弃都在等待对方放弃自己需要的同步资源,就形成了线程的死锁2.出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

F-T-er 994

11_05.【Java】线程安全与线程同步

一、线程的运行模式 1、线程与进程 为了实现在同一个时间运行多个任务,Java引入了多线程的概念。在Java中可以通通过方便、快捷的方式启动多线程模式。多线程常被应用在符合并发机制的程序中,例如网络程序等。 人体可以同时进行呼吸、血液循环、思考问题等活动,可以边听歌边聊天…这种机制在Java中被称为并发机制,通过并发机制可以实现多个线程并发执行,这样多线程就应运而生了。 以多线程在Windows操作系统中的运行模式为例,Windows操作系统事多任务操作系统,它以进程为单位。每个独立执行的程序都被称为进程,

changchuanlai 190

Java中的并发编程:如何在多线程环境中实现线程安全

在现代软件开发中,并发编程是一个不可避免的挑战,尤其是在多线程环境中,如何保证线程安全至关重要。是Java中最基本的实现线程安全的方式。减少线程之间共享的数据可以有效降低竞争条件的发生,从而简化线程安全问题的处理。避免死锁的关键是合理设计锁的获取顺序,并尽量减少锁的使用范围。不可变对象在创建后,其状态不能被修改,因此在并发编程中是天然线程安全的。等工具,结合线程安全的最佳实践,可以有效地避免并发编程中的常见问题,确保程序的稳定性和性能。等,这些工具可以帮助更好地管理多线程间的协作,避免常见的线程安全问题。

微赚淘客系统开发者博客 1086

Java并发面试题&知识点总结(上篇)

大家好,我是栗筝i,从 2022 年 10 月份开始,我便开始致力于对 Java 技术栈进行全面而细致的梳理。这一过程,不仅是对我个人学习历程的回顾和总结,更是希望能够为各位提供一份参考。因此得到了很多读者的正面反馈。而在 2023 年 10 月份开始,我将推出 Java 面试题/知识点系列内容,期望对大家有所助益,让我们一起提升。今天与您分享的,是 Java 并发知识面试题系列的总结篇(上篇),我诚挚地希望它能为您带来启发,并在您的职业生涯中起到助益作用。

栗筝i的博客 3111
上一篇: 一分钟,教你如何迅速定位OOM
下一篇: 06. 就该这么学并发 - 如何保证线程安全
码哥说
博客等级 码龄8年 5645粉丝 172原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码哥说

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值