Java内存模型(JMM)详解-原子性

Java内存模型(JMM)详解-原子性

    • 什么是原子性
    • 举例说明非原子操作——深入探讨 i++ 操作
    • i++操作的具体内存实现过程分析
    • 原子操作
    • 如何解决原子性问题
    • CAS (Compare and swap)
    • 如何使用CAS——手写原子实现
    • J.U.C包内的原子操作封装类(简单介绍不做详解)
    • CAS的三个问题
    • 总结
      • 什么是线程安全
      • 共享资源

什么是原子性

即是一个操作不能被打断,要么执行完要么不执行,类似事务操作,Java 基本类型数据的访问大都是原子操作,顺便详细补充上一篇所提到的long和double的问题——Java内存模型(JMM)详解-可见性,而long 和 double 类型是 64 位,在 32 位 JVM 中会将 64 位数据的读写操作分成两次 32 位来处理,所以 long 和 double 在 32 位 JVM 中是非原子操作,也就是说在并发访问时是线程非安全的,要想保证原子性就得对访问该数据的地方进行同步操作,譬如 synchronized 等。

举例说明非原子操作——深入探讨 i++ 操作

废话不多说,直接上代码

/**
 * @author 潇兮
 * @date 2019/9/16 23:08
 **/
public class Demo1 {
    public static void main(String[] args) throws  InterruptedException{
     final  Counter ct=new Counter();

     for (int i=0;i<10;i++){
         new  Thread(new Runnable() {
             public void run() {
                 for (int j=0;j<10000;j++){
                     ct.add();
                 }
                 System.out.println("执行完毕");
             }
         }).start();
     }

     Thread.sleep(3000L);
     System.out.println(ct.i);//输出的值不一定每次是100000
    }
}

/**
 * @author 潇兮
 * @date 2019/9/16 23:12
 **/
public class Counter{
    volatile int i=0;

    public void  add(){
        i++;
    }
}

执行以上代码,输出的结果不一定每次是100000,这是因为 i++ 操作不是原子性操作。让我们到target目录的.class目录下执行命令javap -v -p Counter.class反编译一下代码可以看到如下字节码指令,可以看出其中 i ++ 是由多个字节码操作完成的,而且顺序不能被打乱或者切割而只执行一部分操作(不可中断性,此处多线程情况下被切割了),所以 i++ 操作不是原子性的。
Java内存模型(JMM)详解-原子性_第1张图片

i++操作的具体内存实现过程分析

Java内存模型(JMM)详解-原子性_第2张图片
上图实例为单线程时内存实现的过程,当有多个线程时, 假设有两个线程,都做了 i++ 操作,那么都会执行getfiled拿到堆内存中的值 0,继续后续的运算,当执行到 iadd 操作时线程T1此时的栈内值都为1,putfileld操作将 1 放入堆内存中,因为 T2线程 getfiled 的值为0, 那么 T2线程的 iadd 操作为 1,putfileld操作将 1 放入堆内存中,最终导致的结果就是两个线程执行完毕的结果为 1 而不是2。换句换说,就是当T1线程中将堆内存的值改为1时,由于T2线程的加载的初始值在操作数詹中仍然为0,和堆内存中的值不一致,出现数据不一致问题,相当于是一个失效的值了,但是T2线程不知道此时为失效的值,导致做了两次 +1 操作只加了一次。而我们的demo中有10个线程,有可能初始 getfiled 获取的值是0 或者 1,所以最终的结果不一定每次都是100000。具体过程如下图
Java内存模型(JMM)详解-原子性_第3张图片

原子操作

原子操作可以是一个或者多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只能执行其中的一部分(简称不可中断性)。
将整个操作视作为一个整体,资源在该次操作内保持一致,是原子性的核心特征

如何解决原子性问题

还是拿我们上面的例子做为参考,要实现原子性:

  1. 一个方式就是将add方法中添加关键字 synchronized,由JVM提供。synchronized语义一方面是加锁(同步锁 | 互斥锁),当我们访问 i++ 操作时,只有当T1线程访问完毕后T2线程才能继续访问,如此类推挨个执行,上述例子得到的结果每次都是为100000;
  2. 另一个方式为lock,也是典型的互斥锁,由JDK提供。

以上两种方式的缺陷就是并发度的问题,从始至终只有一个线程在操作。下面会继续讲解如何解决并发度问题

方式一
Java内存模型(JMM)详解-原子性_第4张图片
方式二
Java内存模型(JMM)详解-原子性_第5张图片

CAS (Compare and swap)

