>  기사  >  Java  >  Java 및 CLH 대기열 원칙의 스레드 안전 구현에 대한 간략한 소개(코드 예)

Java 및 CLH 대기열 원칙의 스레드 안전 구현에 대한 간략한 소개(코드 예)

不言
不言원래의
2018-09-17 15:23:072004검색

이 기사는 Java 및 CLH 대기열 원칙의 스레드 안전 구현에 대한 간략한 소개(코드 예제)를 제공합니다. 이는 특정 참조 가치가 있으므로 도움이 될 수 있습니다.

동기화 차단

Java에서는 여러 스레드의 공유 데이터에 대한 동시 액세스 문제를 해결하기 위해 상호 배타적인 동기화를 달성하기 위해 동기화 키워드를 자주 사용합니다. synchronzied 키워드가 컴파일된 후 두 개의 바이트코드 명령어인 monitorenter 및 monitorexit가 synchized에 포함된 동기화 코드 블록 앞뒤에 추가됩니다. 동기화 키워드를 사용하려면 잠금 및 잠금 해제를 위한 개체를 지정해야 합니다. 예를 들면 다음과 같습니다.

public class Main {

    private static final Object LOCK = new Object();
    
    public static void fun1() {
        synchronized (LOCK) {
            // do something
        }
    }
    
    public static void fun2() {
        synchronized (LOCK) {
            // do something
        }
    }
}

객체가 명시적으로 지정되지 않은 경우 동기화된 수정이 인스턴스 메서드인지 정적 메서드인지 여부에 따라 객체 인스턴스를 사용할지 클래스의 클래스 인스턴스를 객체로 사용할지 결정됩니다. 예:

public class SynchronizedTest {
    public synchronized void doSomething() {
        //采用实例对象作为锁对象
    }
}
public class SynchronizedTest {
    public static synchronized void doSomething() {
        //采用SynchronizedTest.class 实例作为锁对象
    }
}

동기화 기반으로 구현된 차단 뮤텍스로 인해 작동 스레드를 차단하는 것 외에도 운영 체제 수준에서 기본 스레드를 깨우거나 차단해야 하며 사용자 모드에서 커널로 전환해야 합니다. 이 상태 전환에 소요되는 시간은 사용자 코드의 실행 시간보다 길 수 있으므로 Java 언어에서는 동기화를 "무거운 잠금"이라고 자주 말합니다.

비차단 동기화

낙관적 잠금과 비관적 잠금

동기화 키워드를 사용한 동기화의 주요 문제점은 스레드 차단 및 깨우기로 인한 성능 소모입니다. 동기화 차단은 경쟁 가능성이 있는 한 잠금을 수행해야 한다고 믿는 비관적인 동시성 전략입니다.
그러나 동기화 전략에는 또 다른 낙관적 전략이 있습니다. 낙관적 동시성 전략은 데이터 작업을 진행하는 데 다른 스레드도 해당 데이터에서 작업한 것이 발견되지 않으면 작업이 성공한 것으로 간주됩니다. 다른 스레드도 데이터를 조작하는 경우 일반적으로 성공할 때까지 계속해서 재시도합니다. 이 낙관적 잠금 전략은 차단 스레드가 필요하지 않으며 비차단 동기화 수단입니다.

CAS

낙관적 동시성 전략은 주로 두 가지 중요한 단계로 구성됩니다. 하나는 데이터를 작동하는 것이고, 다른 하나는 충돌을 감지하는 것, 즉 다른 스레드도 데이터에 대해 작동하는지 여부를 감지하는 것입니다. 여기서 데이터 작업 및 충돌 감지에는 원자성이 필요합니다. 그렇지 않으면 i++와 유사한 문제가 쉽게 발생합니다.
CAS는 비교 및 ​​교환을 의미합니다. 현재 대부분의 CPU는 기본적으로 CAS 원자성 명령을 지원합니다. 예를 들어 IA64 및 x86의 명령 세트에는 CAS 기능을 완료하기 위한 cmpxchg와 같은 명령이 있습니다. .
CAS 명령에는 일반적으로 값의 메모리 주소, 예상되는 이전 값 및 새 값이라는 세 가지 매개변수가 필요합니다. CAS 명령어가 실행될 때 메모리 주소의 값이 예상되는 이전 값과 일치하면 프로세서는 메모리 주소의 값을 새 값으로 업데이트하고, 그렇지 않으면 업데이트되지 않습니다. 이 작업은 CPU 내에서 원자성이 보장됩니다.
Java에는 CAS 관련 API가 많이 있습니다. 가장 일반적인 API에는 AtomicInteger, 와 같은 <code>java.util.concurrent 패키지 아래에 다양한 원자 클래스가 포함됩니다. >AtomicReference등. java.util.concurrent 包下的各种原子类,例如AtomicIntegerAtomicReference等等。
这些类都支持 CAS 操作,其内部实际上也依赖于 sun.misc.Unsafe 这个类里的 compareAndSwapInt() 和 compareAndSwapLong() 方法。
CAS 并非是完美无缺的,尽管它能保证原子性,但它存在一个著名的 ABA 问题。一个变量初次读取的时候值为 A,再一次读取的时候也为 A,那么我们是否能说明这个变量在两次读取中间没有发生过变化?不能。在这期间,变量可能由 A 变为 B,再由 B 变为 A,第二次读取的时候看到的是 A,但实际上这个变量发生了变化。一般的代码逻辑不会在意这个 ABA 问题,因为根据代码逻辑它不会影响并发的安全性,但如果在意的话,可能考虑采用阻塞同步的方式而不是 CAS。实际上 JDK 本身也对这个 ABA 问题解决方案,提供了 AtomicStampedReference이러한 클래스는 모두 CAS 작업을 지원하며 내부적으로는 실제로 sun.misc.Unsafe 클래스의 CompareAndSwapInt() 및 CompareAndSwapLong() 메서드에 의존합니다.

