线程安全之原子操作

可见性问题:让一个线程对共享变量的修改,能够及时的被其他线程看到。

  • 根据 JMM 中规定的happen before 和 同步原则:对某个volatile字段的写操作 happens-before 每个后续对该volatile 字段的读操作。对volatile变量 v 的写入,与所有其他线程后续对 v 的读同步。

要满足这些条件,所以volatile关键字就有这些功能:

  • 禁止缓存
  • 对volatile变量相关的指令不做重排序

1.线程安全概念

1.1竞态条件与临界区

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

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

  • 临界区:incr方法内部就是临界区域,关键的部分代码由多线程并发执行,会对执行结果产生影响。
  • 竞态条件:可能发生在临界区域内的特殊条件。多线程执行incr方法时的i++关键代码,产生了竞态条件。

1.2共享资源

  • 如果一段代码是线程安全的,则它不包含竞态条件。只有当多个线程更新共享资源时,才会发生竞态条件。
  • 栈封闭时,不会在线程之间共享的变量,都是线程安全的。
  • 局部变量引用本身不共享,但是引用的对象存储在共享堆中。如果方法内创建的对象,只是在方法中传递,并且不会对其他线程可用,那么也是线程安全的。
  • 判定规则:如果创建、使用和处理资源,永远不会逃脱单个线程的控制,该资源的使用是线程安全的。

1.3不可变对象

public class Demo{
    private int value = 0;
    public Demo(int value){
        this.value = value;
    }
    public int getValue(){
        return this.value;
    }
}
  • 创建不可变的共享对象来保证对象在线程间共享时不被修改,从而实现线程安全。
  • 实例被创建,value变量就不能再被修改,这就是不可变性。

2.原子操作定义

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

  • 将整个操作视为一个整体,资源在该次操作中保持一致,这是原子性的核心特征。

  • public class Demo{
        public int i = 0;
        public void incr(){
            i++;
        }
    }
    
  • 上述代码中,i++操作分为三步骤执行,加载i、计算+1、赋值i。

3. CAS机制

3.1 CAS概念

  • 比较和交换(Compare and swap)。属于硬件同步原语,处理器提供了基本内存操作的原子性保证。
  • CAS操作需要输入两个数值,一个旧值A(期望操作前的值)和一个新值B,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
  • JAVA中sun.misc.Unsafe类,提供了compareAndSwapInt()和compareAndSwapLong()等几个方法实现CAS。
  • 多线程并发调用CAS机制时,同一时间内只有一个线程能操作成功。这是操作系统硬件级别为我们提供的并发安全保证。

3.2 CAS的三个问题

  • 循环+CAS,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态,如果操作长时间不成功,会带来很大的CPU资源消耗。
  • 仅针对单个变量的操作,不能用于多个变量来实现原子操作。
  • ABA问题(无法体现出数据的变动)

CAS的ABA问题

ABA问题的解决思路是这样的:

在这里插入图片描述

4.一种不能保证原子性的代码写法

/**
 * 原子性, 两个线程,对i变量进行递增操作
 *
 * @author suvue
 * @date 2020/2/10
 */
public class LockDemo {
    volatile int i = 0;

    public void add() {
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo demo = new LockDemo();
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        demo.add();
                    }
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(demo.i);
    }
}
  • 启动两个线程,分别对变量 i 进行自增1000次,输出结果,我们期望的结果应该为2000,但是正在的结果差强人意。可以复制代码运行。

5. Java中保证原子性的几种方式

5.1 Unsafe方式(不用加锁)

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * 原子性, 两个线程,对i变量进行递增操作
 *
 * @author suvue
 * @date 2020/2/10
 */
public class LockDemo {
    volatile int i = 0;
    /**
     * 直接操作内存,修改对象,数组内存....强大的API
     */
    private static Unsafe unsafe;

    /**
     * 属性的偏移量
     */
    private static Long iOffset;

    static {
        try {
            // 反射技术获取unsafe值
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            //theUnsafe是静态成员变量,null就可以拿到
            unsafe = (Unsafe) field.get(null);
            //利用unsafe,通过属性的偏移量(定位到内存中对象内具体属性的内存地址)
            iOffset = unsafe.objectFieldOffset(LockDemo.class.getDeclaredField("i"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void add() {
        // i++;// JAVA 层面三个步骤
        // CAS + 循环 重试
        int current;
        do {
            //CAS优化
            current = unsafe.getIntVolatile(this, iOffset);
        } while (!unsafe.compareAndSwapInt(this, iOffset, current, current + 1));
        //可能会失败
    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo demo = new LockDemo();
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        demo.add();
                    }
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(demo.i);
    }
  • 具体的步骤注释写在代码里了,可以对照看看。

5.2 JUC包下的AtomicInteger等类

下面贴出关键代码

    private AtomicInteger i= new AtomicInteger(0);
    public void add() {
        i.incrementAndGet();
    }

5.3 原子性实现方式小结

  • 我们发现,同样是实现原子性,JUC包下的类使用起来很简便。
  • 我们看下它是怎样实现的吧!
/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
//...
  • 上述是AtomicInteger的JDK源码,我们发现它居然也是用unsafe来实现的。与我们的第一种实现不谋而合!

6. JUC包内的原子操作封装类

类名类描述
AtomicBoolean原子更新布尔类型
AtomicInteger原子更新整型
AtomicLong原子更新长整型
AtomicIntegerArray原子更新整型数组里的元素
AtomicLongArray原子更新长整型数组里的元素
AtomicReferenceArray原子更新引用类型数组里的元素
AtomicIntegerFieldUpdater原子更新整型字段的更新器
AtomicLongFieldUpdater原子更新长整型字段的更新器
AtomicReferenceFieldUpdater原子更新引用类型里的字段
AtomicReference原子更新引用类型
AtomicStampedReference原子更新带有版本号的引用类型
AtomicMarkableReference原子更新带有标记位的引用类型
  • Atomic类虽然可以保证原子性,但是在高并发下,非常损耗性能。因为同一时间只有一个线程能够修改成功,其它线程都处于循环尝试中,线程多的时候,对性能损耗非常大。
  • 因此 jdk 1.8更新了新的原子操作封装类。
1.8更新
更新器:DoubleAccmulator、LongAccumulator
计数器:DoubleAdder、LongAdder
计数器增强版,高并发下性能更好
频繁更新但不太频繁读取的汇总统计信息时使用
分成多个操作单元,不同线程更新不同的单元
只有需要汇总的时候才计算所有单元的操作

在这里插入图片描述

  • 每个线程去执行自增时,操作的是不同的变量,因此即使是高并发,也不存在线程争抢CPU的问题。
  • 当获取计数器的值,会对所有的变量进行sum求和。
  • 计算的时候很快,取结果的时候很慢,因为要进行累加操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值