Compare and swap 比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证。
CAS 操作需要输入两个数值,一个旧值A (期望操作前的值) 和一个新值B,在操作期间现对旧值进行比较,若没有发生变化,才焦焕成新值,发生了变化则不交换。
JAVA 中的sun.misc.Unsafe类,提供了compareAndSwapInt() 和 compareAndSwapLong() 等几个方法实现CAS
Java内存模型(JMM)详解-原子性_第6张图片
看了上面的CAS是不是对于我们demo有了别的想法,不错,我们可以采用CAS操作(JVM可以调用操作系统指令, 并且给我们暴露出接口Unsafe工具类,从而间接调用CAS指令,后文会演示如何使用并阐述CAS的可能出现的ABA问题)来实现原子性,当线程T1将1的值 采用CAS(0,1)putfiled进堆内存时,假设T2线程的初始加载值为 0 那么此时T2的CAS新旧值为CAS(0,1),我们将T2的旧值0与堆内存中的1对比,发现不相同,就不会将1写入,写入失败后自旋操作,从新加载初始值getfield为1,后续CAS则为(1,2),这样,最终实现了操作的原子性,结果为100000。
Java内存模型(JMM)详解-原子性_第7张图片

如何使用CAS——手写原子实现

不需要集齐七龙珠,不需要收取尾兽力,只需要大喊一声: 码来!!!!

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * @author 潇兮
 * @date 2019/9/17 1:20
 **/
public class CounterUnSafe {
    volatile int i =0;

    private  static Unsafe unsafe=null;

