Atomic原子变量类

3.6 原子变量类

1.原子性介绍

目标

1.理解并发编程的原子性的含义

2.实现原子性操作的传统方式

介绍

所谓原子性操作是指在一次的操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不会执行。

场景说明

银行转账来进行举例说明,比如从Alex的账号往Tina的账号转人1000元,这个动作将包含两个最基本的操作:从Alex的账号上扣除1000元;给Tina的账号增加1000元。
这两个操作必须符合原子性的要求,要么都成功要么都失败,总之不能出现Alex的账号扣除了1000元,但是Tina的账号并未增加1000元或者Alex账号未扣除1000元,Tina的账号反倒增加了1000元的情况。

疑问:i++是原子性操作吗?

两个原子性的操作结合在一起未必还是原子性的,比如i++操作具有多个:

​ 第一个操作命令: get i

​ 第二个操作命令:i+1

​ 第三个操作命令:set i;

以上每个操作命令都是原子性操作,但是i++就不是原子性操

比如多个线程执行i++操作, 上面3个操作就会分开执行,就有可能获取不到正确的值。

示例需求

定义共享变量并进行使用多线程进行操作,运行观察效果发现线程不安全

示例代码

import java.util.Vector;
public class IntTest implements Runnable {
//定义共享变量
private static int num =0;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//操作共享变量递增
num++;
System.out.println(num);
}
public static void main(String[] args) throws InterruptedException {
Vector<Thread> vector = new Vector<>();
IntTest task = new IntTest();
//开启多线程进行操作共享变量
for (int i = 0; i <10 ; i++) {
Thread thread = new Thread(task);
vector.add(thread);
thread.start();
}
for (Thread thread : vector) {
thread.join(); //确保所有thread全部运行完成再继续
}
System.out.println("递增结果:"+num);
}
}

运行效果

使用Synchronized解决原子操作安全

import java.util.Vector;
public class IntTest_synchronized implements Runnable {
//定义共享变量
private static int num =0;
@Override
public void run() {
synchronized(this) {
//操作共享变量递增
num++;
System.out.println(num);
}
}
public static void main(String[] args) throws InterruptedException {
Vector<Thread> vector = new Vector<>();
IntTest_synchronized task = new IntTest_synchronized();
//开启多线程进行操作共享变量
for (int i = 0; i <10 ; i++) {
Thread thread = new Thread(task);
vector.add(thread);
thread.start();
}
for (Thread thread : vector) {
thread.join(); //确保所有thread全部运行完成再继续
}
System.out.println("递增结果:"+num);
}
}

总结

1.理解并发编程的原子性的含义

实现线程安全的操作变量数据,原子操作的代码每个线程独占运行,当前线程运行完成后下一个线程才可以运行

2.实现原子性操作的传统方式

使用synchronized,但是性能低下

2.原子更新基本类型类-CAS原理分析

目标

  1. 掌握java.util.concurrent.atomic包下原子更新基本类型类
  2. 理解juc提供的atomic包的原子操作原理CAS

介绍

Java从JDK 1.5开始提供了java.util.concurrent.atomic包(以下简称Atomic包),这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。actomic实现原子性操作采用的是CAS算法保证原子性操作,性能高效。

java.util.concurrent.atomic包的原子类

原子更新基本类型类

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

上面3个类提供方法一模一样,所以我们以AtomicInteger为例进行讲解api方法

api方法

方法 含义
int addAndGet(int delta): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果
boolean compareAndSet(int expect,int update) 如果输入的数值等于预期值,则以原子方
式将该值设置为输入的值
int getAndIncremeont(); 以原子方式将当前值加1,注意,这里返回的是自增前的值
void lazySet(int newValue); 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值
int getAndSet(int newValue); 以原子方式设置为newValue的值,并返回旧值。
int incrementAndGet() 以原子方式将当前值加1,注意,这里返回的是自增后的值

示例代码

import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerTest implements Runnable {
//使用整型并发原子类
private static AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//进行原子性操作+1
System.out.println(atomicInteger.incrementAndGet());
}
public static void main(String[] args) throws InterruptedException {
//定义集合存储线程
Vector<Thread> vector = new Vector<>();
//10线程运行递增操作
for (int i = 0; i <10 ; i++) {
Thread thread = new Thread(new AtomicIntegerTest());
vector.add(thread);
thread.start();
}
//将所有thread线程运行
for (Thread thread : vector) {
thread.join();
}
System.out.println("递增结果:"+atomicInteger.get());
}
}

运行效果

atomicInteger.incrementAndGet()实现原子操作原理CAS(Compare-And- )算法源码分析

public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
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原理分析

1.使用volatile保证内存可见性,在主存中操作数据
2.使用CAS(Compare-And-Swap)操作保证数据原子性
CAS算法是jdk对并发操作共享数据的支持,包含了3个操作数
​ 第一个操作数:内存值value(V)
​ 第二个操作数:预估值expect(E)
​ 第三个操作数:更新值new(N)

含义:当多线程每个线程执行写的操作时,每个线程都会读取主存最新内存值value,并设置预估的值,只有最新内存值与预估值一致的线程,就会将需要更新的值更新到主存中,其他线程就会失败保证原子性操作;这样就解决了synchronized排队导致性能低下的问题;

