在我们编写多线程程序的时候,经常会因为线程安全问题导致出现各种各样的bug,这里我们总结一些线程安全的原因和大致的解决思路
目录
这个问题时我们编写多线程代码时,出现线程安全问题的罪魁祸首,当然,这个问题也是操作系统方面的,我们并没有办法去解决这个问题
在多线程中,我们多个线程修改同一个变量的时候,也很容易触发线程安全问题,我们可以适当调整代码来解决这个问题,但是并不是每次都可以调节代码的,所以这个也不是重点
我们改变不了上面的两个问题,但是我们可以改变这个问题,我们可以吧操作变成原子操作,通过加锁操作来实现,这样我们就可以一定程度上保护线程安全
所谓的内存可见性,就是指一个线程修改了一个公共的值,另一个线程也能看到
指令重排序就是指,在单线程情况下,很多代码的执行顺序会影响执行速度,写编译器的大佬就把这代码进行重排序优化
前面的两个我们没有办法改变,我们从第三个开始讲解决思路
在java中操作不是原子性的很容易出现问题,就比如我们这一段代码
- public class Main {
- public 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.join();
- t2.join();
- System.out.println(count);
- }
- }

这段代码中我们使用两个线程对count进行累加操作,但是得到的结果好像并不是我们想要的,这就出现了线程安全问题,因为++操作其实有三个子操作,分别是load,add,save,当着三个子操作不是原子操作的时候,可能就会出现两个线程执行++只进行了一次自增的情况,所以导致结果不是我们想要的,
那我们应该怎么解决呢?
加锁!!!
我们可以通过加锁操作,让操作变成原子性的
经过改进,我们给两个线程加上锁,使其成为原子操作(这里的代码编写并不是很好,只是为了表达意思)
- public class Main {
- public static int count = 0;
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(() ->{
- synchronized (Main.class){
- for (int i = 0; i < 5000; i++) {
- count++;
- }
- }
- });
- Thread t2 = new Thread(() ->{
- synchronized (Main.class){
- for (int i = 0; i < 5000; i++) {
- count++;
- }
- }
- });
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println(count);
- }
- }

可以看到我们的结果就得到了10000,也就解决了这种类型的问题
直接看这个问题先
- class A extends Thread {
- public int i = 0;
- public void run() {
- while(i == 0){
-
- }
- }
- }
-
-
- public class Main {
- public static void main(String[] args) throws InterruptedException {
- A t1 = new A();
- Scanner sc = new Scanner(System.in);
- t1.start();
-
- t1.i = sc.nextInt();
- t1.join();
- }
- }
这里我们执行的结果是

可以看到当我们输入5之后,他并没有结束线程,这是为什么呢?
因为我们的编译器进行了自动的优化,让我们的while循环中的判断只执行一次,如果在单线程的情况下,这样当然是没问题的了,但是在多线程的情况下,就会出问题,闹我们如何解决呢?
前面提到了volatile关键字,我们对代码进行修改
- class A extends Thread {
- public volatile int i = 0;
- public void run() {
- while(i == 0){
-
- }
- }
- }
-
-
- public class Main {
- public static void main(String[] args) throws InterruptedException {
- A t1 = new A();
- Scanner sc = new Scanner(System.in);
- t1.start();
-
- t1.i = sc.nextInt();
- t1.join();
- }
- }

可以看到,当我们输入5之后,线程直接就停止了,volatile到底干了什么呢?
其实很简单,他就是跟编译器说,这个变量,会有别的线程来读,就老老实实的去读数据,就那么简单,这样我们就解决了内存可见性的问题.
指令重排序也是编译器优化的一种,就是把一些执行的执行顺序给他改变了,这在单线程的情况下肯定任何问题,但是在多线程的情况下,就不一定了,多线程的情况下可能会导致wait()这种关键字得到错误的信息,那我们怎么办呢?
还是加volatile关键字,给我们可能发生指令重排序的实例或者变量加上这个关键字的时候,就不会发生指令重排序的问题了,比如我们的线程安全的"懒汉"单例模式,就是通过volatile关键字
经典的面试问题:
简述volatile关键字的作用:
1.保证内存的可见性,用屏障指令实现
2.禁止指令重排序,编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。