如果多个线程在做同一件事情的时候。

  • 原子性 Synchronized, AtomicXXX、Lock、
  • 可见性 Synchronized, volatile
  • 有序性 Synchronized,volatile

原子性问题

在下面的案例中,演示了两个线程分别去去调用 demo.incr 方法来对 i 这个变量进行叠加,预期结果应该是20000,但是实际结果却是小于等于20000的值。

public class Demo {
    int i = 0;
    public void incr(){
        i++;
    }
    public static void main(String[] args) {
        Demo demo = new Demo();
        Thread[] threads=new Thread[2];
        for (int j = 0;j<2;j++) {
            threads[j]=new Thread(() -> { // 创建两个线程
                for (int k=0;k<10000;k++) { // 每个线程跑10000次
                    demo.incr();
                }
            });
            threads[j].start();
        }
        try {
            threads[0].join();
            threads[1].join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(demo.i);
    }
}

问题的原因

在上面这段代码中,count++是属于Java高级语言中的编程指令,而这些指令最终可能会有多条CPU指令来组成,而count++最终会生成3条指令, 通过javap -v xxx.class 查看字节码指令如下。

public incr()V
	L0
	LINENUMBER 13 L0
	ALOAD 0
	DUP
	GETFIELD com/gupaoedu/pb/Demo.i : I // 访问变量i
	ICONST_1 // 将整形常量1放入操作数栈
	IADD // 把操作数栈中的常量1出栈并相加,将相加的结
	果放入操作数栈
	PUTFIELD com/gupaoedu/pb/Demo.i : I // 访问类字段(类变量),复制给Demo.i这个变
	量

这三个操作,如果要满足原子性,那么就需要保证某个线程在执行这个指令时,不允许其他线程干扰,然后实际上,确实会存在这个问题。

图解问题本质

前面我们说过,一个CPU核心在同一时刻只能执行一个线程,如果线程数量远远大于CPU核心数,就会发生线程的切换,这个切换动作可以发生在任何一条CPU指令执行完之前。对于 i++ 这三个cpu指令来说,如果线程A在执行指令1之后,做了线程切换,假设切换到线程B,线程B同样执行CPU指令,执行的顺序如下图所示。就会导致最终的结果是1,而不是2

并发编程带来的安全性挑战_java


这就是在多线程环境下,存在的原子性问题,那么,怎么解决这个问题呢?大家认真观察上面这个图,表面上是多个线程对于同一个变量的操作,实际上是count++这行代码,它不是原子的。所以才导致在多线程环境下出现这样一个问题。也就是说,我们只需要保证,count++这个指令在运行期间,在同一时刻只能由一个线程来访问,就可以解决问题。这就需要引出到今天的课程内容, 同步锁Synchronized