이 기사는 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
包下的各种原子类,例如AtomicInteger
,AtomicReference
等等。
这些类都支持 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() 메서드에 의존합니다.
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 중국어 웹사이트의 기타 관련 기사를 참조하세요!