线程安全性

1、定义

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称为这个类是线程安全的。
原子性:提供了互斥访问,同一时刻只能有一个线程来对他进行操作
可见性:一个线程对主内存的修改可以及时的被其他线程观察到
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无章

2、原子性

线程不安全例子

如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。
以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值为 997 而不是 1000。

public class ThreadUnsafeExample {
    private int cnt = 0;
    public void add() {
        cnt++;
    }
    public int get() {
        return cnt;
    }
}

public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    ThreadUnsafeExample example = new ThreadUnsafeExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}


997

线程安全例子

使用 AtomicInteger 重写之前线程不安全的代码之后得到以下线程安全实现:

public class AtomicExample {
    private AtomicInteger cnt = new AtomicInteger();

    public void add() {
        cnt.incrementAndGet();
    }

    public int get() {
        return cnt.get();
    }
}

public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    AtomicExample example = new AtomicExample(); // 只修改这条语句
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}

1000

除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的原子性。它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。

public class AtomicSynchronizedExample {
    private int cnt = 0;

    public synchronized void add() {
        cnt++;
    }

    public synchronized int get() {
        return cnt;
    }
}

public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    AtomicSynchronizedExample example = new AtomicSynchronizedExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}
1000

Atomic包

atomic包提高原子更新基本类型的工具类,主要有这些:
* AtomicBoolean:以原子更新的方式更新boolean;
* AtomicInteger:以原子更新的方式更新Integer;
* AtomicLong:以原子更新的方式更新Long;

JDK提供了AtomicInteger保证对数字的操作是线程安全的,属于乐观锁,它不加锁去完成某项操作,如果因为冲突失败就重试,直到成功为止。
Unsafe 是做一些Java语言不允许但是又十分有用的事情,具体的实现都是native方法,AtomicInteger里调用的 Unsafe 方法 基于的是CPU 的 CAS指令来实现的。所以基于 CAS 的操作可认为是无阻塞的,一个线程的失败或挂起不会引起其它线程也失败或挂起。并且由于 CAS 操作是 CPU 原语,所以性能比较好。

public native int getIntVolatile(Object var1, long var2);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

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;
    }

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。 从代码上我们可以看到do while语句,从而证实当更新出现冲突时,即失败时,它还会尝试更新。符合乐观锁的思想。

CAS算法

CAS(Compare And Swap 比较并交换)指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。
这个指令的操作过程:
* 首先,CPU 会将内存中将要被更改的数据与期望的值做比较。
* 然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。
* 最后,CPU 会将旧的数值返回。


这一系列的操作是原子的。它们虽然看似复杂,但却是 Java 5 并发机制优于原有锁机制的根本。简单来说,CAS 的含义是:我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少。 简单的来说,CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则返回 V。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而 Synchronized 是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。

LongAdder、AtomicLong区别

AtomicLong原理
就像我们所知道的那样,AtomicLong的原理是依靠底层的cas来保障原子性的更新数据,在要添加或者减少的时候,会使用死循环不断地cas到特定的值,从而达到更新数据的目的。那么LongAdder又是使用到了什么原理?难道有比cas更加快速的方式?

LongAdder原理
LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。 
缺点
LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。


3、可见性

可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。

导致共享变量在线程间不可见的原因

  • 线程交叉执行
  • 重排序结合线程交叉执行
  • 共享变量更新后的值没有在工作内存与主存间及时更新

三种实现可见性的方式

synchronized
JMM关于synchronized的两条规定:
* 线程解锁前,必须把共享变量的最新值刷新到主内存
* 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意加锁和解锁是同一把锁)

volatile
通过加入内存屏障和禁止重排序优化来实现
* 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存
* 对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存读取共享变量

注意:volatile是不具有原子性的,所以在多线程操作count++的时候,最后的结果并不是确定的

public static volatile int count = 0;

private static void add() {
    count++;
}

结果不确定

场景1:volatile适合表示布尔值,他的使用场景应该是用来做标记位的时候,循环inited来判断context是不是加载完成。

volatile boolean flag = false;

//线程1:
context = loadContext();
flag = true;

//线程2
while(!flag){
	sleep();
}

场景2:volatile可以用来配合双重检测,因为它可以禁止指令重排序

@ThreadSafe
public class SingletonExample5 {

    // 私有构造函数
    private SingletonExample5() {

    }

    // 1、memory = allocate() 分配对象的内存空间
    // 2、ctorInstance() 初始化对象
    // 3、instance = memory 设置instance指向刚分配的内存

    // 单例对象 volatile + 双重检测机制 -> 禁止指令重排
    private volatile static SingletonExample5 instance = null;

    // 静态的工厂方法
    public static SingletonExample5 getInstance() {
        if (instance == null) { // 双重检测机制        // B
            synchronized (SingletonExample5.class) { // 同步锁
                if (instance == null) {
                    instance = new SingletonExample5(); // A - 3
                }
            }
        }
        return instance;
    }
}

final
被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。

4、有序性

有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。
在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。

先行发生原则(happens-before)

Happens-before 是用来指定两个操作之间的执行顺序。提供跨线程的内存可见性。在 Java 内存模型中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必然存在 happens-before 关系。
上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外,JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。

主要有以下这些原则:
* 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
* 锁定规则:一个unLock操作先行发生于后面对同一个所的lock操作,无论在单线程还是多线程中,同一个锁如果被锁定,那么要先释放这个锁
* volatile变量规则:对一个变量的写操作先行发生与后面怼这个变量的读操作。
* 传递规则:如果操作A先于B,B先于C,那么操作A先于C
* 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
* 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
* 线程总结规则:线程中所有的操作都先发生于线程的终止检测,我们可以通过Thread.join()方法结束,Thread.ISalive()的返回值手段检测到线程已经终止执行。
* 对象终结规则:一个对象的初始化完成先发生于他的finalize()方法的开始。

转载声明:写作不易,商业转载请联系作者获得授权,非商业转载请注明出处,并附上原文链接,感谢!