线程安全问题

一、什么是线程安全问题

        定义:指当多个线程同时访问共享资源(如变量、对象、文件等)时,由于线程执行顺序的不确定性或资源竞争,可能导致数据不一致、程序崩溃或逻辑错误的现象。

现代计算机体系下的CPU、内存、I/O设备的速度是有极大差异的,程序运行时,内存读写和IO设备的读写速度远远跟不上CPU计算的速度,这导致CPU性能过剩,或者说,无法完全发挥CPU性能的问题。

当线程因为等待IO读写而陷入阻塞时,该线程是不需要CPU的,因为此时是由其他硬件设备进行读写操作的。

为了尽量减少这种速度差距带来的CPU浪费,现代计算机体系中的CPU硬件、操作系统、编译器都提出了一系列方法来试图缓和这个矛盾。包括:

  1. CPU增加了高速缓存,以均衡与内存的速度差异。
  2. 编译器和CPU优化了指令执行次序,让指令尽量并行执行,从而提高高速缓存的利用率。
  3. 操作系统增加了进程、线程,更好地分时复用CPU来提高并发效率,进而均衡CPU与I/O设备的速度差异。

上述这些优化在提高了CPU利用效率的同时,也带来了可见性,有序性和原子性这三个问题。在单线程模式下,它们不会出现或者不会带来问题,但在多线程模式下,它们可能会导致线程出现各种不安全的问题。它们是Java多线程开发必须要解决的三个问题,而这三个问题也就是我们常说的线程安全问题。

二、并发编程的三个特性

原⼦性
⼀个操作或者多个操作,要么全部执⾏并且执⾏的过程不会被任何因素打断,要么就都不执⾏。
        原⼦性是拒绝多线程操作的,不论是多核还是单核,具有原⼦性的量,同⼀时刻只能有⼀个线程来对它进⾏操作。 简⽽⾔之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原⼦性。例如 a=1 是原⼦性操作,但是 a++和 a +=1 就不是原⼦性操作。 Java 中的原⼦性操作包括:
        基本类型的读取和赋值操作,且赋值必须是值赋给变量,变量之间的相互赋值不是原⼦性操作;
        所有引⽤reference 的赋值操作;
        java.concurrent.Atomic.* 包中所有类的⼀切操作。
可⻅性
指当多个线程访问同⼀个变量时,⼀个线程修改了这个变量的值,其他线程能够⽴即看得到修改的值。
        在多线程环境下,⼀个线程对共享变量的操作对其他线程是不可⻅的。Java 提供了 volatile 来保证可⻅性,当⼀个变量被volatile 修饰后,表示着线程本地内存⽆效,当⼀个线程修改共享变量后他会⽴即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。synchronize Lock 都可以保证可⻅性。 synchronized Lock 能保证同⼀时刻只有⼀个线程获取锁然后执⾏同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因 此可以保证可⻅性。

有序性
即程序执⾏的顺序按照代码的先后顺序执⾏。
        Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在⼀个线程中观察另⼀个线程,所有操作都是⽆序的。前半句是指“ 线程内表现为串⾏语义 ,后半句是指 指令重排序 现象和 ⼯作内存主主 内存同步延迟” 现象。在Java 内存模型中,为了效率是允许编译器和处理器对指令进⾏重排序,当然重排序不会影响单线程的运⾏结果,但是对多线程会有影响。Java 提供 volatile 来保证⼀定的有序性。最著名的例⼦就是单例模式⾥⾯的 DCL (双重检查锁)。另外,可以通过synchronized Lock 来保证有序性, synchronized Lock 保证每个时刻是有⼀个线程 执⾏同步代码,相当于是让线程顺序执⾏同步代码,⾃然就保证了有序性。
为了让⼤家更好理解可⻅性和有序性,这个就不得不了解 内存模型 重排序 内存屏障 ,因为这三个概 念和他们关系⾮常密切。
内存模型
JMM 决定⼀个线程对共享变量的写⼊何时对另⼀个线程可⻅, JMM 定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory) 中,每个线程都有⼀个私有的本地内存( Local Memory ),本地内存保存了被该线 程使⽤到的主内存的副本拷⻉,线程对变量的所有操作都必须在⼯作内存中进⾏,⽽不能直接读写主内存中的变量。

