Java 高并发之无锁(CAS)

本篇主要讲 Java中的无锁 CAS ,无锁 顾名思义就是 以不上锁的方式解决并发问题,而不使用synchronized 和 lock 等。。

1. Atomic 包

java.util.concurrent.atomic 包下类都是原子类,原子类都是基于 sun.misc.Unsafe 实现的

基本可以分为一下几类:

  1. 原子性基本数据类型:AtomicBoolean、AtomicInteger、AtomicLong
  2. 原子性对象引用类型:AtomicReferenceAtomicStampedReference、AtomicMarkableReference
  3. 原子性数组:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
  4. 原子性对象属性更新:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

image-20200805205141008

2.CAS 的概念

无锁 是指 所有的线程都能 无障碍的到达临界点(但是能不能成功操作临界点的值 是不一定的) ,无锁的实现采用的是 CAS(compare and swap) 算法实现的
  1. CAS 需要依赖 CPU提供的 CAS指令,CPU 为了解决并发问题,提供了 CAS 指令
  2. CAS 指令需要3个参数 ,(变量值 、预期值、新值),只有当 变量值和预期值 相同的时候 才会更新变量值为新值
  3. CAS 是一条 CPU 指令,由 CPU 硬件级别上保证原子性

3.AtomicInteger 案例 以源码

AtomicInteger 我相信应该大部分都用过吧, 用于解决 count++

因为count ++ 操作 不是原子的,可能 第一个线程拿到的 count = 0 将要进行++操作的时候,被其他线程抢占了CPU资源,第二个线程此时读取的还是count = 0 ,那么这样就会造成问题, 而AtomicInteger 它内部使用CAS 算法 保证了并发安全问题。

public class AtomicIntegerDemo {

private static int count = 0;
private static AtomicInteger  atomicInteger = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {

Thread[] threads = new Thread[10];

for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int j = 0; j < 1000; j++){
count++;
atomicInteger.incrementAndGet(); //内部CAS 进行自增
}
});
thread.start();
threads[i] = thread;
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
System.out.println("结果: " + count);
System.out.println("结果: " + atomicInteger.get());
}
}

输出结果发现,atomicInteger 保证了数据正确

image-20200805210737713

源码分析:内部通过 unafe 类提供的方法处理的,Java中的Unsafe类为我们提供了类似C++手动管理内存的能力,可以直接获取某个属性的内容地址,并且提供了一些对内存地址上的值得操作

private static final Unsafe unsafe = Unsafe.getUnsafe();

