多线程初阶——线程安全

线程安全

toc

1.什么是线程安全

场景:用两个线程同时对一个变量进行5万次自增操作,预期结果是自增10万次。

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

结果如下

和我们预期的结果10000不一样,因此这种现象称为线程不安全。

2.为什么会造成线程不安全

2.1线程抢占式的执行

我们知道线程的执行顺序并不是我们代码中的顺序执行的,抢占式执行是造成线程不安全的主要原因。

2.2 多个线程修改同一变量

当多个线程对同一个变量修改时由于线程的执行是抢占式的,因此导致了线程不安全

2.3 原子性

当我们在要执行自增操作时,count在CPU中通过指令进行自增

  1. 从内存或者寄存器中把count读出来 LOAD
  2. 执行自增操作 ADD
  3. 将计算结果写回内存或寄存器 **STORE **

当指令的执行按着以下的顺序执行时,保证了线程的原子性,这时我们就能得到我们预期的结果

当时当执行顺序出现了下面的情况时,还能得到我们预期的结果吗

这时的在CPU中的count的变化如下

指令执行前 count=0;

  1. t2获取count的值 count=0
  2. t1获取count的值 count=0
  3. t2进行ADD指令 count=1
  4. t1进行ADD指令 count=1
  5. t2写回count 的值
  6. t1写回count 的值

看似count在两个线程中分别进行了自增的操作,实际上在内存中的count只进行了一次自增操作(t2 最后将count的值写回内存后,t1再次将count的值写回到的内存,并覆盖了t2返回的值)。

2.4内存可见性

这里我们介绍一下JMM(Java Memory Model) Java内存模型

这里的主内存是Java虚拟机中的内存。

在JMM中每一个工作内存都是独立的,相互之间不可以访问

而内存可见性就是保证当一个线程改变了一个变量的值时,其他线程也能感知到变量的值发生了变化

因此JMM规定了

  1. 所有线程不能直接修改主内存中的共享变量
  2. 如果要修改内存中的变量,就需要把这个变量从主内存中复制到自己的工作内存中,修改完后再将数据刷新回主内存中
  3. 各个线程之间不能相互通信,做到内存级别的线程隔离

2.5 指令重排序

指令重排序,调整了代码的执行顺序,提高了代码的运行效率,在单线程中指令重排序对其没有影响,但由于多线程的执行顺序是抢占式的,代码执行顺序的调整,会出现难以预料的结果。

因此指令发生重排序的条件是

  1. 结果必须正确
  2. 重排序的操作逻辑上互不相干

总结

线程安全产生的原因

  1. 线程在CPU上是抢占式执行,抢占CPU资源是没有顺序的(程序猿无法处理)
  2. 多个线程修改了同一变量的值
  3. 指令执行没有保证原子性
  4. 修改变量时没有保证内存可见性
  5. 程序在编译时,可能会存在指令重排序

3.如何解决线程安全问题

要想解决线程安全问题,最最主要的是将线程的执行变为原子操作

那么该如何解决呢?这时我们就要进行加锁操作。

如上图当多个线程中的一个线程在执行自增操作时,自增之前进行加锁,自增操作结束后,进行解锁。使其他线程无法对count进行操作,让其他线程处于阻塞状态。

Java中加锁操作,需要用到synchronized关键字

java 复制代码
class Counter2{
    public int count =0;
    public synchronized void increase(){
        count++;
    }
}

加锁以后,其他线程就处于阻塞状态,这时程序由并行变成了串行,而大大的降低了运行效率,但是保证了线程安全。

这时我们就能很好的理解在学习String时,StringBuilderStringBuffr的区别了。

对于synchronized关键字,我们下回分析。

相关推荐
考虑考虑19 天前
图片居中
java·后端·java ee
云恒要逆袭1 个月前
Java SE、EE、ME到底啥区别?我被这个问题困扰了一整年
java·java ee
寻见9032 个月前
Java为什么能“一次编写,到处运行”?JVM到底解决了什么核心痛点?
java·jvm·java ee
技术小白菜2 个月前
海康平台通过代理播放视频流
java·java ee
寻见9032 个月前
救命!Spring Boot 凭什么火?从道法术器讲透,新手也能一键上手
java·spring boot·java ee
狂奔小菜鸡2 个月前
Day41 | Java中的锁分类
java·后端·java ee
狂奔小菜鸡2 个月前
Day40 | Java中的ReadWriteLock读写锁
java·后端·java ee
狂奔小菜鸡2 个月前
Day39 | Java中更灵活的锁ReentrantLock
java·后端·java ee
考虑考虑2 个月前
JDK25模块导入声明
java·后端·java ee
虚拟世界3 个月前
Spring Boot 中 static 到底怎么用?一文讲清,避开90%的坑
spring boot·java ee