对于普通的共享变量来讲,线程 A 将其修改为某个值发⽣在线程 A 的本地内存中,此时还未同步到主内存中去;⽽线 程B 已经缓存了该变量的旧值,所以就导致了共享变量值的不⼀致。解决这种共享变量在多线程模型中的不可⻅性 问题,可以使⽤volatile synchronized final 等,
此时 A B 的通信过程如下:
⾸先,线程 A 把本地内存 A 中更新过的共享变量刷新到主内存中去;
然后,线程 B 到主内存中去读取线程 A 之前已更新过的共享变量。
JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可⻅性保证,需要注意的是,JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,并不⼀定就真实的对应cpu缓存和物理内存。总结⼀句话,内存模型JMM控制多线程对共享变量的可⻅性!!!
重排序
重排序是指编译器和处理器为了优化程序性能⽽对指令序列进⾏排序的⼀种⼿段。
重排序需要遵守⼀定规则:
重排序操作不会对存在数据依赖关系的操作进⾏重排序。⽐如: a=1;b=a; 这个指令序列,由于第⼆个操作依赖于第⼀个操作,所以在编译时和处理器运⾏时这两个操作不会被重排序。
重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执⾏结果不能被改变。 ⽐如:
a=1;b=2;c=a+b 这三个操作,第⼀步( a=1) 和第⼆步 (b=2) 由于不存在数据依赖关系, 所以可能会发⽣重排 序,但是c=a+b 这个操作是不会被重排序的,因为需要保证最终的结果⼀定是 c=a+b=3
重排序在单线程下⼀定能保证结果的正确性,但是在多线程环境下,可能发⽣重排序,影响结果,请看下⾯的示例
代码:
class ReorderExample {
 int a = 0;
 boolean flag = false;
 public void writer() {
 a = 1; //1
 flag = true; //2
 }
 Public void reader() {
 if (flag) { //3
 int i = a * a; //4
 System.out.println(i);
 }
 }
}
内存屏障
为了保证内存可⻅性,可以通过 volatile final 等修饰变量, java 编译器在⽣成指令序列的适当位置会插⼊内存屏障 指令来禁⽌特定类型的处理器重排序。内存屏障主要有3 个功能: 它确保指令重排序时不会把其后⾯的指令排到内存屏障之前的位置,也不会把前⾯的指令排到内存屏障的后
⾯;即在执⾏到内存屏障这句指令时,在它前⾯的操作已经全部完成;
它会强制将对缓存的修改操作⽴即写⼊主存;
如果是写操作,它会导致其他 CPU 中对应的缓存⾏⽆效。
volatile boolean flag = false; 
     int  a= 0;

    public void writer() {
        a = 1; //1
        flag = true; //2
    }
    public void reader() {
        if (flag) { //3
            int i = a * a; //4
            System.out.println(i);
        }
    }

内容概要:本文档详细介绍了如何使用MATLAB实现粒子群优化算法(PSO)优化极限学习机(ELM)进行时间序列预测的项目实例。项目背景指出,PSO通过模拟鸟群觅食行为进行全局优化,ELM则以其快速训练和强泛化能力著称,但对初始参数敏感。结合两者,PSO-ELM模型能显著提升时间序列预测的准确性。项目目标包括提高预测精度、降低训练时间、处理复杂非线性问题、增强模型稳定性和鲁棒性,并推动智能化预测技术的发展。面对数据质量问题、参数优化困难、计算资源消耗、模型过拟合及非线性特征等挑战,项目采取了数据预处理、PSO优化、并行计算、交叉验证等解决方案。项目特点在于高效的优化策略、快速的训练过程、强大的非线性拟合能力和广泛的适用性。; 适合人群:对时间序列预测感兴趣的研究人员、数据科学家以及有一定编程基础并希望深入了解机器学习优化算法的工程师。; 使用场景及目标:①金融市场预测,如股票走势预测;②气象预报,提高天气预测的准确性;③交通流量预测,优化交通管理;④能源需求预测,确保能源供应稳定;⑤医疗健康预测,辅助公共卫生决策。; 其他说明:文档提供了详细的模型架构描述和MATLAB代码示例,涵盖数据预处理、PSO优化、ELM训练及模型评估等关键步骤,帮助读者全面理解和实践PSO-ELM模型。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值