Java5 下实现锁无关数据结构
2008-07-19 22:53
806 查看
from:IBM developworks
本文将介绍锁无关数据结构的应用及其相关概念,并在 Java 环境下利用 JDK 1.5 提供的一组类进行锁无关数据结构设计,从而避免基于锁的数据结构可能引发的同步问题,以改善程序的可靠性。
介绍
通
常在一个多线程环境下,我们需要共享某些数据,但为了避免竞争条件引致数据出现不一致的情况,某些代码段需要变成原子操作去执行。这时,我们便需要利用各
种同步机制如互斥(Mutex)去为这些代码段加锁,让某一线程可以独占共享数据,避免竞争条件,确保数据一致性。但可惜的是,这属于阻塞性同步,所有其
他线程唯一可以做的就是等待。基于锁(Lock based)的多线程设计更可能引发死锁、优先级倒置、饥饿等情况,令到一些线程无法继续其进度。
锁无关(Lock free)算法,顾名思义,即不牵涉锁的使用。这类算法可以在不使用锁的情况下同步各个线程。对比基于锁的多线程设计,锁无关算法有以下优势:
对死锁、优先级倒置等问题免疫:它属于非阻塞性同步,因为它不使用锁来协调各个线程,所以对死锁、优先级倒置等由锁引起的问题免疫;
保证程序的整体进度:由于锁无关算法避免了死锁等情况出现,所以它能确保线程是在运行当中,从而确保程序的整体进度;
性能理想:因为不涉及使用锁,所以在普遍的负载环境下,使用锁无关算法可以得到理想的性能提升。
自 JDK 1.5 推出之后,当中的
的一组类为实现锁无关算法提供了重要的基础。本文介绍如何将锁无关算法应用到基本的数据结构中,去避免竞争条件,允许多个线程同时存取和使用集合中的共享
数据。如果一个数据结构本身并非是线程安全的,一旦在多线程环境下使用这个数据结构,必须施加某种同步机制,否则很可能会出现竞争条件。我们即将设计的锁
无关数据结构是线程安全的,所以使用时无需再编写额外代码去确保竞争条件不会出现。
数据结构的设计
本
文会由浅入深,先提出锁无关栈(Stack)的实现方法,为读者提供必须的基础知识,栈是一个先入后出(Last in first
out)的基本数据结构。当读者掌握必要的技术之后,我们便会着手设计相对复杂的链表(Linked
List)数据结构,链表是很多其他数据结构的基础组成部分。不过,对比起栈,链表可以面对更棘手的线程同步问题。
在开始设计之前,我们需要理解一个十分重要的原语
栈
栈能以数组或者链表作为底下的储存结构,虽然采取链表为基础的实现方式会占用多一点空间去储存代表元素的节点,但却可避免处理数组溢出的问题。故此我们将以链表作为栈的基础。
首先,我们分析一下一个非线程安全的版本。为了清楚表达和集中于文章的主题,代码没有包含对异常及不正当操作的处理,读者请留意。它的代码如下:
清单 1. 非线程安全的栈实现
数据成员
push 方法
让我们先考虑一下
pop 方法
至于
peek 方法
而
在 Java 要解决
清单 2. 锁无关的栈实现
这个新的实现方式和刚刚的很不同,看似比较复杂。成员数据
push 方法
现在我们先分析新的
pop 方法
链表
栈是一个相当简单的数据结构,要解决的同步问题亦比较直接和容易。但很多环境下,栈并不能满足我们的需求。我们将介绍链表,它有更广泛的应用范围。为了保持简洁,这个链表所提供的方法较少。以下是链表的非线程安全版本:
清单 3. 非线程安全的链表实现
数据成员
addAfter 方法
先考虑一下
在多线程环境下,如果两个线程同时调用
试想像现在链表内有三个元素,分别是 A、B 和 C 。如果一个线程准备把一个元素加到 A 之后,它首先确定 A 节点的位置,然后新建一个节点 A1,这个节点的
成员连接起来时,此时线程因为时间片用完而被挂起。另一个线程亦准备在 A 之后加入一个新元素,它建立 A2 节点,并解除 A 和 B
原本的连结,然后依着 A-A2-B 的次序重新连接三个节点,操作完成。现在,刚刚被挂起的线程恢复执行,并依着 A-A1-B
的次序去重新连接三个节点。这时问题出现,刚刚新加入的 A2 节点遗失了。解决方法是,每一次准备把 A 元素和新建立的节点连接时,检查 A 节的
addAfter 和 remove 的冲突
同时间执行
成员把 A 和 C 互相连接,完成移除操作。可是,刚刚被加入的 B2 元素则遗失了,因为 A 节点跳过了 B 节点,直接连接着 C
节点。故此,我们要有一个解决方案。 Timothy L. Harris
提供了一个方法,他把整个移除过程分成两个步骤,逻辑删除和物理删除。逻辑删除并非真正地移除一个节点,而是把要移除的节点标记为已删除。另一方面,物理
删除则真实从集合左移除一个节点。每次要加入新元素到指定节点之后,都必先检查该节点有没有被标记为删除,没有的话才把新的节点连接到集合中。这是通过
remove 方法
链表有可能发生的冲突比较多,另一个问题便是两个线程同时间执行
方法移除 C 元素,它解除 B 和 C 的连结,并把 B 和 D 连接起来,移除操作完成。之后刚才的线程恢复运行,继续执行余下的操作,把 A
和 C 连接起来,这样之前的移除 C 的操作便受到了破坏。最终链表中的元素变成 A-C-D,C 元素没有被移除。所以,我们
从上文的各种情况可见,我们必须原子地施加某些检查机制,确保数据的一致性。我们现在看看解决这些问题的锁无关链表是如何实现的,这些代码应该和读者在算法书上看到的很不同。以下是它的代码:
清单 4. 锁无关的链表实现
和之前不同,
addAfter 方法
首先考虑一下
addFirst 方法
Remove 方法
ABA 问题
因为
仍然会看作两者相同。不过,一些算法因为需求的关系无法容忍这种行为。当一些内存位置被重用的时候,这个问题便可能会发生。在没有垃圾回收机制的环境
下,ABA 问题需要一些机制例如标记去解决。但由于 JVM 会为我们处理内存管理的问题,故此以上的实现足以避免 ABA 问题的出现。
结束语
以往很多的锁无关数据结构都以 Immutable object 的方式去达致线程安全,这很像 Java 中的
,但因为涉及过多的复制操作,令性能低下。但经过十多年,锁无关数据结构已发展得十分成熟,性能并不逊色于传统的实现方式。编写锁无关算法是十分困难的,
但因为数据结构是经常被重用的部分,首先把这个概念应用到数据结构中,可以轻易让程序进入锁无关的世界,体验它所带来的好处。
本文将介绍锁无关数据结构的应用及其相关概念,并在 Java 环境下利用 JDK 1.5 提供的一组类进行锁无关数据结构设计,从而避免基于锁的数据结构可能引发的同步问题,以改善程序的可靠性。
介绍
通
常在一个多线程环境下,我们需要共享某些数据,但为了避免竞争条件引致数据出现不一致的情况,某些代码段需要变成原子操作去执行。这时,我们便需要利用各
种同步机制如互斥(Mutex)去为这些代码段加锁,让某一线程可以独占共享数据,避免竞争条件,确保数据一致性。但可惜的是,这属于阻塞性同步,所有其
他线程唯一可以做的就是等待。基于锁(Lock based)的多线程设计更可能引发死锁、优先级倒置、饥饿等情况,令到一些线程无法继续其进度。
锁无关(Lock free)算法,顾名思义,即不牵涉锁的使用。这类算法可以在不使用锁的情况下同步各个线程。对比基于锁的多线程设计,锁无关算法有以下优势:
对死锁、优先级倒置等问题免疫:它属于非阻塞性同步,因为它不使用锁来协调各个线程,所以对死锁、优先级倒置等由锁引起的问题免疫;
保证程序的整体进度:由于锁无关算法避免了死锁等情况出现,所以它能确保线程是在运行当中,从而确保程序的整体进度;
性能理想:因为不涉及使用锁,所以在普遍的负载环境下,使用锁无关算法可以得到理想的性能提升。
自 JDK 1.5 推出之后,当中的
java.util.concurrent.atomic
的一组类为实现锁无关算法提供了重要的基础。本文介绍如何将锁无关算法应用到基本的数据结构中,去避免竞争条件,允许多个线程同时存取和使用集合中的共享
数据。如果一个数据结构本身并非是线程安全的,一旦在多线程环境下使用这个数据结构,必须施加某种同步机制,否则很可能会出现竞争条件。我们即将设计的锁
无关数据结构是线程安全的,所以使用时无需再编写额外代码去确保竞争条件不会出现。
数据结构的设计
本
文会由浅入深,先提出锁无关栈(Stack)的实现方法,为读者提供必须的基础知识,栈是一个先入后出(Last in first
out)的基本数据结构。当读者掌握必要的技术之后,我们便会着手设计相对复杂的链表(Linked
List)数据结构,链表是很多其他数据结构的基础组成部分。不过,对比起栈,链表可以面对更棘手的线程同步问题。
在开始设计之前,我们需要理解一个十分重要的原语
Compare-and-swap (CAS),Herlihy 证明了
CAS是实现锁无关数据结构的通用原语,
CAS可以原子地比较一个内存位置的内容及一个期望值,如果两者相同,则用一个指定值取替这个内存位罝里的内容,并且提供结果指示这个操作是否成功。很多现代的处理器已经提供了
CAS的硬件实现,例如在 x86 架构下的
CMPXCHG8指令。而在 Java 下,位于
java.util.concurrent.atomic内的
AtomicReference<V>类亦提供了
CAS原语的实现,并且有很多其他的扩展功能。
CAS操作将会是稍后实现的锁无关数据算法无可缺少的指令。
栈
栈能以数组或者链表作为底下的储存结构,虽然采取链表为基础的实现方式会占用多一点空间去储存代表元素的节点,但却可避免处理数组溢出的问题。故此我们将以链表作为栈的基础。
首先,我们分析一下一个非线程安全的版本。为了清楚表达和集中于文章的主题,代码没有包含对异常及不正当操作的处理,读者请留意。它的代码如下:
清单 1. 非线程安全的栈实现
class Node<T> { Node<T> next; T value; public Node(T value, Node<T> next) { this.next = next; this.value = value; } } public class Stack<T> { Node<T> top; public void push(T value) { Node<T> newTop = new Node<T>(value, top); top = newTop; } public T pop() { Node<T> node = top; top = top.next; return node.value; } public T peek() { return top.value; } } |
top储存着栈顶的节点,它的类型为
Node<T>,这是因为我们的栈是基于链表的。
Node<T>代表一个节点,它有两个数据成员,
value储存着入栈的元素,而
next储存下一个节点。这个类有三个方法,分别是
push、
pop和
peek,它们是基本的栈操作。除了
peek方法是线程安全之外,其余两个方法在多线程环境之下都有可能引发竞争条件。
push 方法
让我们先考虑一下
push方法,它能将一个元素入栈。调用
push时,它首先建立一个新的节点,并将
value数据成员设定为传入的参数,而
next数据成员则被赋值为当前的栈顶。然后它把
top数据成员设定成新建立的节点。假设有两个线程 A 和 B 同时调用
push方法,线程 A 获取当前栈顶的节点去建立新的节点(
push方法代码第一行),但由于时间片用完,线程 A 暂时挂起。此时,线程 B 获取当前栈顶的节点去建立新的节点,并把
top设定成新建立的节点(
push方法代码第二行)。然后,线程 A 恢复执行,更新栈顶。当线程 B 对
push的调用完成后,线程 A 原本获得的栈顶已经「过期」,因为线程 B 用新的节点取代了原本的栈顶。
pop 方法
至于
pop方法,它把栈顶的元素弹出。
pop方法把栈顶暂存在一个本地变量
node,然后用下一个节点去更新栈顶,最后返回变量
node的
value数据成员。如果两个线程同时调用这个方法,可能会引起竞争条件。当一个线程将当前栈顶赋值到变量
node,并准备用下一个节点更新栈顶时,这个线程挂起。另一个线程亦调用
pop方法,完成并返回结果。刚刚被挂起的线程恢复执行,但由于栈顶被另一个线程变更了,所以继续执行的话会引起同步问题。
peek 方法
而
peek方法只是简单地返回当前位于栈顶的元素,这个方法是线程安全的,没有同步问题要解决。
在 Java 要解决
push和
pop方法的同步问题,可以用
synchronized这个关键词,这是基于锁的解决方案。现在我们看看锁无关的解决方案,以下是锁无关栈实现的代码:
清单 2. 锁无关的栈实现
import java.util.concurrent.atomic.*; class Node<T> { Node<T> next; T value; public Node(T value, Node<T> next) { this.next = next; this.value = value; } } public class Stack<T> { AtomicReference<Node<T>> top = new AtomicReference<Node<T>>(); public void push(T value) { boolean sucessful = false; while (!sucessful) { Node<T> oldTop = top.get(); Node<T> newTop = new Node<T>(value, oldTop); sucessful = top.compareAndSet(oldTop, newTop); }; } public T peek() { return top.get().value; } public T pop() { boolean sucessful = false; Node<T> newTop = null; Node<T> oldTop = null; while (!sucessful) { oldTop = top.get(); newTop = oldTop.next; sucessful = top.compareAndSet(oldTop, newTop); } return oldTop.value; } } |
top的类型由
Node<T>改为
AtomicReference<Node<T>>,
AtomicReference<V>这个类可以对
top数据成员施加
CAS操作,亦即是可以允许
top原子地和一个期望值比较,两者相同的话便用一个指定值取代。从上文可知,我们需要解决遇到栈顶「过期」的问题。
push 方法
现在我们先分析新的
push方法如何处理这个问题,确保竞争条件不会出现。在
while循环中,通过在
top数据成员调用
AtomicReference.get(),
oldTop持有当前栈顶节点,这个栈顶稍后会被取替。变量
newTop则被初始化为新的节点。最重要的一步,
top.compareAndSet(oldTop, newTop),它比较
top和
oldTop这两个引用是否相同,去确保
oldTop持有的栈顶并未「过期」,亦即未被其他线程变更。假如没有过期,则用
newTop去更新
top,使之成为新的栈顶,并返回
boolean值
true。否则,
compareAndSet方法便返回
false,并且令到循环继续执行,直至成功。因为
compareAndSet是原子操作,所以可以保证数据一致。
pop 方法
pop方法把栈顶的元素弹出,它的实现方式和
push方法十分类同。在
while循环内,
compareAndSet检查栈顶有没有被其他线程改变,数据一致的话便更新
top数据成员并把原本栈顶弹出。如果失败,便重新尝试,直至成功。
push和
pop都没有使用任何锁,所以全部线程都不用停下来等待。
链表
栈是一个相当简单的数据结构,要解决的同步问题亦比较直接和容易。但很多环境下,栈并不能满足我们的需求。我们将介绍链表,它有更广泛的应用范围。为了保持简洁,这个链表所提供的方法较少。以下是链表的非线程安全版本:
清单 3. 非线程安全的链表实现
class Node<T> { Node<T> next; T value; public Node(T value, Node<T> next) { this.value = value; this.next = next; } } class LinkedList<T> { Node<T> head; public LinkedList() { head = new Node<T>(null, null); } public void addFirst(T value) { addAfter(head.value, value); } public boolean addAfter(T after, T value) { for (Node<T> node = head; node != null; node = node.next) { if (isEqual(node.value, after)) { Node<T> newNode = new Node<T>(value, node.next); node.next = newNode; return true; } } return false; } public boolean remove(T value) { for (Node<T> node = head; node.next != null; node = node.next) { if (isEqual(node.next.value, value)) { node.next = node.next.next; return true; } } return false; } boolean isEqual(T arg0, T arg1) { if (arg0 == null) { return arg0 == arg1; } else { return arg0.equals(arg1); } } } |
head是链表的头,它没有存储任何元素,而是直接指向第一个元素,这可以令稍后的
remove方法较易实现。这个链表有三个公用方法,其中
addAfter和
remove比较重要。
addAfter 方法
先考虑一下
addAfter方法,这个方法把一个新元素加入到集合内指定元素之后的位置,并返回一个
boolean值指示元素有没有被加入到集合中,元素没有被加入的原因是因为集合内没有所指定的元素。它首先在一个
for循环中寻找指定元素的节点,成功发现指定的节点后,便建立一个新节点。这个新节点的
next数据成员连接到指定节点原本的
next,指定节点的
next则连到新节点上。另一方面,
remove方法寻找指定元素并从集合中移除,并且返回一个
boolean值指示元素有没有被移除,返回
false代表集合中没有指定的元素。这个方法在一个循环寻找要移除的元素,并且将左右两边的元素重新连接。
在多线程环境下,如果两个线程同时调用
addAfter或
remove方法,或者一个线程调用
addAfter方法而同一时间另一个线程调用
remove方法均有机会引发竞争条件。
试想像现在链表内有三个元素,分别是 A、B 和 C 。如果一个线程准备把一个元素加到 A 之后,它首先确定 A 节点的位置,然后新建一个节点 A1,这个节点的
value数据成员储存着新加入的元素,而
next数据成员则储存着 B 节点的引用。当这个线程准备把 A 和 A1 通过
next
成员连接起来时,此时线程因为时间片用完而被挂起。另一个线程亦准备在 A 之后加入一个新元素,它建立 A2 节点,并解除 A 和 B
原本的连结,然后依着 A-A2-B 的次序重新连接三个节点,操作完成。现在,刚刚被挂起的线程恢复执行,并依着 A-A1-B
的次序去重新连接三个节点。这时问题出现,刚刚新加入的 A2 节点遗失了。解决方法是,每一次准备把 A 元素和新建立的节点连接时,检查 A 节的
next有否被其他线程改动过,没有改动过才进行连接,这是通过
CAS操作原子地进行的。
addAfter 和 remove 的冲突
同时间执行
addAfter和
remove亦有可能引起竞争条件。同样地,现在一个链表内有三个元素 A、B 和 C 。当一个线程准备调用
remove方法从这个集合中移除 B 元素,它首先获得 A 节点,然后准备通过改变 A 节点的
next成员,把 A 和 C 互相连接时,这个线程突然被挂起。此时另一个线程调用
addAfter方法在 B 节点后插入一个新的元素 B2 。插入操作完成后,刚才被挂起的线程恢复执行,并且通过改变
next
成员把 A 和 C 互相连接,完成移除操作。可是,刚刚被加入的 B2 元素则遗失了,因为 A 节点跳过了 B 节点,直接连接着 C
节点。故此,我们要有一个解决方案。 Timothy L. Harris
提供了一个方法,他把整个移除过程分成两个步骤,逻辑删除和物理删除。逻辑删除并非真正地移除一个节点,而是把要移除的节点标记为已删除。另一方面,物理
删除则真实从集合左移除一个节点。每次要加入新元素到指定节点之后,都必先检查该节点有没有被标记为删除,没有的话才把新的节点连接到集合中。这是通过
AtomicMarkableReference
<V>类中的
compareAndSet方法原子地进行的。
remove 方法
链表有可能发生的冲突比较多,另一个问题便是两个线程同时间执行
remove方法。这个问题和同时间执行
addAfter有点类同。现在假设一个集合内有四个元素 A、B、C 和 D,一个线程调用
remove方法去移除元素 B 。它首先确定了 A 和 C 的位置,然后准备解除 A 和 B 的连结,再将 A 和 C 连接起来,实际的移除还未实行,这时这个线程被挂起了。另一个线程亦调用
remove
方法移除 C 元素,它解除 B 和 C 的连结,并把 B 和 D 连接起来,移除操作完成。之后刚才的线程恢复运行,继续执行余下的操作,把 A
和 C 连接起来,这样之前的移除 C 的操作便受到了破坏。最终链表中的元素变成 A-C-D,C 元素没有被移除。所以,我们
remove方法需要确定要移除的元素的
next有没有被改变。例如移除 B 的时候,检查 A 的
next有没有被其他线程更动,以及有没有被标记为已经逻辑地删除。这亦是透过
CAS操作去完成的。
从上文的各种情况可见,我们必须原子地施加某些检查机制,确保数据的一致性。我们现在看看解决这些问题的锁无关链表是如何实现的,这些代码应该和读者在算法书上看到的很不同。以下是它的代码:
清单 4. 锁无关的链表实现
import java.util.concurrent.atomic.*; class Node<T> { AtomicMarkableReference<Node<T>> next; T value; public Node(T value, Node<T> next) { this.next = new AtomicMarkableReference<Node<T>>(next, false); this.value = value; } } class LinkedList<T> { AtomicMarkableReference<Node<T>> head; public LinkedList() { Node<T> headNode = new Node<T>(null, null); head = new AtomicMarkableReference<Node<T>>(headNode, false); } public void addFirst(T value) { addAfter(head.getReference().value, value); } public boolean addAfter(T after, T value) { boolean sucessful = false; while (!sucessful) { boolean found = false; for (Node<T> node = head.getReference(); node != null && !isRemoved(node); node = node.next.getReference()) { if (isEqual(node.value, after) && !node.next.isMarked()) { found = true; Node<T> nextNode = node.next.getReference(); Node<T> newNode = new Node<T>(value, nextNode); sucessful = node.next.compareAndSet(nextNode, newNode, false, false); break; } } if (!found) { return false; } } return true; } public boolean remove(T value) { boolean sucessful = false; while (!sucessful) { boolean found = false; for (Node<T> node = head.getReference(), nextNode = node.next.getReference(); nextNode != null; node = nextNode, nextNode = nextNode.next.getReference()) { if (!isRemoved(nextNode) && isEqual(nextNode.value, value)) { found = true; logicallyRemove(nextNode); sucessful = physicallyRemove(node, nextNode); break; } } if (!found) { return false; } } return true; } void logicallyRemove(Node<T> node) { while (!node.next.attemptMark(node.next.getReference(), true)) { } } boolean physicallyRemove(Node<T> leftNode, Node<T> node) { Node<T> rightNode = node; do { rightNode = rightNode.next.getReference(); } while (rightNode != null && isRemoved(rightNode)); return leftNode.next.compareAndSet(node, rightNode, false, false); } boolean isRemoved(Node<T> node) { return node.next.isMarked(); } boolean isEqual(T arg0, T arg1) { if (arg0 == null) { return arg0 == arg1; } else { return arg0.equals(arg1); } } } |
Node类中的
next成员数据属于
AtomicMarkableReference<V>类,不是
Node<T>,亦不是
AtomicReference<V>。这是因为我们不但需要在
next成员进行
CAS操作,也需要在 next 中加上标记。
AtomicMarkableReference<V>上的标记是以一个
boolean表示的。我们会以设定它为
true来代表一个节点已被逻辑删除,
false则代表这个节点未被逻辑删除。当一个节点被标记为已经逻辑地删除,它的
next数据成员的标记位会被设定成
boolean值
true。另一方面,链表中的
head亦属
AtomicMarkableReference<V>这类型,因为我们也需要进行同样的操作。
addAfter 方法
首先考虑一下
addAfter方法,
addAfter方法的设计必须顾虑到两个线程同时间插入及同时间一个线程插入和一个线程移除的冲突。在一个
for循环中,它遍历集合去寻找
after所位于的节点,并通过调用
getReference()把
after的下一个节点赋值到
nextNode变量中。然后,建立一个新的节点去容纳新加入的元素,它通过
next数据成员和
nextNode变量进行连接。下一步,在
node变量上调用
compareAndSet。不同之处在于它不但比较两个引用是否相同去确保
next数据成员没有被其他线程改变过,它亦会比较
boolean标记位和期望值是否相同。上文提到,
AtomicMarkableReference
<V>类拥有一个标记位,我们利用它去检查一个节点是否已被逻辑地删除了。如果我们发现那个节点已被
logicallyRemove方法标记为已经被逻辑地删除,
compareAndSet方法便会失败,并继续循环寻找下一个符合的节点。若果循环终结后仍未寻找到指定的元素,
a
ddAfter方法便会返回
false以表示由于集合内不存在指定的元素,所以元素无法被加入到集合中。
compareAndSet返回
true便代表元素插入成功,方法便会返回并结束。
addFirst 方法
addFirst方法只是简单地调用
addAfter方法去把一个新的元素插入到集合的开端。
Remove 方法
remove方法在一个循环内寻找要移除元素的前一个节点,然后确定这个节点未被逻辑地移除。确定后,它首先调用
logicallyRemove逻辑地删除节点,然后调用
physicallyRemove物理地删除节点。在
logicallyRemove中,我们调用
AtomicMarkableReference
<V>中的
attemptMark来设定标记位。由于
attemptMark可能会失败,所以要将它放进一个
while循环中,经过
attemptMark设定标记的节点代表该节点已被逻辑地删除。在
physicallyRemove方法中,它首先检查邻近的其他节点是否都已经被标记为逻辑删除,若果已被标记则顺道物理地移除它们。这是通过
compareAndSet完成,
compareAndSet会检查节点是否已被逻辑地删除,以及上一个节点的
next成员未被其他线程更改。这样可以确保两个线程同时调用
remove方法,或者分别同时间调用
remove方法和
a
ddAfter方法的时候,竞争条件不会发生。
ABA 问题
因为
CAS操作比较一个内存位置的内容及一个期望值是否相同,但如果一个内存位置的内容由 A 变成 B,再由 B 变成 A,
CAS
仍然会看作两者相同。不过,一些算法因为需求的关系无法容忍这种行为。当一些内存位置被重用的时候,这个问题便可能会发生。在没有垃圾回收机制的环境
下,ABA 问题需要一些机制例如标记去解决。但由于 JVM 会为我们处理内存管理的问题,故此以上的实现足以避免 ABA 问题的出现。
结束语
以往很多的锁无关数据结构都以 Immutable object 的方式去达致线程安全,这很像 Java 中的
String
,但因为涉及过多的复制操作,令性能低下。但经过十多年,锁无关数据结构已发展得十分成熟,性能并不逊色于传统的实现方式。编写锁无关算法是十分困难的,
但因为数据结构是经常被重用的部分,首先把这个概念应用到数据结构中,可以轻易让程序进入锁无关的世界,体验它所带来的好处。
相关文章推荐
- Java 下实现锁无关数据结构
- Java 下实现锁无关数据结构
- java并发编程5:实现锁无关数据结构
- 【转】Java 下实现锁无关数据结构
- Java 数据结构实现(DataStructure)2
- 数据结构之队列的java实现
- java实现队列数据结构
- 常用数据结构及算法C#/Java实现
- 数据结构(java语言描述)-- 二叉查找树的链式存储结构的实现
- Java数据结构与算法—及实现 线性表 顺序表、链表、栈、队列详解
- 数据结构算法之排序系列Java、C源码实现(3)--直接选择排序
- 数据结构(三)---双向循环链表的实现---java版
- java数据结构之单链表的简单实现
- 【数据结构与算法】数组应用2:矩阵转置(Java实现)
- JSON复杂数据处理之Json树形结构数据转Java对象并存储到数据库的实现
- 【数据结构与算法】二叉树的Java实现及特点总结
- [图解数据结构之Java实现](0) --- 概要分析和目录分类
- 【数据结构与算法】数组应用3:稀疏矩阵压缩(Java实现)
- 算法——数据结构图的最短路径实现JAVA代码
- 数据结构基础 —— 快速排序 java 实现