Java并发编程02-线程安全性

一、线程安全

1. 线程安全

可以简单的理解为:一个方法或者一个实例可以在多线程环境中使用而不会出现问题。

2. 线程不安全的原因

多个线程使用了相同的资源,如同一内存区(变量、数组或对象)、系统(数据库、web服务等)或文件等。更准确的说,是多个线程对同一资源进行了写操作。多个线程只读取相同的资源,是没有线程安全问题的。

3. 如何保证线程安全

保证共享内存的原子性、可见性和有序性。

二、原子性

对共享内存的操作必须是要么全部执行直到执行结束,且中间过程不能被任何外部因素打断,要么就不执行。

1. Java 如何实现原子操作

在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。

使用锁很好理解,下面重点说一下循环 CAS 实现的思路。

(1)Atomic包(使用循环 CAS 实现原子操作)

Jdk1.5 开始提供了以 Atomic 开头的类,例如 AtomicBoolean(用原子方式更新的 boolean 值)、AtomicInteger(用原子的方式更新的 int 值)等。

使用 AtomicInteger 实现的线程安全的计数器程序示例:

public class N18_CAS_AtomicInteger {
    private AtomicInteger atomicI = new AtomicInteger(0);
    private int i = 0;

    private void safeCount() {
        atomicI.incrementAndGet();
    }

    private void count() {
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        N18_CAS_AtomicInteger counter = new N18_CAS_AtomicInteger();
        ArrayList<Thread> ts = new ArrayList<>(600);
        for (int i = 0; i < 100; ++i) {
            Thread t = new Thread(() -> {
                for (int j = 0; j < 10000; ++j) {
                    counter.count();
                    counter.safeCount();
                }
            });
            t.start();
            ts.add(t);
        }

        // 等待所有线程执行完成
        for (Thread t: ts)
            t.join();

        System.out.println(counter.i);
        System.out.println(counter.atomicI);
    }
}

运行结果:

992034
1000000

AtomicInteger 中的 incrementAndGet 方法就是乐观锁的一个实现,使用自旋 CAS(循环检测更新)的方式来更新内存中的值并通过底层CPU执行来保证是更新操作是原子操作。

getAndIncrement() 方法的内部:

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

getAndAddInt() 方法:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

此时可以看到 compareAndSwapInt 方法,就是 CAS 缩写的由来。

其中 var5 是更新后要返回的值。var1 由前面的 this 参数可以看出是 AtomicInteger 实例,var2 是偏移量(AtomicInteger 内部通过改变偏移量记录值)。

compareAndSwapInt(var1, var2, var5, var5 + var4)其实换成compareAndSwapInt(obj, offset, expect, update)比较清楚,意思就是如果 obj 内的 value 和 expect 相等,就证明没有其他线程改变过这个变量,那么就更新它为 update,如果这一步的 CAS 没有成功,那就采用自旋的方式继续进行 CAS 操作。取出乍一看这也是两个步骤了啊,其实在 JNI 里是借助于一个 CPU 指令完成的。所以还是原子操作。

(2)CAS 实现原子操作的问题

  • 1)ABA 问题

    • 因为 CAS 需要在操作值的时候,检查值有没有发生变化,如果没有变化则更新值。但是如果一个值原来是 A,变成了 B,有变成了 A,那么使用 CAS 进行检查的时候会发现它的值没有发生变化,但实际上发生了变化。
    • 解决思路就是使用版本号。Atomic 包中提供了 AtomicStampedReference 来解决 ABA 问题。这个类的 compareAndSet 方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
  • 2)循环时间开销大

    如果 CAS 不成功,则会原地自旋,如果长时间自旋会给 CPU 带来非常大的执行开销。

  • 3)只能保证一个共享变量的原子操作

(3)synchronize、 lock、 Atomic 原子性对比

  • synchronize:不可中断锁,适合竞争不激烈,可读性好

  • lock:可中断锁,多样化同步,竞争激烈时能维持常态

  • Atomic:竞争激烈时能维持常态,比 lock 性能好;只能同步一个值

三、可见性

多线程操作共享内存时,执行结果能够及时的同步到共享内存,确保其他线程对此结果及时可见。

1. 共享变量在线程间不可见的原因

共享变量更新后的值没有在工作内存与主内存间及时更新

2. synchronized

JMM 的规范中提供了 synchronized 具备的可见性:

  • 线程解锁前,必须把共享变量的最新值刷新到主内存
  • 线程加锁时,将清空工作内存中共享变量的值,从主内存中读取最新的值

3. volatile

使用 volatile关键字,保证变量可见性(直接从主内存读,而不是从线程cache读)

注:volatile 变量具有 synchronized 的可见性特性,但是不具备原子性

四、有序性

程序的执行顺序按照代码顺序执行,在单线程环境下,程序的执行都是有序的,但是在多线程环境下,JMM 为了性能优化,编译器和处理器会对指令进行重排,程序的执行会变成无序。

1. volatile/synchronized/lock 可保证有序性

2. happens-before

JMM 通过 happens-before 关系向程序员提供跨线程的内存可见性保证。(如果 A 线程的写操作 a 与 B 线程的读操作 b 之间存在 happens-before 关系,尽管 a 操作和 b 操作在不同的线程中执行,但 JMM 向程序员保证 a 操作将对 b 操作可见)

happens-before 规则:

  • 1)程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
  • 2)监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
  • 3)volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
  • 4)传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
  • 5)start() 规则:如果线程A执行操作 ThreadB.start() (启动线程B),那么 A 线程的 ThreadB.start() 操作 happens-before 于线程 B 中的任意操作。
  • 6)join() 规则:如果线程 A 执行操作 ThreadB.join() 并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join() 操作成功返回。

参考:

posted @ 2020-03-24 03:09  吹不散的流云  阅读(111)  评论(0编辑  收藏  举报