我们在详细了解线程安全问题前,我们首先要了解的就是所谓的线程安全问题是什么,是什么原因造成了线程安全问题。
在这里,全部的万恶之源,罪魁祸首都来自于一点——多线程的抢占式执行。
如果没有多线程,此时程序的运行只能是固定的,代码的运行顺序固定,程序的结果就是固定的。
但是在多线程,抢占式执行下,此时代码就会出现很多的变数,**代码的执行顺序就成单一情况变成了无数种情况!**因此要保证这种情况下代码的运行正确就非常困难,只要在许多种情况中,有一种情况代码的结果不正确,就是视为线程不安全!
单纯的文字解释并不能很好的理解问题的本质,所以,我通过下面的代码来进行更加详细的解释。
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);}
}
运行结果如下:
从上面的两次结果我们不难发现,不但每次的计算数字不同,而且,计算的值与预期完全不符,预期为 10W 但是完全达不到。
要更好的解释这个问题我们就不得不说到 “count++” 这个操作的底层逻辑了。
下面通过画图来简单解释一下:
情况1:
上面是两种最理想的情况,两个线程正好都可以完整的执行一个周期。
底层逻辑图示:
情况2:
上图是两种一般情况(一般情况有很多种,这里就简单举出两个例子),可以看出,因为抢占式执行的原因,每个线程都不能做到完整的一个周期,这就是出错的根本来源!
底层逻辑图示
不难理解,正是因为这些一般情况,最终的导致计算结果出现很大的偏差。
我们肯定会有疑问,到底什么情况下会出现线程安全问题?是所有涉及到多线程的代码都会有线程安全问题吗?
整体上来看,最典型的有下面5种:
抢占式执行,随机调度。(根本原因)
这一点在前面已经进行了详细的解释。
代码结构:多个线程共同修改同一个变量
这个问题可以通过调整代码结构来规避,但是并不是所有的问题都可以调整代码结构来解决,这种方法的普适性较低。
原子性:以上面提到的 “count++” 拆分出的三个操作来说,其中的单个指令已经不能再拆分了,这就是原子性。
针对线程安全问题,让每个线程执行的操作原子化,即就是加锁操作将非原子转换为原子。
内存可见性问题: 一个线程针对一个变量进行读取,同时另一个线程针对这个变量进行修改,此时读到的值不一定就是修改后的值。 这个读线程没有感知到变量的变化。(这个原因会在后面的文章中进行详细的介绍)
指令重排序:本质上就是编译器优化出现 bug
编译器为了加快执行效率,在保证逻辑不改变的情况下,将代码自作主张的进行了调整。
上一篇:C语言—初识C语言
下一篇:网页基础及Python库的使用