CAS는 원자성을 보장하지만 그 유명한

ABA 문제를 안고 있습니다. 변수의 값은 처음 읽을 때 A이고, 다시 읽을 때에도 A입니다. 이 변수는 두 번의 읽기 사이에 변경되지 않았다고 설명할 수 있습니까? 할 수 없습니다. 이 기간 동안 변수는 A에서 B로, 그리고 B에서 A로 바뀔 수 있습니다. 두 번째 읽을 때는 A로 보이지만 실제로는 변수가 변경되었습니다. 일반 코드 로직에서는 이 ABA 문제를 신경 쓰지 않습니다. 코드 로직에 따르면 동시성 보안에 영향을 주지 않기 때문입니다. 하지만 관심이 있는 경우 CAS 대신 차단 동기화를 사용하는 것을 고려할 수 있습니다. 실제로 JDK 자체도 ABA 문제를 해결하기 위해 변수에 버전을 추가하는 AtomicStampedReference 클래스를 제공하여 이 ABA 문제에 대한 솔루션을 제공합니다.

스핀 잠금
동기화로 표시되는 차단 동기화. 차단된 스레드가 스레드 작업을 재개하기 때문입니다. 이를 위해서는 운영 체제 수준에서 사용자 모드와 커널 모드 간 전환이 필요하며 이는 시스템 성능에 큰 영향을 미칩니다. 큰. 스핀 잠금의 전략은 스레드가 잠금을 획득할 때 다른 스레드가 이미 잠금을 차지하고 있음을 발견하면 즉시 CPU의 실행 시간 조각을 포기하지 않고 "의미 없는" 루프에 들어가는 것입니다. 스레드 잠금이 포기되었는지 확인하십시오.

그러나 스핀 잠금은

중요 섹션

이 상대적으로 작은 상황에 적합합니다. 잠금을 너무 오랫동안 유지하면 스핀 작업 자체가 시스템 성능을 낭비하게 됩니다. 🎜🎜다음은 간단한 스핀 잠금 구현입니다. 🎜
import java.util.concurrent.atomic.AtomicReference;
public class SpinLock {
   private AtomicReference<Thread> owner = new AtomicReference<Thread>();
   public void lock() {
       Thread currentThread = Thread.currentThread();
        // 如果锁未被占用,则设置当前线程为锁的拥有者
       while (!owner.compareAndSet(null, currentThread)) {}
   }

   public void unlock() {
       Thread currentThread = Thread.currentThread();
        // 只有锁的拥有者才能释放锁
       owner.compareAndSet(currentThread, null);
   }
}

上述的代码中, owner 变量保存获得了锁的线程。这里的自旋锁有一些缺点,第一个是没有保证公平性,等待获取锁的线程之间,无法按先后顺序分别获得锁;另一个,由于多个线程会去操作同一个变量 owner,在 CPU 的系统中,存在着各个 CPU 之间的缓存数据需要同步,保证一致性,这会带来性能问题。

公平的自旋

为了解决公平性问题,可以让每个锁拥有一个服务号,表示正在服务的线程,而每个线程尝试获取锁之前需要先获取一个排队号,然后不断轮询当前锁的服务号是否是自己的服务号,如果是,则表示获得了锁,否则就继续轮询。下面是一个简单的实现:

import java.util.concurrent.atomic.AtomicInteger;