//获取value 属性的 对象偏移量
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
/**

  • Atomically increments by one the current value.

*

  • @return the updated value

*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

unsafe的 getAndAddInt 方法 ,var5 相当于期望值 只有 内部调用 compareAndSwapInt 判断var1 对象(AtomicInteger) 的内存偏移值var2 (valueOffset) 是否和期望值相同,如果相同表示 这期间没有其他线程修改, 否则 自旋再 获取 当前var5 再比较

//var5是期望值 var5+var4 是新值
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);//获取this上的 valueOffset的偏移量的值 value
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}

这个方法是 native (C/C++)编写的代码,内部实现 使用了CPU的 CAS指令 具有原子性

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

AtomicLong 和 AtomicInteger类似 这里不展开说了,

4.AtomicBoolean 原理和使用场景

而 AtomicBoolean 也类似,不过内部value 是一个int 通过 int值是 1 还是 0 当做 true / false

public AtomicBoolean(boolean initialValue) {
value = initialValue ? 1 : 0;
}

public final boolean compareAndSet(boolean expectedValue, boolean newValue) {
return VALUE.compareAndSet(this,
(expectedValue ? 1 : 0),
(newValue ? 1 : 0));
}

当在多个线程 只处理 一次初始化功能的时候 可以使用

private static AtomicBoolean initialized = new AtomicBoolean(false);
public void init()
{
if( initialized.compareAndSet(false, true) )
{
// 这里放置初始化代码....
}
}

5.AtomicReference

一直很难理解 AtmoicReference 的 使用场景,毕竟 java赋值操作本来就是 原子性的

作用在 对 对象进行原子操作 提供了一种读和写都是原子性的 对象引用变量

AtomicReference和AtomicInteger非常类似 AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性。

疑问:引用类型的赋值是原子的 为什么要有AtomicReference 来保证修改对象引用时的线程安全性 呢?

真正需要使用AtomicReference的场景是你需要CAS类操作时,由于涉及到比较、设置等多于一个的操作

下面的案例是 :模拟 拦截器拦截到请求 记录ip 然后 去更新最新的 LoginDetailInfo 登录信息 使用AtomicReference 去包装老的 登录信息 , 使用5个线程去同时更新最新的 登录信息

package com.johnny.atomic;


import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

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

/**

  • @author johnny
  • @create 2020-09-16 下午3:58

**/
public class AtomicReferenceDemo {


private static final int CORE_SIZE = 5;
private static final int MAX_SIZE = 10;

private static final int QUEUE_SIZE = 100;

private static final CountDownLatch countDownLatch = new CountDownLatch(5);


public static void main(String[] args) throws InterruptedException {


ExecutorService executorService = new ThreadPoolExecutor(CORE_SIZE, MAX_SIZE, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(QUEUE_SIZE));

//interceptor ip
//根据拦截器拦截到 最新的登录ip
String newIp = "47.98.250.170";

//from db search old LoginDetailInfo
//从数据库中查到 最新的一次 登录信息
LoginDetailInfo oldLoginDetailInfo = LoginDetailInfo.builder()
.loginCount(10)
.ip("47.98.250.138")
.timeStamp(System.currentTimeMillis()).build();

//封装老的 登录信息
AtomicReference<LoginDetailInfo> atomicReference = new AtomicReference<>(oldLoginDetailInfo);

//封装任务
TaskRun taskRun = new TaskRun(atomicReference, newIp);
//使用5个线程去 同时更新 最新的 登录信息,只要有一个成功即可
for (int i = 0; i < 5; i++) {
executorService.execute(taskRun);
}

//主线程 等待
countDownLatch.await();

System.out.println("【所有线程执行完毕 main 线程结束 】");
}


static class TaskRun implements Runnable {

private String ip;

private AtomicReference<LoginDetailInfo> atomicReference;

/**

  • @param atomicReference : 老的 atomicReference

*/
public TaskRun(AtomicReference<LoginDetailInfo> atomicReference, String ip) {
this.ip = ip;
this.atomicReference = atomicReference;
}

@Override
public void run() {

if (atomicReference != null) {

LoginDetailInfo oldLoginDetailInfo = atomicReference.get();

long count = oldLoginDetailInfo.getLoginCount() + 1;

LoginDetailInfo newLoginDetailInfo = new LoginDetailInfo();
newLoginDetailInfo.setLoginCount(count);
newLoginDetailInfo.setIp(ip);
newLoginDetailInfo.setTimeStamp(System.currentTimeMillis());

try {
//模拟 执行一下其他操作 可以让多个线程 都同时走到了 这里 保证多个线程 拿到的 oldLoginDetailInfo 是一个 ,这样才会只有一个 能更新成功
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}

if (atomicReference.compareAndSet(oldLoginDetailInfo, newLoginDetailInfo)) {
//save To Db or some ...
System.out.println(Thread.currentThread().getName() + " 线程更新成功 --- {}");
System.out.println(newLoginDetailInfo);
}else{
System.out.println(Thread.currentThread().getName() + " 线程更新失败 其他线程已经更新 ---");
}
//计数器 -1
countDownLatch.countDown();
}

}
}


@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
static class LoginDetailInfo {

private long loginCount;

private String ip;

private long timeStamp;

}

}

运行结果如下:

image-20200921181136102

这里的使用场景很牵强,不知道你会 怎么使用AtomicReference 欢迎在下方留言 给我提点思路

6. AtomicStampRefence

线程判断被修改对象是否可以正确写入的条件是对象的当前值和期望是否一致。这个逻辑从一般意义上来说是正确的。但有可能出现一个小小的例外,就是当你获得对象当前数据后,在准备修改为新值前,对象的值被其他线程连续修改了2次,而经过这2次修改后,对象的值又恢复为旧值。这样,当前线程就无法正确判断这个对象究竟是否被修改过,这就是典型的 CAS ABA问题

如何解决 ABA问题呢,其实Java 提供了 AtomicStampRefence 就是用来解决ABA问题的

模拟如下案例:

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

head.compareAndSet(A,B);

image-20200922090126967

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

image-20200922090201870

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

image-20200922090215862

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

下面我们就来用代码 模拟上面这个案例:

6.1 首先我们定义 栈 :