    private static long valueOffset;//定义偏移量,因为要操作内存,我们需要告诉unsafe我们的对象引用和他需要修改的对应属性字段的偏移量
    static {
        //如何直接使用new的话运行会直接报安全错误,
        // 虽然提供了这些方法就是不允许使用,JDK经常只许州官放火,不许百姓点灯啦,大喊一声剑来可解锁意念大嘴巴子抽JDK作者
       // unsafe=Unsafe.getUnsafe(); //错误使用

        /**
         * 正确解锁方式——反射
         */
        try {
            Field field=Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe=(Unsafe) field.get(null);

            //获取i的偏移量
            Field field1=CounterUnSafe.class.getDeclaredField("i");
            valueOffset=unsafe.objectFieldOffset(field1);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public void add(){
        //因为CAS可能会失败,需要自旋
        for (;;){
            //拿旧值
            int current = unsafe.getIntVolatile(this,valueOffset);
            //通过CAS操作来修改i的值 unsafe.compareAndSwapInt(this,valueOffset,current,current+1);返回boolean 判断是否成功
            if (unsafe.compareAndSwapInt(this,valueOffset,current,current+1)){
                break;
            }
        }

    }

    public static  void main(String[] args){
       //在demo1中 final  Counter ct=new Counter(); 
       //改为我们手写原子性操作类 final  CounterUnSafe ct=new CounterUnSafe();运行,不会翻车的
    }
}

这前面讲解 i++ 操作是为了加深理解原子性操作的原理和后文中各种原子性工具类的底层实现原理。

J.U.C包内的原子操作封装类(简单介绍不做详解)

AtomicBoolean : 原子更新布尔类型
AtomicInteger: 原子更新整型
AtomicLong:原子更新长整型

AtomicIntegerArray: 原子更新整形数组里的元素
AtomicLongArray: 原子更新长整型数组里的元素
AtomicReferenceArray: 原子更新引用类型数组里的元素

AtomicIntegerFieldUpdater: 原子更新整型的字段更新器
AtomicLongFieldUpdater: 原子更新长整型字段的更新器
AtomicLReferenceFieldUpdater: 原子更新引用类型的字段

AtomicReference: 原子更新引用类型
AtomicStampedReference: 原子更新带有版本号的引用类型
AtomicMarkableReference: 原子更新带有标记位的引用类型

1.8JDK更新
计算器增强版,高并发下性能更好
更新器: DoubleAccumulator、LongAccumulator
计数器: DoubleAdder、LongAdder
原理 :分成多个操作单元,不同线程更新不同的单元,只有需要汇总的时候才计算所有单元的操作
场景:高并发下频繁更新、不太频繁地读取

CAS的三个问题

1. 循环+CAS,自旋的实现让所有的线程都处于高频运行,争抢CPU执行时间的状态。如果长时间操作不成功,会带来很大的CPU资源的消耗;
2. 仅针对单个变量的操作,不能用于多个变量来实现原子性;
3. ABA问题

我们重点理解ABA问题以及解决方式:
Java内存模型(JMM)详解-原子性_第8张图片

如上图所示(ABA) 问题:

  1. T1、T2同时读取到 i=0 后 ;
  2. T1、 T2都要执行CAS(0,1)操作;
  3. 假设T2操作比T1后,则T1执行成功,T2预期失败;
  4. 但是假如T1的CAS(0,1)执行后紧接着执行CAS(1,0),则将 i 的值改为了0,但是本质是不一样,狸猫换太子,即使T执行成功(可以理解为版本变更)。

此处代码可以理解将ABA理解成出入栈的问题(作理解使用,以下说的栈都是类比说法,不同于实际栈)

package aba;
/**
 * @author 潇兮
 * @date 2019/9/17 03:07
 **/
// 存储在栈里面元素 -- 对象
public class Node {
    public final String value;
    public Node next;

    public Node(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "value=" + value;
    }
}
package aba;

import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;

/**
 * @author 潇兮
 * @date 2019/9/17 03:07
 * 此类没有比对版本标记,会出现ABA问题
 **/

// 实现一个 栈(后进先出)
public class Stack {
    // top cas无锁修改
    AtomicReference<Node> top = new AtomicReference<Node>();

    public void push(Node node) { // 入栈
        Node oldTop;
        do {
            oldTop = top.get();
            node.next = oldTop;
        }
        while (!top.compareAndSet(oldTop, node)); // CAS 替换栈顶
    }


    // 出栈 -- 取出栈顶 ,为了演示ABA效果, 增加一个CAS操作的延时
    public Node pop(int time) {

        Node newTop;
        Node oldTop;
        do {
            oldTop = top.get();
            if (oldTop == null) {   //如果没有值,就返回null
                return null;
            }
            newTop = oldTop.next;
            if (time != 0) {    //模拟延时
                LockSupport.parkNanos(1000 * 1000 * time); // 休眠指定的时间
            }
        }
        while (!top.compareAndSet(oldTop, newTop));     //将下一个节点设置为top
        return oldTop;      //将旧的Top作为值返回
    }
}
package aba;

import java.util.concurrent.atomic.AtomicStampedReference;
import java.util.concurrent.locks.LockSupport;
/**
 * @author 潇兮
 * @date 2019/9/17 03:07
 * 对比了版本 不会出现ABA问题
 **/

public class ConcurrentStack {
    // top cas无锁修改
    //AtomicReference top = new AtomicReference();
    //null 0 初始化了版本
    AtomicStampedReference<Node> top =
            new AtomicStampedReference<>(null, 0);

    public void push(Node node) { // 入栈
        Node oldTop;
        int v;
        do {
            v = top.getStamp();
            oldTop = top.getReference();
            node.next = oldTop;
        }
        //v旧版本 v+1新版本  作比较 
        while (!top.compareAndSet(oldTop, node, v, v+1)); // CAS 替换栈顶
    }


    // 出栈 -- 取出栈顶 ,为了演示ABA效果, 增加一个CAS操作的延时
    public Node pop(int time) {

        Node newTop;
        Node oldTop;
        int v;

        do {
            v = top.getStamp();
            oldTop = top.getReference();
            if (oldTop == null) {   //如果没有值,就返回null
                return null;
            }
            newTop = oldTop.next;
            if (time != 0) {    //模拟延时
                LockSupport.parkNanos(1000 * 1000 * time); // 休眠指定的时间
            }
        }
        while (!top.compareAndSet(oldTop, newTop, v, v+1));     //将下一个节点设置为top
        return oldTop;      //将旧的Top作为值返回
    }
}

package aba;

import java.util.concurrent.locks.LockSupport;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Stack stack = new Stack();
        //ConcurrentStack stack = new ConcurrentStack();

        stack.push(new Node("B"));      //B入栈
        stack.push(new Node("A"));      //A入栈

        Thread thread1 = new Thread(() -> {
            Node node = stack.pop(800);
            System.out.println(Thread.currentThread().getName() +" "+ node.toString());

            System.out.println("done...");
        });
        thread1.start();

        Thread thread2 = new Thread(() -> {
            LockSupport.parkNanos(1000 * 1000 * 300L);

            Node nodeA = stack.pop(0);      //取出A
            System.out.println(Thread.currentThread().getName()  +" "+  nodeA.toString());

            Node nodeB = stack.pop(0);      //取出B,之后B处于游离状态
            System.out.println(Thread.currentThread().getName()  +" "+  nodeB.toString());

            stack.push(new Node("D"));      //D入栈
            stack.push(new Node("C"));      //C入栈
            stack.push(nodeA);                    //A入栈

            System.out.println("done...");
        });
        thread2.start();

        LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);


        System.out.println("开始遍历Stack:");
        Node node = null;
        while ((node = stack.pop(0))!=null){
            System.out.println(node.value);
        }
    }
}

总结

前面Java内存模型(JMM)详解-可见性 和 此篇幅 原子性都是导致线程安全问题的主要原因。

什么是线程安全

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。大多数竞态条件的本质,是基于某种可能失效的观察结果来做出的判断或者执行的某个计算。导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件。

共享资源

只有当多个线程更新共享资源时,才会发生竞态条件,可能会出现线程安全问题。

栈封闭,不会在线程中共享的变量都是线程安全的。
局部对象引用本身不共享,但是引用的对象存储在共享堆中,如方法区内创建的对象,只是在方法中传递,并不会对其他线程可用,那么也是线程安全的。
不可变的共享对象来保证对象在线程中共享时不会被修改,从而实现线程安全。(如不提供set方法只有get方法,正常使用,不用反射去修改是不会产生线程安全问题)
使用ThreadLocal时,相当于不同的线程操作的是不同的资源,所以不存在线程安全问题

你可能感兴趣的:(Java内存模型-JMM)