线程安全问题的原因和解决方案

1. 线程安全问题

  其实,简单来说就是执行了正常的程序处理逻辑得到的逻辑和预想的结果不一致。(列如:莫个代码,在单线程下执行没有问题,多线程执行下出现了bug,这样的代码就存在线程安全问题/线程不安全)。

1.1 线程不安全的情况

  这里是使用一个变量count,1. 让当线程完成100_0000次(count++) 2.让俩个线程分别进行50_0000次(count++),看count的值是否跟预期一样。 

 1.1.1 单线程执行
public class ThreadDemo14 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
           for(int i = 0; i < 100_0000; i++){
               count++;
           }
        });
        t.start();
        t.join();
        System.out.println("count的值为:" + count);
    }
}

运行结果:

线程安全问题的原因和解决方案_第1张图片

  可以看到这个与我们预期结果一致。

1.1.2 多线程执行(这里就用俩个线程来进行) 
public class ThreadDemo13 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //用俩个线程实现从0到100_0000的相加
        Thread t = new Thread(() -> {

           for(int i = 0; i < 50_0000; i++){
               count++;
           }
        });

        Thread tt = new Thread(() -> {
            for(int i = 50_0000; i <= 100_0000 ; i++){
                count++;
            }
        });
        //启动俩个线程开始计数
        t.start();
        tt.start();
        //让线程等待没算好的线程
        t.join();
        tt.join();
        System.out.println("count的值为:"+count);
    }
}

运行结果:

 线程安全问题的原因和解决方案_第2张图片

   在上图可以清楚的看到count的值为:558364 与我们预期的值相差很多,而且在这里我还想说明一点每次运行count的值也不同,那为什么会不同???

1.2 不安全情况的分析  

本质上是因为,线程之间的调度是"无序"的(抢占式运行/随机调度  给线程的顺序带来了变故)。

同时也是因为count++ 这个操作本质上是三个CPU指令构成的:

1. load: 把内存数据加载到寄存器中。

2. add: 把CPU中寄存器上的值+1。

3. save: 把寄存器数据写回到内存中

  因为当前是俩个线程一起修改同一个变量,每次修改需要进行这三步,并且线程之间的调度顺序的不确定的。 

   下图就是举出了一些列子:

线程安全问题的原因和解决方案_第3张图片 

1.2.1 什么是原子性

   一个操作或一组操作要么全部执行成功,要么全部不执行,不会出现部分执行的情况 。这里我们可以简单理解为,你在上厕所时锁门这个操作,你锁了门(锁门就是需要一定的机制来保证安全)另一个人进不来,这样子保证了原子性,反之则无。

  有的时候也把这个现象叫做同步互斥。

1.3 线程不安全的原因

 1.3.1 抢占式执行

   这个也是线程不安全的罪魁祸首

  线程的调度是不确定的, 并不是顺序执⾏, 也不是先调度出CPU的那 个线程就⼀定会在下⼀次第⼀个调度回CPU, 这种线程"抢占式"的 执⾏⽅式是造成线程不安全的主要原因。
 1.3.2 多线程修改同一个变量

1.单线程修改同一个变量不会产生线程不安全的现象。

2.多个线程度同一个线程不会产生线程不安全的想象。

3.多个线程修改不同的变量也不会产生线程不安全的想象。

1.3.3 原子性

上述多线程修改操作(count++),本身不是"原子的" 。像修改操作其实是执行load,add,save三条指令。CPU 执行指令, 都是以 "一个指令" 为单位进行执行的, 一个指令就相当于 CPU 上的最小单位, 不会发生指令执行到一半, 线程被调度走了的情况. 

1.3.4 内存可见性问题和指令重排问题

这是JVM的代码优先引出的bug。

2. 线程安全解决方案

   通过上述的分析我们可以总结出来,我们可以通过特殊的手段,把这三个指令打包到一起,成为一个整体 ----- "加锁"(锁具有"互斥","排它"这样的特性)。

  加锁: 可以把加锁这个操作看成谈恋爱,没有跟你在一起的时候可以跟很多异性朋友玩,但是在一起了就相当于你给你对象上了一把锁没有你的解锁别人不可能有机会。

2.1 synchronized 关键字(加锁的一种办法)

public class ThreadDemo13 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //在使用synchronized必须要有一个对象
        Object locked = new Object();
        //用俩个线程实现从0到100_0000的相加
        Thread t = new Thread(() -> {

           for(int i = 0; i < 50_0000; i++){
               synchronized (locked){
                   count++;
               }
           }
        });

        Thread tt = new Thread(() -> {
            for(int i = 50_0000; i < 100_0000 ; i++){
                synchronized (locked){
                    count++;
                }
            }
        });
        //启动俩个线程开始计数
        t.start();
        tt.start();
        //让线程等待没算好的线程
        t.join();
        tt.join();
        System.out.println("count的值为:"+count);
    }
}

运行结果:

 线程安全问题的原因和解决方案_第4张图片

   这里我们可以通过上述代码来简单来学习一下synchronized关键字:

   1.这里我们必须要一有一个任意对象(是什么不重要)放在synchronized (对象){语句},让系统知道是哪些线程去竞争这个锁。(如果是上述的列子,那俩个线程应该需要同一个对象,假设对象不相同则count则也会跟预期结果不一致哦!!!要千万注意哈)。

  2.其实synchronized它是一个非常强大的一个功能,它底层逻辑把加锁和解锁连接到一起,可以有效的防止程序猿忘记解锁的等等一系列操作。

  3. synchronized (对象){语句} 在语句的后一个花括号表示出了这个范围就解锁了,

线程安全问题的原因和解决方案_第5张图片

  所以我们可以看到关于线程安全问题我们可以通过synchronized()关键字来解决(上述设计到的只是对该关键字简单的解释,如果想要深入了解可以去找找),而且解决线程的方法也有很多种我这边涉及到的是一种比较简单,不容易的一种方式进行解析。 

你可能感兴趣的:(javaee)