Java线程安全 - 原子性

作者: 右耳菌 | 来源:发表于2022-06-16 21:57 被阅读0次

先看一个例子

  • Counter
package cas;

public class Counter {
    volatile int i = 0;

    public void add() {
        i++; 
    }
}
  • Demo1_CounterTest
package cas;

public class Demo1_CounterTest {

    public static void main(String[] args) throws InterruptedException {
        final Counter ct = new Counter();

        for (int i = 0; i < 6; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        ct.add();
                    }
                    System.out.println("done...");
                }
            }).start();

        }

        Thread.sleep(4000L);
        System.out.println(ct.i); //输出的结果是??
    }
}

以上的代码的执行结果,按照正常来说,i的值最后应该是 60000,但是实际的情况如下所示:

done...
done...
done...
done...
done...
done...
26643 // 这是一个随机的数字,每次的执行的结果可能不一致

对于以上的问题,我们可以对Counter进行反编译操作

# 编译
javac Counter.java
# 反编译
javap -v -p Counter.class

在出现的结果中,查看add方法的部分内容

  public void add();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #7                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #7                  // Field i:I
        10: return
      LineNumberTable:
        line 7: 0
        line 8: 10

可见i++ 这个代码并不是原子性的,在多个线程并发对 i 进行++操作的时候,会因为读取的和修改的操作非原子性的问题导致以上例子中的问题的发生。


解决方式1:加上Lock锁

  • CounterLock
package cas;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class CounterLock {
    volatile int i = 0;

    Lock lock = new ReentrantLock();

    public void add() {
        lock.lock();
        i++;
        lock.unlock();
    }
}

加锁的方式虽然可以解决问题,但是会带来性能上的消耗。


扩展:原子操作

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

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


CAS(Compare and swap)

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

CAS操作需要输入两个数值,一个旧值A(期望操作前的值〉和一个新值B,在操作期间先对旧值进行比较,若没有发生变化,才交换成新值,发生了变化则不交换。

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

Compare and swap

解决方式2:CAS方式的之Unsafe

  • CounterUnsafe
package cas;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class CounterUnsafe {
    volatile int i = 0; //cas 硬件  

    private static Unsafe unsafe = null;

    private static long valueOffSet;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);

            Field field1 = CounterUnsafe.class.getDeclaredField("i");
            valueOffSet = unsafe.objectFieldOffset(field1);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }


    public void add() {
        for (; ; ) { // 失败后重试
            int current = unsafe.getIntVolatile(this, valueOffSet); //性能 : 分析 原子性问题 阻塞  自旋 分别损失的是谁? cpu 阻塞 内存
            //系统 内存 cpu100% ---损耗内存
            if (unsafe.compareAndSwapInt(this, valueOffSet, current, current + 1)) { //要么成功,要么失败
                break;
            }
        }
    }
}

J.U.C 包内的原子操作封装类

  • AtomicBoolean:原子更新布尔类型
  • Atomiclnteger:原子更新整型
  • AtomicLong:原子更新长整型
package cas;

import java.util.concurrent.atomic.AtomicInteger;

public class CounterAtomic {
    //volatile int i = 0;

    AtomicInteger i = new AtomicInteger(0); //封装了CAS机制.

    public void add() {
        i.incrementAndGet();
    }
}
  • AtomicIntegerArray:原子更新整型数组里的元素。
  • AtomicLongArray:原子更新长整型数组里的元素。
  • AtomicReferenceArray:原子更新引用类型数组里的元素。
package cas.atomic;

import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.concurrent.atomic.AtomicReference;

public class AtomicTest {
    public static void main(String args[]) {
        AtomicIntegerArray array = new AtomicIntegerArray(3);
        array.set(1, 14);
        array.compareAndSet(1, 14, 13);

        AtomicReference<Thread> th = new AtomicReference<>();
        th.get();
        th.compareAndSet(null, Thread.currentThread());
    }
}

注:对于需要更新的字段,需要更新的一定要用volatile来修饰

package cas.atomic;

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class Demo2_AtomicIntegerFieldUpdater {
    // 新建AtomicIntegerFieldUpdater对象,需要指明是哪个类中的哪个字段


    private static AtomicIntegerFieldUpdater<User> atom =
            AtomicIntegerFieldUpdater.newUpdater(User.class, "id");

    private static AtomicIntegerFieldUpdater<User> atom1 =
            AtomicIntegerFieldUpdater.newUpdater(User.class, "age");


    public static void main(String[] args) {

        User user = new User(100, 100, "Kane");

        atom.addAndGet(user, 50);
        atom1.addAndGet(user,18);
        System.out.println("addAndGet(user, 50)             调用后值变为:" + user);

    }
}