/**
*
*/
@Data
static class Node<T> {

private T value;

private Node<T> next;

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


static class Stack<T> {

private Node<T> top;

private int length = 0;


public boolean isEmpty() {
if (length == 0 || top == null) {
return true;
} else {
return false;
}
}

public void push(Node<T> node) {
if (isEmpty()) {
top = node;
} else {
//交换
//node.setNext(top);
top = node;
}
length++;
}

public void push(T value) {
Node<T> node = new Node<T>(value);
if (isEmpty()) {
top = node;
} else {
//交换
node.setNext(top);
top = node;
}
length++;
}

public T pop() {

if (isEmpty()) {
System.out.println("栈 为空");
return null;
} else {
T result = top.getValue();
top = top.next;
length--;
return result;
}
}

public T top() {
if (isEmpty()) {
System.out.println("栈 为空");
return null;
} else {
return top.getValue();
}
}

public Node<T> topNode() {
if (isEmpty()) {
System.out.println("栈 为空");
return null;
} else {
return top;
}
}

public Node<T> topNextNode() {
if (isEmpty()) {
System.out.println("栈 为空");
return null;
} else {
return top.next;

}
}

public int size() {
return length;
}

public void deleteStack() {
top = null;
length = 0;
}

}

6.2 使用 AtmoicReference 来看看带来的问题

package com.johnny.atomic;

import lombok.Data;

import java.util.concurrent.atomic.AtomicReference;

/**

  • ABA 问题Demo

*

  • @author johnny
  • @create 2020-09-20 下午5:52

**/
public class AtomicReferenceDemo {


/**

  • 使用 Stack 来 模拟 CAS ABA 问题

*

  • @param args

*/
public static void main(String[] args) throws InterruptedException {


//1.初始化 栈
Stack<String> stack = new Stack<String>();

stack.push("B");
stack.push("A");

//headReference 保存 当前的 栈的 top
AtomicReference<Node<String>> headReference = new AtomicReference<>(stack.topNode());


//2.需要2个线程
TaskA taskA = new TaskA(stack, headReference);
TaskB taskB = new TaskB(stack, headReference);


Thread threadA = new Thread(taskA);
Thread threadB = new Thread(taskB);

threadA.start();
threadB.start();


threadA.join();
threadB.join();

System.out.println(stack.pop());
System.out.println(stack.pop());
System.out.println(stack.pop());

}

static class TaskA implements Runnable {

private AtomicReference<Node<String>> headReference;
private Stack<String> stack;

public TaskA(Stack<String> stack, AtomicReference<Node<String>> headReference) {
this.stack = stack;
this.headReference = headReference;
}

@Override
public void run() {

if (stack.topNode() == headReference.get()) {
Node<String> aNode = headReference.get();
Node<String> bNode = stack.topNextNode();

//让TaskB 去把 AB 出栈并且 入 D C A
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//此时 TaskB 把 栈 变成 D C A top还是A 更新 因为此时TaskA还是认为 B.next = null 就会弄丢 D C
if (headReference.compareAndSet(aNode, bNode)) {
//把A出栈
stack.pop();
//添加B 当做top
stack.push(bNode);
} else {
System.out.println("有其他线程 已更新 top");
}
}
}
}

static class TaskB implements Runnable {

private AtomicReference<Node<String>> headReference;
private Stack<String> stack;

public TaskB(Stack<String> stack, AtomicReference<Node<String>> headReference) {
this.stack = stack;
this.headReference = headReference;
}

@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (stack.topNode() == headReference.get()) {
Node<String> aNode = headReference.get();
//A 出栈
stack.pop();
//B 出栈
stack.pop();

Node<String> dNode = new Node<>("D");
Node<String> cNode = new Node<>("C");
dNode.next = cNode;
cNode.next = aNode;
//依次入栈 D C A
if (headReference.compareAndSet(aNode, dNode)) {
stack.push(dNode);
}
if (headReference.compareAndSet(dNode, cNode)) {
stack.push(cNode);
}
if (headReference.compareAndSet(cNode, aNode)) {
stack.push(aNode);
}
}


}
}

}

可以看到 最后栈里 只有 B了,其他的 D C 都被弄丢了

image-20200922090515321

6.3 通过使用 AtomicStampedReference 来改进
AtomicStampedReference 内部还维护了一个Stamp 来记录 更改次数 ,这样当 从 A 变到 B 再变回A的时候,虽然值 没变,但是Stamp 变化了,依然不能更新成功

/**

  • 使用 Stack 来 模拟 CAS ABA 问题

*

  • @param args

*/
public static void main(String[] args) throws InterruptedException {


//1.初始化 栈
Stack<String> stack = new Stack<String>();

stack.push("B");
stack.push("A");

AtomicStampedReference<Node<String>> head = new AtomicStampedReference<>(stack.topNode(), 0);


//2.需要2个线程
TaskA taskA = new TaskA(stack, head);
TaskB taskB = new TaskB(stack, head);


Thread threadA = new Thread(taskA);
Thread threadB = new Thread(taskB);

threadA.start();
threadB.start();


threadA.join();
threadB.join();

System.out.println(stack.pop());
System.out.println(stack.pop());
System.out.println(stack.pop());

}