public class TicketLock {
   private AtomicInteger serviceNum = new AtomicInteger(); // 服务号
   private AtomicInteger ticketNum = new AtomicInteger(); // 排队号

   public int lock() {
       // 首先原子性地获得一个排队号
       int myTicketNum = ticketNum.getAndIncrement();
       // 只要当前服务号不是自己的就不断轮询
       while (serviceNum.get() != myTicketNum) {
       }
       return myTicketNum;
    }

    public void unlock(int myTicket) {
        // 只有当前线程拥有者才能释放锁
        int next = myTicket + 1;
        serviceNum.compareAndSet(myTicket, next);
    }
}

虽然解决了公平性的问题,但依然存在前面说的多 CPU 缓存的同步问题,因为每个线程占用的 CPU 都在同时读写同一个变量 serviceNum,这会导致繁重的系统总线流量和内存操作次数,从而降低了系统整体的性能。

MCS 自旋锁

MCS 的名称来自其发明人的名字:John Mellor-Crummey和Michael Scott。
MCS 的实现是基于链表的,每个申请锁的线程都是链表上的一个节点,这些线程会一直轮询自己的本地变量,来知道它自己是否获得了锁。已经获得了锁的线程在释放锁的时候,负责通知其它线程,这样 CPU 之间缓存的同步操作就减少了很多,仅在线程通知另外一个线程的时候发生,降低了系统总线和内存的开销。实现如下所示:

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class MCSLock {
    public static class MCSNode {
        volatile MCSNode next;
        volatile boolean isWaiting = true; // 默认是在等待锁
    }
    volatile MCSNode queue;// 指向最后一个申请锁的MCSNode
    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater
            .newUpdater(MCSLock.class, MCSNode.class, "queue");

    public void lock(MCSNode currentThread) {
        MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1
        if (predecessor != null) {
            predecessor.next = currentThread;// step 2
            while (currentThread.isWaiting) {// step 3
            }
        } else { // 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己已获得锁
            currentThread.isWaiting = false;
        }
    }

    public void unlock(MCSNode currentThread) {
        if (currentThread.isWaiting) {// 锁拥有者进行释放锁才有意义
            return;
        }

        if (currentThread.next == null) {// 检查是否有人排在自己后面
            if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
                // compareAndSet返回true表示确实没有人排在自己后面
                return;
            } else {
                // 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
                // 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
                while (currentThread.next == null) { // step 5
                }
            }
        }
        currentThread.next.isWaiting = false;
        currentThread.next = null;// for GC
    }
}

MCS 的能够保证较高的效率,降低不必要的性能消耗,并且它是公平的自旋锁。

CLH 自旋锁

CLH 锁与 MCS 锁的原理大致相同,都是各个线程轮询各自关注的变量,来避免多个线程对同一个变量的轮询,从而从 CPU 缓存一致性的角度上减少了系统的消耗。
CLH 锁的名字也与他们的发明人的名字相关:Craig,Landin and Hagersten。
CLH 锁与 MCS 锁最大的不同是,MCS 轮询的是当前队列节点的变量,而 CLH 轮询的是当前节点的前驱节点的变量,来判断前一个线程是否释放了锁。
实现如下所示:

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class CLHLock {
    public static class CLHNode {
        private volatile boolean isWaiting = true; // 默认是在等待锁
    }
    private volatile CLHNode tail ;
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
            . newUpdater(CLHLock.class, CLHNode .class , "tail" );
    public void lock(CLHNode currentThread) {
        CLHNode preNode = UPDATER.getAndSet( this, currentThread);
        if(preNode != null) {//已有线程占用了锁,进入自旋
            while(preNode.isWaiting ) {
            }
        }
    }

    public void unlock(CLHNode currentThread) {
        // 如果队列里只有当前线程,则释放对当前线程的引用(for GC)。
        if (!UPDATER .compareAndSet(this, currentThread, null)) {
            // 还有后续线程
            currentThread.isWaiting = false ;// 改变状态,让后续线程结束自旋
        }
    }
}

从上面可以看到,MCS 和 CLH 相比,CLH 的代码比 MCS 要少得多;CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋;CLH的队列是隐式的,通过轮询关注上一个节点的某个变量,隐式地形成了链式的关系,但CLHNode并不实际持有下一个节点,MCS的队列是物理存在的,而 CLH 的队列是逻辑上存在的;此外,CLH 锁释放时只需要改变自己的属性,MCS 锁释放则需要改变后继节点的属性。

CLH 队列是 J.U.C 中 AQS 框架实现的核心原理。

위 내용은 Java 및 CLH 대기열 원칙의 스레드 안전 구현에 대한 간략한 소개(코드 예)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.