Bootstrap

线程安全问题

目录

一、线程安全概念

二、线程安全产生的原因

1、随机调度,抢占式执行

2、多个线程同时修改同一个变量

3、修改操作不是原子的

4、内存可见性

5、指令重排序

三、如何解决线程安全问题

一、线程安全概念

        线程安全是指一个类或者一段代码在多线程环境下被多个线程同时访问时,能够保证数据的正确性和完整性,不会出现数据错乱、结果不一致等异常情况。

        简单来说,出现bug,就是线程不安全行为。

二、线程安全产生的原因

1、随机调度,抢占式执行

        什么是随机调度?随机调度指的是在多线程操作系统中,多个线程会同时运行,但是,cpu的核心数量是有限的,所以得按一定的方法或者策略,像是时间片这样(时间片:每一个线程按一定的时间运行,时间用完到下一个线程使用时间,而这个时间就是时间片)的方法,让每一个线程都获得一定的cpu来执行。

       什么是抢占式执行?举一个例子,在我们听歌的时候,突然有一个电话打过来,我们的歌就会暂停,是因为电话的优先级线程比听歌的优先级线程要高,所以它会抢占当时正在运行的低优先级线程,执行自己的任务,直到终止通话,而抢占式执行就是一个高优先级的线程抢占低优先级的线程执行自己的任务

2、多个线程同时修改同一个变量

        在多线程中,它们可能会修改同一个变量,然后导致结果不一样;如下图

        在count=0的时候,分别执行了一次thread1和thread2,我们预想的结果是count=2,不巧的是,在执行的过程中thread1和thread2的值拿到的都是原先的count=0的值,所以它们俩个修改原先的值都修改变成1,所以结果也是1。

3、修改操作不是原子的

        原子操作是指不可分割的才足以,即不会被中断的操作,如果修改不是原子的话,那就有可能会被中断,以代码为例子:

public class ThreadDemo15ThreadS {
    private static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Object locker=new Object();
        Object locker2=new Object();

        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                    count++;
            }
            System.out.println("t1结束");
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                    count++;
            }
            System.out.println("t2结束");
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

        我们想象的结果应该是100000 ,但结果如下:

        为什么呢?因为线程要执行count++这个步骤并不是一步形成的,而是分几步 ,第一步需要加载load,也就是读取值,第二部add,也就是count++,第三步是save,保存。如图:

              而由于多线程并不是原子的,所以这三个步骤可能在某一步骤会被中断,比如在线程t1load的时候然后被中断,t2load,那么它们读取的值都是同一个值,而t2就拿不到t1add之后的值,这就会导致结果和预想不一样,也被称为线程不安全。

4、内存可见性

        内存可见性指的就是当一个线程对共享变量的修改,能够及时被其他线程看到。而在多线程中,每个线程都有自己的工作内存,工作内存包i村里线程的变量副本。线程对变量的读写操作都是在工作内存中进行的,不是直接操作主内存。简单举例:线程A线程B需要读取变量C的值,而线程A变量C的修改不能及时被线程B看到,而线程B拿到的变量还是原来的变量,而这样就会导致结果错误。

5、指令重排序

        指令重排序是指编译器和处理器为了优化程序功能,可能对指令的执行顺序进行调整,比如:

boolean initialize = false;
int value = 0;
// 初始化对象
value = 1; // 1
initialize = true; // 2

        按顺序来看,先执行1,再执行2,但通过重排序可能会变成先执行2,再执行1,这样会导致一个问题, 其他线程再还没执行value=1这个步骤的时候,就读取initiallize=true,此时value还是为0,这样就出现了错误。

三、如何解决线程安全问题

        我们先写一个synchronized的例子:

public static void main(String[] args) throws InterruptedException {
        Object locker=new Object();
        Object locker2=new Object();

        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (locker){
                        count++;
                }
            }
            System.out.println("t1结束");
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (locker){
                        count++;
                }
            }
            System.out.println("t1结束");
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

        结果如下:

        我们可以发现,结果是100000,而在前面那个例子并不是100000,是因为加入了synchronized,使用方法也很简单,实例化一个Object类 ,然后把这个类用synchronized锁上需要加入锁的变量或者类,这样就能得到锁的效果(锁同一个Object类才会有效果)。

        synchronized主要有俩个特性,第一个特性是互斥,第二个特性是可重入;我们先来讲一下互斥,互斥,顾名思义就是不能放一起,当某个线程在执行某个对象使用synchronized的时候,其他线程如果先要用到同一个synchronized,必须要阻塞等待前一个使用完,举一个例子,就好比我们上厕所,每次只能进去一个人,如果后面的人还需要上厕所的话那就要等前面一个人上完厕所才能进去,这就是互斥。

        而第二个可重入,我们以代码为例子:

 public static void main(String[] args) throws InterruptedException {
        Object locker=new Object();
        Object locker2=new Object();

        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (locker){//双重锁
                    synchronized (locker){
                        count++;
                    }
                }
            }
            System.out.println("t1结束");
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (locker){
                    synchronized (locker){
                        count++;
                    }
                }
            }
            System.out.println("t1结束");
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

        和上面代码相比,第二个代码中同一个变量多了几个锁 ,而这些锁是可重入的,也就是锁,加了多重锁跟没加一样,这也是jvm的好处之一。

     

;