JavaEE——线程安全问题

文章目录

  • 一、何为线程安全问题
  • 二、解释线程安全问题——抢占式执行
    • 1. 通过代码展示
    • 2.通过计算机底层逻辑解释
  • 三、出现线程安全问题的原因

一、何为线程安全问题

我们在详细了解线程安全问题前,我们首先要了解的就是所谓的线程安全问题是什么,是什么原因造成了线程安全问题。

在这里,全部的万恶之源,罪魁祸首都来自于一点——多线程的抢占式执行

如果没有多线程,此时程序的运行只能是固定的,代码的运行顺序固定,程序的结果就是固定的

但是在多线程,抢占式执行下,此时代码就会出现很多的变数,**代码的执行顺序就成单一情况变成了无数种情况!**因此要保证这种情况下代码的运行正确就非常困难,只要在许多种情况中,有一种情况代码的结果不正确,就是视为线程不安全!

二、解释线程安全问题——抢占式执行

1. 通过代码展示

单纯的文字解释并不能很好的理解问题的本质,所以,我通过下面的代码来进行更加详细的解释。

class Counter{
    public int count = 0;

    public void increase(){
        count++;
    }
}

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        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);
    }
}

运行结果如下:
JavaEE——线程安全问题_第1张图片
JavaEE——线程安全问题_第2张图片

从上面的两次结果我们不难发现,不但每次的计算数字不同,而且,计算的值与预期完全不符,预期为 10W 但是完全达不到。

2.通过计算机底层逻辑解释

要更好的解释这个问题我们就不得不说到 “count++” 这个操作的底层逻辑了。

  1. load:先将内存中的值,读取到 CPU 寄存器中。
  2. add:将 CPU 寄存器中的数值 +1 运算。
  3. save:将得到的结果写入内存中。

下面通过画图来简单解释一下:
情况1:
JavaEE——线程安全问题_第3张图片
上面是两种最理想的情况,两个线程正好都可以完整的执行一个周期。
底层逻辑图示:
JavaEE——线程安全问题_第4张图片

情况2:
JavaEE——线程安全问题_第5张图片
上图是两种一般情况(一般情况有很多种,这里就简单举出两个例子),可以看出,因为抢占式执行的原因,每个线程都不能做到完整的一个周期,这就是出错的根本来源!
底层逻辑图示
JavaEE——线程安全问题_第6张图片
不难理解,正是因为这些一般情况,最终的导致计算结果出现很大的偏差。

三、出现线程安全问题的原因

我们肯定会有疑问,到底什么情况下会出现线程安全问题?是所有涉及到多线程的代码都会有线程安全问题吗?

整体上来看,最典型的有下面5种:

  1. 抢占式执行,随机调度。(根本原因)
    这一点在前面已经进行了详细的解释。

  2. 代码结构:多个线程共同修改同一个变量
    这个问题可以通过调整代码结构来规避,但是并不是所有的问题都可以调整代码结构来解决,这种方法的普适性较低。

  3. 原子性:以上面提到的 “count++” 拆分出的三个操作来说,其中的单个指令已经不能再拆分了,这就是原子性。
    针对线程安全问题,让每个线程执行的操作原子化,即就是加锁操作将非原子转换为原子。

  4. 内存可见性问题: 一个线程针对一个变量进行读取同时另一个线程针对这个变量进行修改此时读到的值不一定就是修改后的值。 这个读线程没有感知到变量的变化。(这个原因会在后面的文章中进行详细的介绍)

  5. 指令重排序:本质上就是编译器优化出现 bug
    编译器为了加快执行效率,在保证逻辑不改变的情况下,将代码自作主张的进行了调整。

你可能感兴趣的:(JavaEE,java-ee,java)