在多线程环境中,无锁算法提供了一种线程可以访问共享资源的方式,而没有锁的复杂性,也不会永远阻塞线程。这些算法成为程序员的选择,因为它们提供更高的吞吐量并防止死锁。
这主要是因为设计基于锁的算法,对于并发带来了自己的挑战。编写高效锁和同步以减少线程争用的复杂性并不是每个人都喜欢的。而且,即使编写了复杂的代码,很多时候在生产环境中也会出现难以发现的bug,涉及多线程,解决起来更加困难。
保持这种观点,我们将讨论如何将无锁算法应用于Java广泛使用的数据结构之一,称为堆栈。正如我们所知,堆栈用于许多现实生活中的应用程序,例如字处理器中的撤消/重做功能、表达式评估和语法解析、语言处理中、支持递归以及我们自己的 JVM 是面向堆栈的。因此,让我们深入了解如何编写无锁堆栈。希望它足以点燃您的思想,以进一步阅读并获得有关该主题的知识。
Java的原子类
Java提供了大量支持无锁和线程安全编程的类。 Java提供的 Atomic API, Java.util.concurrent.atomic包包含许多高级类和特性,它们提供并发控制而无需使用锁。 AtomicReference也是 API 中的一个这样的类,它提供对可以原子读写的底层对象引用的引用。通过原子,我们的意思是对这些变量的读取和写入是线程安全的。详情请参阅以下链接。 CAS Inside – CompareAndSwap 操作:
作为无锁算法的基本构建块的最重要的操作是比较和交换。它编译成一个单一的硬件操作,这使得它在同步出现在粒度级别时更快。此外,此操作适用于所有原子类。 CAS 旨在通过将变量/引用与其当前值进行比较来更新变量/引用的值。为非阻塞堆栈应用 CAS:
非阻塞堆栈基本上意味着堆栈的操作对所有线程都可用,并且没有线程被阻塞。为了在堆栈操作中使用 CAS,编写了一个循环,其中使用 CAS 检查堆栈的顶部节点(称为堆栈顶部)的值。如果 stackTop 的值符合预期,则将其替换为新的 top 值,否则没有任何更改,线程再次进入循环。
假设我们有一个整数堆栈。假设,当栈顶值为 90 时,线程 1 想要将值 77 压入堆栈。线程 2 想要弹出栈顶,当前为 90。如果线程 1 尝试访问堆栈并且由于当时没有其他线程访问它而被授予访问权限,则该线程首先获取堆栈顶部的最新值。然后它进入 CAS 循环并使用预期值 (90) 检查堆栈顶部。如果两个值相同,即:CAS 返回 true,这意味着没有其他线程对其进行修改,则将新值(在我们的示例中为 77)压入堆栈。 77 成为新的栈顶。同时,thread2 不断循环 CAS,直到 CAS 返回 true,以从堆栈顶部弹出一个项目。这如下图所示。
非阻塞堆栈的代码示例:
堆栈代码示例如下所示。在此示例中,定义了两个堆栈。一种使用传统同步(此处称为ClassicStack )来实现并发控制的。另一个堆栈使用AtomicReference类的比较和设置操作来建立无锁算法(此处称为LockFreeStack )。在这里,我们计算 Stack 在 1/2 秒的跨度内执行的操作数。我们比较下面两个堆栈的性能:
Java
// Java program to demonstrate Lock-Free
// Stack implementation
import java.io.*;
import java.util.List;
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
class GFG {
public static void main(String[] args)
throws InterruptedException
{
// Defining two stacks
// Uncomment the following line to see the
// standard stack implementation.
// ClassicStack operStack = new
// ClassicStack(); Lock-Free Stack
// definition.
LockFreeStack operStack
= new LockFreeStack();
Random randomIntegerGenerator = new Random();
for (int j = 0; j < 10; j++) {
operStack.push(Integer.valueOf(
randomIntegerGenerator.nextInt()));
}
// Defining threads for Stack Operations
List threads = new ArrayList();
int stackPushThreads = 2;
int stackPopThreads = 2;
for (int k = 0; k < stackPushThreads; k++) {
Thread pushThread = new Thread(() -> {
System.out.println("Pushing into stack...");
while (true) {
operStack.push(Integer.valueOf(
randomIntegerGenerator.nextInt()));
}
});
// making the threads low priority before
// starting them
pushThread.setDaemon(true);
threads.add(pushThread);
}
for (int k = 0; k < stackPopThreads; k++) {
Thread popThread = new Thread(() -> {
System.out.println(
"Popping from stack ...");
while (true) {
operStack.pop();
}
});
popThread.setDaemon(true);
threads.add(popThread);
}
for (Thread thread : threads) {
thread.start();
}
Thread.sleep(500);
System.out.println(
"The number of stack operations performed in 1/2 a second-->"
+ operStack.getNoOfOperations());
}
// Class defining the implementation of Lock Free Stack
private static class LockFreeStack {
// Defining the stack nodes as Atomic Reference
private AtomicReference > headNode
= new AtomicReference >();
private AtomicInteger noOfOperations
= new AtomicInteger(0);
public int getNoOfOperations()
{
return noOfOperations.get();
}
// Push operation
public void push(T value)
{
StackNode newHead = new StackNode(value);
// CAS loop defined
while (true) {
StackNode currentHeadNode
= headNode.get();
newHead.next = currentHeadNode;
// perform CAS operation before setting new
// value
if (headNode.compareAndSet(currentHeadNode,
newHead)) {
break;
}
else {
// waiting for a nanosecond
LockSupport.parkNanos(1);
}
}
// getting the value atomically
noOfOperations.incrementAndGet();
}
// Pop function
public T pop()
{
StackNode currentHeadNode = headNode.get();
// CAS loop defined
while (currentHeadNode != null) {
StackNode newHead = currentHeadNode.next;
if (headNode.compareAndSet(currentHeadNode,
newHead)) {
break;
}
else {
// waiting for a nanosecond
LockSupport.parkNanos(1);
currentHeadNode = headNode.get();
}
}
noOfOperations.incrementAndGet();
return currentHeadNode != null
? currentHeadNode.value
: null;
}
}
// Class defining the implementation
// of a Standard stack for concurrency
private static class ClassicStack {
private StackNode headNode;
private int noOfOperations;
// Synchronizing the operations
// for concurrency control
public synchronized int getNoOfOperations()
{
return noOfOperations;
}
public synchronized void push(T number)
{
StackNode newNode = new StackNode(number);
newNode.next = headNode;
headNode = newNode;
noOfOperations++;
}
public synchronized T pop()
{
if (headNode == null)
return null;
else {
T val = headNode.getValue();
StackNode newHead = headNode.next;
headNode.next = newHead;
noOfOperations++;
return val;
}
}
}
private static class StackNode {
T value;
StackNode next;
StackNode(T value) { this.value = value; }
public T getValue() { return this.value; }
}
}
Java
// Lock Based Stack programming
// This will invoke the lock-based version of the stack.
import java.io.*;
class GFG {
public static void main(String[] args)
{
ClassicStack operStack = new ClassicStack();
// LockFreeStack operStack = new LockFreeStack();
}
}
输出:
Pushing into stack...
Pushing into stack...
Popping from stack ...
Popping from stack ...
The number of stack operations performed in 1/2 a second-->28514750
上述输出是从实现无锁堆栈数据结构中接收到的。我们看到有 4 个不同的线程,2 个用于推送,2 个用于从 Stack 弹出。操作数表示堆栈上的 Pop 或 Push 操作。
为了将其与使用传统同步进行并发的标准堆栈版本进行比较,我们可以取消注释第一行代码并注释第二行代码,如下所示。
Java
// Lock Based Stack programming
// This will invoke the lock-based version of the stack.
import java.io.*;
class GFG {
public static void main(String[] args)
{
ClassicStack operStack = new ClassicStack();
// LockFreeStack operStack = new LockFreeStack();
}
}
基于锁的堆栈的输出如下。它清楚地表明无锁实现(以上)提供了几乎 3 倍的输出。
输出:
Pushing into stack...
Pushing into stack...
Popping from stack ...
Popping from stack ...
The number of stack operations performed in 1/2 a second-->8055597
尽管无锁编程提供了无数的好处,但正确编程并非易事。
优点:
- 真正的无锁编程。
- 死锁预防。
- 更高的吞吐量。
缺点
- ABA 问题仍然可能发生在无锁算法中(这是一个变量的值从 A 到 B 然后返回到 A,同时两个线程正在读取相同的值 A,而另一个线程不知道它)
- 无锁算法可能并不总是很容易编码。
无锁算法和数据结构是Java世界中一个备受争议的话题。在使用基于锁或无锁算法时,必须彻底了解系统。必须非常注意使用它们中的任何一个。对于不同类型的并发问题,没有“一刀切”的解决方案或算法。因此,决定哪种算法最适合某种情况,是多线程世界中编程的关键部分。
参考:
- 并发原子包-摘要
- 锁支持
- Treiber_stack
- 非阻塞算法简介