线程安全问题

⭐ 作者:小胡_不糊涂

🌱 作者主页:小胡_不糊涂的个人主页

📀 收录专栏:JavaEE

💖 持续更文,关注博主少走弯路,谢谢大家支持 💖

线程安全

  • [1. 产生线程不安全的原因](#1. 产生线程不安全的原因)
    • [1.1 修改共享数据](#1.1 修改共享数据)
    • [1.2 内存可见性问题](#1.2 内存可见性问题)
    • [1.3 原子性问题](#1.3 原子性问题)
    • [1.4 指令重排序问题](#1.4 指令重排序问题)
  • [2. 解决办法](#2. 解决办法)
    • [2.1 加锁](#2.1 加锁)
    • [2.2 加volatile](#2.2 加volatile)

1. 产生线程不安全的原因

线程的调度是随机的,随机调度会使一个程序在多线程环境下,执行顺序存在很多的变数,此时就需要我们保证代码在任意执行顺序下,都能正常工作。

1.1 修改共享数据

多个线程修改同一个变量。

下面的代码中,涉及到多个线程针对 count 变量进⾏修改。此时这个 count 是⼀个多个线程都能访问到的 "共享数据"。

java 复制代码
public class TestDemo1 {
    private static int count =0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
           for(int i=0;i<5000;i++){
               count++;
           }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<5000;i++){
                count++;
            }
        });
        t1.start();
        t2.start();
        //保证t1,t2自增完后再打印
        t1.join();
        t2.join();
        System.out.println("count="+count);
    }
}

按照正常的思路来讲,t1自增5000,t2自增5000,count最终应该是10000,但为什么真正的输出跟我们想的不一样呢?

1.2 内存可见性问题

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

Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型。

⽬的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到⼀致的并发效果。

  • 线程之间的共享变量存在主内存 (Main Memory)
  • 每⼀个线程都有⾃⼰的 "⼯作内存" (Working Memory)
  • 当线程要读取⼀个共享变量的时候,会先把变量从主内存拷⻉到⼯作内存,再从⼯作内存读取数据
  • 当线程要修改⼀个共享变量的时候,也会先修改⼯作内存中的副本,再同步回主内存
    由于每个线程有⾃⼰的⼯作内存,这些⼯作内存中的内容相当于同⼀个共享变量的 "副本"。此时修改线程1的⼯作内存中的值,线程2的⼯作内存不⼀定会及时变化。
    这里的"主内存"是真正硬件角度的"内存"。⽽所谓的 "工作内存",则是指 CPU 的寄存器和高速缓存

例如:

1.初始情况下,两个线程的工作内容都是一样的

2.⼀旦线程1修改了 a 的值,此时主内存不⼀定能及时同步。对应的线程2的⼯作内存的 a 的值也不⼀定能及时同步

此时代码就容易出现问题。

1.3 原子性问题

什么是原⼦性?

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

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

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

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

如果⼀个线程正在对⼀个变量操作,中途其他线程插⼊进来了,如果这个操作被打断了,结果就可能是错误的。

1.4 指令重排序问题

⼀段代码的逻辑是这样的:

  1. 去前台取下 U 盘
  2. 去教室写10分钟作业
  3. 去前台取下快递
    如果是在单线程情况下,JVM、CPU指令集会对其进⾏优化,⽐如,按 1->3->2的⽅式执⾏,也是没问题,可以少跑⼀次前台。这种叫做指令重排序

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

2. 解决办法

2.1 加锁

synchronized 能够保证原⼦性,解决互斥问题。

java 复制代码
public class TestDemo1 {
    private static int count =0;
    public static void main(String[] args) throws InterruptedException {
        Object loker=new Object();
        Thread t1=new Thread(()->{
           for(int i=0;i<5000;i++){
               synchronized (loker) {
                   count++;
               }
           }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<5000;i++){
                synchronized (loker) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count="+count);//10000
    }
}

当t1线程拿到锁(loker)后开始count自增,t2线程则进入阻塞等待,等t1解锁后,t2获得锁,count再进行自增,就避免了因看不见内存中的变化而产生的错误。

2.2 加volatile

volatile关键字的作用主要有如下两个:

  1. 保证内存可见性:基于屏障指令实现,即当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
  2. 保证有序性:禁止指令重排序。编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。
    注意: volatile 不能保证原子性
相关推荐
无问8172 个月前
Javaee:线程安全问题和synchronized关键字
java·线程安全
蜗牛沐雨2 个月前
Rust中的Send特征:线程间安全传输所有权详解
开发语言·安全·rust·线程安全·send·sync
高耳机High-Earphone3 个月前
【Java】单例模式详解与实践
java·开发语言·单例模式·多线程·线程安全
小乖兽技术3 个月前
C#开发基础之单例模式下的集合数据,解决并发访问读写冲突的问题
单例模式·c#·线程安全·读写冲突·并发访问
小乌龟不会飞3 个月前
【Linux系统编程】用互斥量和信号量加锁STL容器,避免并发问题
c++·线程安全·stl容器··信号量·互斥量
一只淡水鱼664 个月前
【Java并发编程】JUC(java.util.concurrent) 包中的常见类的使用以及线程安全集合类
java·开发语言·java-ee·线程安全
初晴~4 个月前
【多线程】深入剖析线程安全问题
java·多线程·thread·线程安全
趙卋傑5 个月前
多线程初阶(二)- 线程安全问题
java·jvm·多线程·线程安全·synchronized·volatile·死锁
funnyZpC5 个月前
同时使用线程本地变量以及对象缓存的问题
java·缓存·线程安全·同步锁
IYF.星辰5 个月前
Java多线程-----线程安全问题(详解)
java·开发语言·线程安全