static class TaskA implements Runnable {

private Stack<String> stack = null;

private AtomicStampedReference<Node<String>> headStampReference;

public TaskA(Stack<String> stack, AtomicStampedReference<Node<String>> headStampReference) {
this.stack = stack;
this.headStampReference = headStampReference;
}

@Override
public void run() {

if (stack.topNode() == headStampReference.getReference()) {

AtomicStampedReference<Node<String>> headNode = this.headStampReference;
int stamp = headNode.getStamp();

Node<String> bNode = stack.topNextNode();

//让TaskB 去把 AB 出栈并且 入 D C A
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//此时 TaskB 把 栈 变成 D C A top还是A 更新 因为此时TaskA还是认为 B.next = null 就会弄丢 D C
if (headNode.compareAndSet(stack.topNode(), bNode, stamp, stamp + 1)) {
stack.pop(); //A出栈
//入栈B 但是 B的 next = null
stack.push(bNode);
} else {
System.out.println("有其他线程 已更新 top");
}
}
}
}

static class TaskB implements Runnable {

private Stack<String> stack;

private AtomicStampedReference<Node<String>> headStampReference;

public TaskB(Stack<String> stack, AtomicStampedReference<Node<String>> headStampReference) {
this.stack = stack;
this.headStampReference = headStampReference;
}


@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (stack.topNode() == headStampReference.getReference()) {
//A 出栈
stack.pop();
//B 出栈
stack.pop();

Node<String> aNode = headStampReference.getReference();

Node<String> dNode = new Node<>("D");
Node<String> cNode = new Node<>("C");
cNode.next = dNode;
aNode.next = cNode;

//依次入栈 D C A
if (headStampReference.compareAndSet(aNode, dNode, headStampReference.getStamp(), headStampReference.getStamp() + 1)) {
stack.push(dNode);
}
if (headStampReference.compareAndSet(dNode, cNode, headStampReference.getStamp(), headStampReference.getStamp() + 1)) {
stack.push(cNode);
}
if (headStampReference.compareAndSet(cNode, aNode, headStampReference.getStamp(), headStampReference.getStamp() + 1)) {
stack.push(aNode);
}
}

}
}

可以看到 B 并没有被更新到 栈中 因为 通过AtomicStampedReference 的Stampd 已经能够知道 这个栈已经被更改过

image-20200922090729304

7. AtomicMarkableReference

AtomicMarkableReference可以理解为上面AtomicStampedReference的简化版,就是不关心修改过几次,仅仅关心是否修改过。因此变量mark是boolean类型,仅记录值是否有过修改。

private static class Pair<T> {
final T reference;
final boolean mark;
private Pair(T reference, boolean mark) {
this.reference = reference;
this.mark = mark;
}
static <T> Pair<T> of(T reference, boolean mark) {
return new Pair<T>(reference, mark);
}
}

和上面的 AtomicStampedReference 类似 也来解决 ABA 问题 就不展开说了

8. AtomicIntegerFieldUpdater

AtomicIntegerFieldUpdater,它实现了可以线程安全地更新对象中的整型变量。比如如下的 int

类型的 count 变量 ,当你的系统有些变量当初并没有设计成 Atomic ,那么现在可以通过FieldUpdater来安全的 更新,并且不需要带来 对系统代码的修改,最多只需要添加 一个 volatile , 不会造成什么影响

public class AtomicIntegerFieldUpdaterDemo {

public int count = 100;

public static AtomicIntegerFieldUpdater<AtomicIntegerFieldUpdaterDemo> atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicIntegerFieldUpdaterDemo.class, "count");


public static void main(String[] args) {

AtomicIntegerFieldUpdaterDemo atomicIntegerFieldUpdaterDemo = new AtomicIntegerFieldUpdaterDemo();

if (atomicIntegerFieldUpdater.compareAndSet(atomicIntegerFieldUpdaterDemo, 100, 200)) {
System.out.println("更新成功 count : " + atomicIntegerFieldUpdaterDemo.count);
}

if(atomicIntegerFieldUpdater.compareAndSet(atomicIntegerFieldUpdaterDemo,100,300)){
System.out.println("更新成功 count : " + atomicIntegerFieldUpdaterDemo.count);
}
}

报错了:因为必须是 volatile 变量,并且不能是private

image-20200927142809354

public volatile int count = 100;

打印结果:

更新成功 count : 200

总结

本篇主要讲解了 Java中 无锁CAS相关知识,包括 常用的AtomicInteger ,AtomicBoolean , AtomicReference AtomicStampedReference 和AtomicMarkableReference, AtomicIntegerFieldUpdater,以及介绍了ABA问题,和解决方法。 一起来了解吧!


Aska小强
100 声望6 粉丝