📜  使用Java锁定自由堆栈

📅  最后修改于: 2021-09-07 03:26:22             🧑  作者: Mango

在多线程环境中,无锁算法提供了一种线程可以访问共享资源的方式,而没有锁的复杂性,也不会永远阻塞线程。这些算法成为程序员的选择,因为它们提供更高的吞吐量并防止死锁。

这主要是因为设计基于锁的算法,对于并发带来了自己的挑战。编写高效锁和同步以减少线程争用的复杂性并不是每个人都喜欢的。而且,即使编写了复杂的代码,很多时候在生产环境中也会出现难以发现的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,以从堆栈顶部弹出一个项目。这如下图所示。

为非阻塞堆栈应用 CAS

非阻塞堆栈的代码示例:

堆栈代码示例如下所示。在此示例中,定义了两个堆栈。一种使用传统同步(此处称为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
  • 非阻塞算法简介