class User {
    volatile int id; //需要更新的一定要用volatile来修饰
    volatile int age;

    private String name;

    public User(int id, int age, String name) {
        this.id = id;
        this.age = age;
        this.name = name;
    }

    public String toString() {
        return "id:" + id + " " + "age:" + age;
    }
}
  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。

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

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

  • AtomicReference:原子更新引用类型。

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

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


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

  • 更新器:DoubleAccumulator、LongAccumulator

  • 计数器:DoubleAdder、LongAdder

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

  • 场景:高并发频繁更新、不太频繁读地读取

package cas.atomic;

import java.util.concurrent.atomic.LongAdder;

public class Demo3_LongAdder {
    public static void main(String[] args) throws InterruptedException {
        LongAdder longAdder = new LongAdder();

        for (int i = 0; i <6 ; i++) {
            new Thread(()->{
                long startTime = System.currentTimeMillis();
                while (System.currentTimeMillis()-startTime<2000){
                    longAdder.increment();
                }
            }).start();
        }
        Thread.sleep(3000L);
        System.out.println(longAdder.sum());
    }
}

CAS的三个问题

  • 循环+CAS,自旋(循环尝试)的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长
    时间不成功,会带来很大的CPU资源消耗。

  • 仅针对单个变量的操作,不能用于多个变量来实现原子操作。

  • ABA问题。 (链表或是栈的例子,next指向的修改,多线程的情况下可能会导致问题的出现)

ABA问题
  • 栈的例子
栈的例子
  • 男女朋友的例子
  1. 高富帅和穷屌丝同时追求一个女生
  2. 高富帅先追到手了,然后两个人在一起了很短的时间(emmmm,请自行想象)
  3. 高富帅抛弃了女生
  4. 穷屌丝最后跟女生在一起了,且没有发现女生有跟高富帅已经在一起过了很短的时间的这件事
  • 解决ABA问题的方式:加上版本号
    即更新的时候,不光对比值,还对比版本号。
package cas.aba;

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

public class ConcurrentStack {
    // top cas无锁修改
    //AtomicReference<Node> top = new AtomicReference<Node>();
    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;
        }
        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作为值返回
    }
}


线程安全的概念

  • 竞态条件:如果程序运行顺序的改变会影响最终结果,就说存在竞态条件。
    大多数竞态条件的本质,就是基于某种可能失效的观察结果来做出判断或执行某个计算

  • 临界区:存在静态条件的代码区域叫临界区。


总结

  • i+这个操作不是原子操作
  • 原子操作的概念
  • CAS机制的概念,利用CAS实现原子性的数字变更
  • AtomicInteger等类底层就是利用CAS机制实现
  • JDK提出了高并发场景性能更好的累加计数器
  • 带有版本号的数字引用类型,可以实现版本号锁
  • 线程安全相关的概念

如果觉得有收获就点个赞吧,更多知识,请点击关注查看我的主页信息哦~

相关文章

  • Java-可见性、原子性、有序性

    关键字:Java内存模型(JMM)、线程安全、可见性、原子性、有序性 1.线程安全(JMM) 多线程执行某个操作的...

  • java多线程(壹)——线程与锁

    线程与线程安全问题 所有的线程安全问题都可以概况为三个点:原子性,可见性、有序性——————by Java多线程编...

  • Java线程安全-原子性

    什么是原子性 如果把一个事务可看作是一个程序,它要么完整的被执行,要么完全不执行。这种特性就叫原子性 问题1 以上...

  • Java线程安全 - 原子性

    先看一个例子 Counter Demo1_CounterTest 以上的代码的执行结果,按照正常来说,i的值最后应...

  • 内存模型

    1.4 Java的 内存模型 是否线程安全线程安全与cpu资源的抢夺多线程在读写共享变量时引发的问题 线程的原子性...

  • Java多线程

    在java中,多线程一直是代表着高级 线程安全性 线程安全需要考虑操作的自己状态的原子性,竟态条件,复合操作 保证...

  • ios 原子性和非原子性

    原子性和非原子行相对于线程的安全来讲 nonatomic:非原子属性,线程不安全的,效率高 atomic:原子属性...

  • iOS中属性关键词

    1.原子性与非原子性 atomic:原子性,只有一个线程可以同时访问实例。atomic 是线程安全的(因为会为se...

  • Java多线程目录

    Java多线程目录 Java多线程1 线程基础Java多线程2 多个线程之间共享数据Java多线程3 原子性操作类...

  • 线程基本概念

    1、基本概念 在学习Java并发之前我们需要先理解一些基本的概念:共享、可变、线程安全性、线程同步、原子性、可见性...

网友评论

    本文标题:Java线程安全 - 原子性

    本文链接:https://www.haomeiwen.com/subject/isexvrtx.html