说明

Atomic包里的类基本都是使用Unsafe实现的,让我们一起看一下Unsafe的源码,我们发现Unsafe只提供了3种CAS方法

/**
* 如果当前数值是expected,则原子的将Java变量更新成x
* @return 如果更新成功则返回true
*/
public final native boolean compareAndSwapObject(
Object o,
long offset,
Object expected,
Object x);
public final native boolean compareAndSwapInt(
Object o,
long offset,
int expected,
int x);
public final native boolean compareAndSwapLong(
Object o,
long offset,
long expected,
long x);

小结

  1. 掌握java.util.concurrent.atomic包下原子更新基本类型类

    含义
    AtomicBoolean 原子更新布尔类型
    AtomicInteger 原子更新整型
    AtomicLong 原子更新长整型
  2. 理解juc提供的atomic包的原子操作原理CAS

    使用CAS(Compare-And-Swap)操作保证数据原子性
    根据3个操作数
    第一个操作数:内存值value(V)
    第二个操作数:预估值expect(E)
    第三个操作数:更新值new(N)
    如果内存值与预估值一样,就更新为指定的值

3.原子更新数组类型

目标

掌握java.util.concurrent.atomic包下原子更新数组类型

介绍

通过原子的方式更新数组里的某个元素,Atomic包提供了以下3个类。

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

以上几个类提供的方法几乎一样,所以本节仅以AtomicIntegerArray为例进行讲解

AtomicIntegerArray类方法

AtomicIntegerArray类方法
int addAndGet(int i,int delta) 以原子方式将输入值与数组中索引i的元素相加
boolean compareAndSet(int i,int expect,int update) 如果当前值等于预期值,则以原子
方式将数组位置i的元素设置成update值

示例需求

使用普通整型数组存储一些元素,之后使用原子数组操作对普通数组元素进行修改,观察使用

示例代码

import java.util.concurrent.atomic.AtomicIntegerArray;
public class AtomicIntegerArrayTest {
public static void main(String[] args) {
int[] ints = {1,2,3};
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(ints);
atomicIntegerArray.addAndGet(0, 5);
System.out.println("原子数组操作结果:"+atomicIntegerArray.get(0));
System.out.println("普通数组操作结果:"+ints[0]);
}
}

示例效果

注意:

数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组
复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组

小结

  • 掌握java.util.concurrent.atomic包下原子更新数组类型

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

4.原子更新引用类型

目标

使用原子操作更新引用类型数据(也就是原子更新多个变量)

介绍

原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需
要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类

类API

介绍
AtomicReference 原子更新引用类型
AtomicReferenceFieldUpdater 原子更新引用类型里的字段
AtomicMarkableReference 原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是
AtomicMarkableReference(V initialRef,boolean initialMark)

AtomicReference类api方法

AtomicReference类
boolean compareAndSet(V expect, V update) 如果是期望值expect与当前内存值一样,更新为update

示例需求

使用原子引用类更新对象

示例代码

import java.util.concurrent.atomic.AtomicReference;
public class _05DemoTest {
public static void main(String[] args) {
//1.创建一个封装数据User对象
User user = new User("hguo",13);
//2.实例一个原子引用类型AtomicReference封装存储User类型数据
AtomicReference<User> atomicReference = new AtomicReference<>();
//3.将user对象的数据存入原子引用类型对象中
atomicReference.set(user);
//4.更新原子引用类型存储的数据
atomicReference.compareAndSet(user,new User("bozai",14));
//5.打印普通user对象数据与原子引用类型对象数据
System.out.println("普通对象数据:"+user+",对象hashcode:"+user.hashCode());
System.out.println("原子引用类型对象数据:"+atomicReference.get()+",对象hashcode:"+atomicReference.get().hashCode());
}
}

示例效果

小结

原子更新多个变量的数据什么解决?

答:使用原子更新引用类:AtomicReferece

5.原子更新字段类-ABA问题解决

目标

1.掌握原子更新引用类型里面的指定的字段

2.掌握原子类解决CAS带来的ABA问题

介绍

如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下3个类进行原子字段更新

api类

介绍
AtomicIntegerFieldUpdater 原子更新整型的字段的更新器
AtomicLongFieldUpdater 原子更新长整型字段的更新器
AtomicStampedReference 原子更新带有版本号的引用类型。该类将整数值与引用关联起来,
可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题

上3个类提供的方法几乎一样,所以本节仅以AtomicIntegerFieldUpdater为例进行讲解

示例

需求

使用AtomicIntegerFieldUpdater更新User类age属性的值

原子更新属性字段类型要求

  • 第一步,更新类的字段(属性)必须使用public volatile修饰符

    public volatile int 更新的属性或字段;
  • 第二步,因为原子更新字段类都是抽象类,每次创建AtomicIntegerFieldUpdater对象的
    时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。

    AtomicIntegerFieldUpdater.newUpdater(class, 更新的属性名);

实现代码

User类修改age属性为public volatile

public volatile int age;

