总结一下多线程为啥不安全?

1.抢占式执行:

由于线程和线程之间的执行顺序是无序的,是抢占式执行的,所以系统调度执行线程式随机的。(导致不安全最重要的一点)

解决方案:加入Thread类中的join方法,(在哪个线程中加入join方法,就让哪个线程进行等待)让一个线程等待另一个线程执行完毕之后,这个线程再开始执行,此时就会从并发变成完整的串行执行了。

2.多个线程修改同一个变量:

注意这里的措辞,首先式多个,所以单线程就不涉及到什么安不安全的问题了,第二个,是修改,而不是读取或者写入,因为修改这个操作,不是原子性的(不可分割的最小单位),第三个,是同一个变量(如果是多个线程修改多个变量就是安全的,换个角度来说也算是一个线程修改一个变量)。

解决方案:加锁。锁的核心操作有两个,一个是加锁,另一个是解锁。加入synchronized关键字来完成加锁操作,是以代码块的形式来进行加锁和解锁的(进了这个关键字修饰的代码块就对代码块中的逻辑执行进行加锁,出了这个代码块就自动解锁了),当加锁之后,加锁的代码块就进行串行执行了,此时不会产生bug。

要对同一个对象进行加锁才可以产生效果,也就是synchronized()中的参数需要是相同的,()中的参数是锁对象的意思,就是对哪一个对象进行加锁,比如上厕所要锁门,此时这个锁对象就是这个坑位,对这个坑位进行加锁,下一个人来的时候也要用这个坑位,他就得等着你完事了之后,解锁了,他才能去加锁解锁。只有是一个锁对象的时候才会产生锁竞争,要不然你对这个坑位进行加锁,他来了去你旁边的坑位了,此时这就不是同一个锁对象了,你俩就可以同时进行并发操作了。

加锁操作和join方法的区别:加锁操作只是一个线程中的一部分代码进行串行执行,而join方法是对两个线程完全执行串行执行。如下代码:  当两个线程启动之后,只是对打印这行代码进行了操作,而t1线程中还有给i赋值,判定i<100,然后还有i++这些操作是不影响的,是可以和t2线程并发执行的,只有说当t1线程执行到了打印这行代码,而t2这个线程也正好执行到了打印这行代码的时候,此时这两行代码会进行串行执行。

public static void main(String[] args) {
        Object object = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                synchronized (object) {
                    System.out.println("hello t1");
                }
            } 
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                synchronized (object) {
                    System.out.println("hi t2");
                }
            }
        });
        t1.start();
        t2.start();
    }

3.修改操作不是原子性的:

总结修改这个操作为什么不是原子的,比如对一个变量进行一个++操作。看起来只是一个++操作,实际上这里大概要分成3步,有3个CPU指令来完成这个代码级别的++操作:

1.CPU中的寄存器首先从内存中读取这个变量(把这个变量写到寄存器中)。

2.在寄存器中对这个变量进行++操作。

3.再把寄存器中的这个变量写回到内存中。

所以说这里就涉及到了3个CPU的指令,而线程和线程之间的执行又是抢占式执行,此时就可能导致一个线程会覆盖另一个线程执行的结果,此时代码就出了bug,引起线程不安全。

解决方案:同上一样,也是进行加锁操作,引入synchronized关键字,操作执行的原子性。

4.内存可见性引起的线程不安全:

java 多线程专题 java 多线程问题_加锁

可以看上图代码:当t1和t2线程启动的时候,我们要修改flag的值,通过此种方式让t1线程结束,但是实际效果并未达到,t1这个线程并未结束。

原因:编译器的自动优化。当t1线程走到while循环时,其实CPU中执行了两个操作,一个是将内存中的flag读到寄存器中,第二步就是在寄存器中进行比较。注意:load(从内存读到寄存器中)是需要耗时的,而第二步从寄存器中进行比较也是需要耗时的,但是此时load这个操作的耗时远超出从寄存器中读值,大概进行一次从内存中读取,可以进行几千次从寄存器中读取,所以编译器认为load这个操作是一个负担,所以编译器就会优化这个load操作,只执行一次对flag的读取,接下来进行比较的时候就复用读取到的这个flag的值,所以在t2线程对flag值进行修改的时候,也感知不到这个操作,所以t1线程也就不会结束。

解决方案:使用volatile关键字,禁止编译器的优化,保证内存可见性(引入volatile关键字之后的效果称为 ”内存可见性“ )。volatile关键字不保证操作的原子性,所以如果是第一条和第二条原因产生的线程不安全,此种方式是无法解决的。

  所以每个关键字都会有自己的应用场景:

  volatile关键字:应用在一个线程读,一个线程写的场景。

  synchronized关键字:应用在多个线程写的场景。

5.执行指令重排序引起的线程不安全:

java 多线程专题 java 多线程问题_java 多线程专题_02

 有一个Student类,类中包含成员方法learn,只是举例子,先不关心语法错误,当t1和t2线程启动后,t1线程这个new对象其实是大概分三个指令的,

1.给这个对象分配一块内存空间,

2.调用构造方法来初始化这块空间里的数据,

3.将这个对象的引用赋值给s。

指令重排序也是编译器的一种优化,但是这个例子可能体现不出来。1这个指令一定是要先取执行的,但是2和3两个指令在优化的时候可能会颠倒顺序,可能先执行3,先将这个引用赋值给s之后,t1线程和t2线程是并发执行的,此时t2线程判断s是不为空的,就会去调用这个对象的learn方法,但是指令2此时还未执行到,还没有调用构造方法去构造这个对象,对这块空间的数据进行初始化,所以就会出bug。

解决方案:同上一样,也是引入volatile关键字来禁止操作指令重排序,禁止编译器的优化。