线程安全——概念及三要素(一)

线程安全

当多个线程同时访问某个类时,不管运行时环境采用何种调用方式,或者这些进程如何交替执行,并且在主调代码中不需要额外的同步和协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

三要素

  1. 原子性:提供了互斥访问,同一时刻只能有一个线程对他进行操作。
  2. 可见性:一个线程对主内存的修改可以及时的被其他线程观察到。
  3. 有序性:一个线程观察其他线程的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。

原子性

  1. 什么是原子操作?

    原子是世界上的最小单位,具有不可分割性。比如 a=0;(a 非 long 和 double 类型)这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++;这个操作实际是 a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。

  2. 如何保证原子性?

    原子类: 比如:AtomicInteger、AtomicLong、AtomicReference 等。 原子类底层实现通常借助于本地方法 Unsafe 及 CAS 实现,每次执行计算之前都会拿当前工作内存中的值和主内存的值比较,如果不相同就会从新从主内存中获取最新值赋值给当前对象,直到相同执行对应操作。

    同步锁: synchronsized、lock等。

  3. 原子类的不足(ABA问题)

    我们知道原子操作都是基于 cas 来保障原子性,但同时在 cas 中又会存在 ABA 问题,当一个线程对一个对象的值进行 cas 操作时,另外一个线程对值进行了两次修改,值最后改为旧值,此时就无法判断对象是否会被修改。

    为了解决 ABA 问题,伟大的 java 为我们提供了 AtomicMarkableReference 和 AtomicStampedReference 类,为我们解决了问题。
    AtomicStampedReference 是利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在 ABA 问题了。

可见性

  1. 什么是可见性?

    可见性是指,当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。可见性问题是好多人忽略或者理解错误的一点。

    CPU 从主内存中读数据的效率相对来说不高,现在主流的计算机中,都有几级缓存。每个线程读取共享变量时,都会将该变量加载进其对应 CPU 的高速缓存里,修改该变量后,CPU 会立即更新该缓存,但并不一定会立即将其写回主内存(实际上写回主内存的时间不可预期)。此时其它线程(尤其是不在同一个 CPU 上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。

  2. 怎么保证可见性?

    volatile 关键字,java 使用 volatile 关键字来保证可见性,通过加入 内存屏障 来实现。

    内存屏障 是一个 CPU 指令。编译器和 CPU 可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障, 相当于告诉 CPU 和编译器先于这个命令的必须先执行,后于这个命令的必须后执行;另一个作用是强制更新一次不同 CPU 的缓存。例如,一个写屏障会 把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个 cpu 核心或者哪颗 CPU 执行的。

    注:volatile并不能保证原子性

有序性

  1. 什么是有序性?

    处理器为了提高程序整体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码。

  2. 如何保证有序性?

    Java 中可通过 volatile 在一定程序上保证顺序性,另外还可以通过 synchronized 和锁来保证顺序性。

    synchronized 和锁保证顺序性的原理和保证原子性一样,都是通过保证同一时间只会有一个线程执行目标代码段来实现的。

    除了从应用层面保证目标代码段执行的顺序性外,JVM 还通过被称为 happens-before 原则隐式地保证顺序性。

文章目录
  1. 1. 线程安全
  2. 2. 三要素
  3. 3. 原子性
  4. 4. 可见性
  5. 5. 有序性
|