AtomicIntegerFieldUpdaterTest测试类

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class _06DemoTest {
public static void main(String[] args) {
//1.实例一个AtomicIntegerFieldUpdater类
AtomicIntegerFieldUpdater atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
//2.实例一个普通的User对象
User user = new User("hguo",13);
//3.更新User对象的age属性加1操作
atomicIntegerFieldUpdater.getAndIncrement(user); //这里会直接更新user对象的age属性
//4.打印更新结果
System.out.println("普通对象age属性的结果:"+user.getAge());//14
System.out.println("原子更新后age属性的结果:"+atomicIntegerFieldUpdater.get(user));//14
}
}

示例效果

ABA问题介绍

CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差数据可有有变化。比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。

ABA问题示例

在运用CAS做Lock-Free(无锁编程)操作中有一个经典的ABA问题

线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题。如下例子:

现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:

head.compareAndSet(A,B);

在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:

此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:

其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题,例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败:

ABA问题解决

前面提到的原子操作AtomicStampedReference/AtomicMarkableReference可以解决ABA问题,允许一对变化的元素进行原子操作。

示例需求

例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败

AtomicStampedReference初始化api

public AtomicStampedReference(V initialRef, int initialStamp) {
//initialRef 初始化值
//initialStamp 初始化标记
...
}

AtomicStampedReference标记更新api方法

public boolean compareAndSet(V expectedReference, //预估值
V newReference, //更新值
int expectedStamp, //旧标记
int newStamp) //新标记
//如果预估值与旧标记与内存一致,就会将更新值与新标记进行更新到内存中,并返回更新成功或失败

示例代码

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class _07DemoTest {
//初始化原子操作类值为100
private static AtomicInteger atomicInteger = new AtomicInteger(100);
//使用AtomicStampedReference初始化值需要指定2个值,一个是初始化的数据值与一个标记号值
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,0);
public static void main(String[] args) throws InterruptedException {
//目标1:演示AtomicInteger会产生ABA问题
//1.创建第一个线程更新进行100->101,再从101->100
Thread t1 = new Thread(()->{
atomicInteger.compareAndSet(100,101);
atomicInteger.compareAndSet(101,100);
});
//2.创建第二个线程更新100->103
Thread t2 = new Thread(()->{
//阻塞,保证线程t1先执行完
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = atomicInteger.compareAndSet(100, 103);
System.out.println("AtomicInteger执行操作结果:"+b); //true
});
t1.start();
t2.start();
t1.join();
t2.join();
//保证线程全部执行完毕
//目标2:演示AtomicStampedReference解决ABA问题
//1.创建第一个线程更新进行100->101,再从101->100
Thread ref1 = new Thread(()->{
//阻塞1秒,保证ref2先运行
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//atomicStampedReference.getStamp() 用于获取当前标记
System.out.println("ref1-0-stamp:"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(100,101,
atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println("ref1-1-stamp:"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,
atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println("ref1-2-stamp:"+atomicStampedReference.getStamp());
});
//2.创建第二个线程更新100->103
Thread ref2 = new Thread(()->{
//先获取标记
int stamp = atomicStampedReference.getStamp();
System.out.println("ref2-0-stamp:"+stamp);
//阻塞2秒,保证ref1线程运行完成
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = atomicStampedReference.compareAndSet(100, 103,
stamp, stamp + 1);//失败
System.out.println("atomicStampedReference执行操作结果:"+b+
",内存最新值:"+atomicStampedReference.get(new int[]{atomicStampedReference.getStamp()}));
System.out.println("ref1-2-stamp:"+atomicStampedReference.getStamp());
});
//启动线程
ref1.start();
ref2.start();
}
}

示例效果

小结

原子更新引用类型里面资源的原子类

原子操作类如何解决ABA问题?

使用的是乐观锁,就是在CAS的基础上更新的时候再使用标记号或版本号进行更新,解决ABA问题的原子类

AtomicStampedReferece, 这个标记号是数字类型(推荐)

AtomicMarkableReference,这个标记号是boolean类型

6.总结

Atomic包下原子操作类

含义
AtomicBoolean 基本类型-原子更新布尔类型
AtomicInteger 基本类型-原子更新整型
AtomicLong 基本类型-原子更新长整型
AtomicIntegerArray 数组类型-原子更新整型数组里的元素
AtomicLongArray 数组类型-原子更新长整型
AtomicReferenceArray 数组类型-原子更新引用类型数组里的元素
AtomicReference 引用类类型-原子更新引用类型
AtomicReferenceFieldUpdater 引用类类型-原子更新引用类型里的字段
AtomicMarkableReference 引用类类型-原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是
AtomicMarkableReference(V initialRef,boolean initialMark)
AtomicIntegerFieldUpdater 引用类类型的字段-原子更新整型的字段的更新器
AtomicLongFieldUpdater 引用类类型的字段-原子更新长整型字段的更新器
AtomicStampedReference 引用类类型的字段-原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题

黄色部分是最为常用,原子操作类实现原理就是CAS操作,

带来的ABA问题也得到了解决,使用乐观锁给更新数据添加标记版本号解决的

posted @   Lz_蚂蚱  阅读(81)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起