概述
在多线程应用场景下,一般需要在代码中实现 加锁/解锁 操作来确保线程安全。在某些情况下,锁的处理会给程序带了很大的性能影响。在这种情况下, 我们就考虑无锁操作即在不加锁的情况下,依旧能保证线程的安全执行。
JAVA 无锁队列/栈 的实现
本篇就以下六个模块展开讨论:
- 无锁的原理
- 无锁队列的实现
- CountDownLatch简单介绍
- 无锁队列的测试
- 无锁栈的实现
- 无锁栈的测试
1、无锁的原理
无锁的实现原理是 “CAP”(Campare and swap)。翻译过来即“比较和交换”。
关于 CAP 的实现原理本篇我们不详细展开论述,在后面的文章专门介绍。下面主要介绍一下 CAP 的核心方法:compareAndSet ()。
在代码中该方法一般是这样调用的:
a.compareAndSet(oldValue, newValue)
其中 oldValue 表示老值,newValue 表示新值,该方法执行的结果有以下两种:
- 如果变量a的值此时和 oldValue 相等,那么将 newValue 赋予它,并返回 True。
- 如果变量a的值此时和 oldValue 不相等,那么它不做任何操作,并返回 False。
其中为了进行循环判断,该方法一般和while循环结合使用,这也就是我们常称的 自旋锁。
锁 的作用是保证同一时刻最多只能有一个线程操作某块内存,其他线程只能等到该线程处理完毕后进行读写操作。
无锁 是无法保证某块内存在同一时刻不被多个线程共享,它是在写内存过程前提供一个期望值,如果期望值与当前值相等,说明该线程上一次读取值之后,没有其他线程对这块内存进行处理,也就是说该操作是线程安全的
通过上面的概述其实我们不难发现,无锁还是存在以下隐患的:即操作的内存被其他线程操作两次,最终又返回原值时,无锁是无法判断是否有线程处理过该块内存的,但这种场景一般不影响结果。
举例:变量a = 5,现在有两个线程处理变量a,线程1要将它的值+5,线程2要将它的值+10,我们使用无锁来模拟这种场景下可能产生的情况:
2、无锁队列的实现
这里我直接贴出代码,通过注释的方式列举出每种操作对应的场景:
public class CapQueue<V> {
/**
* 模拟队列中的节点,通过 next属性相连接
*
* @param <V>
*/
private class Node<V> {
// 具体的值
private V value;
// 模拟队列中节点相连接
private AtomicReference<Node<V>> next;
public Node(V value, Node<V> next) {
this.value = value;
this.next = new AtomicReference<>(next);
}
}
/**
* 队列头,其中 head不计算在队列中
*/
private AtomicReference<Node<V>> head = null;
/**
* 队列尾,标记队尾元素
*/
private AtomicReference<Node<V>> tail = null;
/**
* 队列大小
*/
private AtomicInteger queueSize = new AtomicInteger(0);
public CapQueue() {
// 初始化队列头和尾
Node<V> node = new Node<>(null, null);
head = new AtomicReference<>(node);
tail = new AtomicReference<>(node);
}
/**
* 入队操作
*
* @param value
*/
public void enQueue(V value) {
Node<V> newNode = new Node<>(value, null);
Node