线程安全概念

竞态条件与临界区

public class Demo{
    public int i=0;
    public void incr(){
        i++
    }
}

多个线程访问了相同的资源,向这些资源做了写操作时,对执行顺序有要求

临界区:incr方法內部就是临界区域,关键部分代码的多线程并发执行,会对执行结果产生影响。

竞态条件:可能发生在临界区域内的特殊条件。多线程执行incr方法中的i++关键代码时,就产生了竞态条件。

一些情况是线程安全的

资源不共享

如果一段代码是线程安全的,则它不包含竞态条件。只有当多个线程更新共享资源时,才会发生竞态条件。

栈封闭时,不会在线程之间共享的变量,都是线程安全的。

局部对象引用本身不共享,但是引用的对象存储在共享堆中。如果方法內创建的对象,只是在方法中传递,并且不对其他线程可用,那么也是线程安全的。

//例子
public void someMethod(){
    LocalObject localObject = new LocalObject();
    localObject.callMethod();
    method2(localObject);
}
public void method2(LocalObject localObject{
	localObject.setValue("value");
}

判定规则:如果创建、使用和处理资源,永远不会逃脱单个线程的控制,该资源的使用是线程安全的。

对象不可变

创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。

实例被创建,value变量就不能再被修改,这就是不可变性。

//比如没有提供set方法的私有属性
public class Demo{
    private int value =0;
    public Demo(int value){
        this.value= value
    }
    public int getValue(){
        return this.value;
    }
}

原子操作

原子操作定义

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。

将整个操作视作一个整体是原子性的核心特征。

最开始例子里面的i++就不是一个原子操作,她包含三步骤:

  1. 加载i
  2. 计算+1
  3. 赋值i

存在竞态条件的情况下,线程不安全,需要转变为原子操作才能安全。

方式一: 循环CAS

CAS机制

Compare and swap比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证。

CAS操作需要输入两个数值,一个旧值A(期望操作前的值)和一个新值B,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。(类似乐观锁)

JAVA中的 sun.misc.Unsafe类,提供了 compareAndSwaplnt()compareAndSwapLong()等几个方法实现CAS

package com.study.lock.lock;

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class LockDemo1 {
    volatile int value = 0;

    static Unsafe unsafe; // 直接操作内存,修改对象,数组内存....强大的API
    private static long valueOffset;

    static {
        try {
            // 反射技术获取unsafe值
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);

            // 获取到 value 属性偏移量(用于定位value属性在内存中的具体地址)
            valueOffset = unsafe.objectFieldOffset(LockDemo1.class
                    .getDeclaredField("value"));

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public void add() {
        // TODO xx00
        // i++;// JAVA 层面三个步骤
        // CAS + 循环 重试
        int current;
        do {
            // 操作耗时的话, 那么 线程就会占用大量的CPU执行时间
            current = unsafe.getIntVolatile(this, valueOffset);
        } while (!unsafe.compareAndSwapInt(this, valueOffset, current, current + 1));
        // 因为跨平台的问题,而我们是用反射的非正常方式获得unsafe,所以可能会失败
    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo1 ld = new LockDemo1();

        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    ld.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(ld.value);
    }
}

其实AtomicInteger这些封装类的底层就是这么写的

java.util.concurrent包内的原子操作封装类
  • AtomicBoolean:原子更新布尔类型

  • AtomicInteger:原子更新整型

  • AtomicLong:原子更新长整型

  • AtomicIntegerArray:原子更新整型数组里的元素

  • AtomicLongArray:原子更新长整型数组里的元素

  • AtomicReferenceArray:原子更新引用类型数组里的元素

  • AtomiclntegerFieldUpdater:原子更新整型的字段的更新器

  • AtomicLongFieldUpdater:原子更新长整型字段的更新器

  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段

  • AtomicReference:原子更新引用类型

  • AtomicStampedReference:原子更新带有版本号的引用类型

  • AtomicMarkableReference:原子更新带有标记位的引用类型。

    jdk1.8更新

  • 更新器: DoubleAccumulator、 LongAccumulator

  • 计数器: DoubleAdder、 LongAdder(一个使用场景是统计接口调用次数时可使用)

计数器增强版,高并发下性能更好

频繁更新但不太频繁读取的汇总统计信息时使用

分成多个操作单元,不同线程更新不同的单元只有需要汇总的时候才计算所有单元的操作

即分成多个值(Cell)减少竞争,对外暴露一个虚拟值,最后取得时候使用sum()把多个内存块的Cell相加

image-20200825135731552
三个缺点:
  1. 循环+CAS,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的cPU资源消耗。
  2. 仅针对单个变量的操作,不能用于多个变量来实现原子操作。即很长的代码没法用CAS
  3. ABA问题。(详情查看:https://www.sunjianbo.com/cas-aba/)。

方式二: 锁

锁能保证可见性,是基于Happens-before原则

一个线程的unlock一定执行在另一个线程的lock前面,也就是说另一个线程不会插进该线程的执行过程(lock->op->unlock)中