Heim  >  Artikel  >  Java  >  Eine kurze Einführung in die Thread-Sicherheitsimplementierung in Java und das CLH-Warteschlangenprinzip (Codebeispiel)

Eine kurze Einführung in die Thread-Sicherheitsimplementierung in Java und das CLH-Warteschlangenprinzip (Codebeispiel)

不言
不言Original
2018-09-17 15:23:072003Durchsuche

Dieser Artikel bietet Ihnen eine kurze Einführung (Codebeispiel) zur Thread-Sicherheitsimplementierung in Java und zum CLH-Warteschlangenprinzip. Ich hoffe, dass er für Sie hilfreich ist . helfen.

Blockierung der Synchronisierung

In Java verwenden wir häufig das Schlüsselwort „synced“, um eine sich gegenseitig ausschließende Synchronisierung zu erreichen und Probleme mit der Parallelität mehrerer Threads zu lösen Zugriff auf freigegebene Daten. Nachdem das Schlüsselwort „synchronzied“ kompiliert wurde, werden zwei Bytecode-Anweisungen, „monitorenter“ und „monitorexit“, vor und nach dem in „synchized“ enthaltenen Synchronisationscodeblock hinzugefügt. Das synchronisierte Schlüsselwort erfordert die Angabe eines Objekts zum Sperren und Entsperren. Beispiel:

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
        }
    }
}

Wenn das Objekt nicht explizit angegeben ist, bestimmt die Frage, ob die synchronisierte Änderung eine Instanzmethode oder eine statische Methode ist, ob die Objektinstanz oder die Klasseninstanz der Klasse als Objekt verwendet werden soll. Beispiel:

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

Aufgrund des auf der Synchronisierung implementierten blockierenden Mutex muss zusätzlich zum Blockieren des Betriebsthreads auch der native Thread auf Betriebssystemebene aktiviert oder blockiert werden und von dort aus übergegangen werden Die Konvertierung dieses Zustands in den Kernel-Zustand dauert möglicherweise länger als die Ausführungszeit des Benutzercodes. Daher sagen wir oft, dass synchronisiert eine „Schwergewichtssperre“ in der Java-Sprache ist.

Nicht blockierende Synchronisierung

Optimistische Sperre und pessimistische Sperre

Synchronisierung mit dem synchronisierten Schlüsselwort Das Hauptproblem bei dieser Methode ist der Leistungsverbrauch, der durch das Blockieren und Aufwecken des Threads verursacht wird. Das Blockieren der Synchronisierung ist eine pessimistische Parallelitätsstrategie. Solange die Möglichkeit eines Wettbewerbs besteht, wird davon ausgegangen, dass eine Sperre durchgeführt werden muss.
Die Synchronisationsstrategie hat jedoch auch eine andere optimistische Strategie. Die optimistische Parallelitätsstrategie fördert die Datenoperation. Wenn keine anderen Threads gefunden werden, die die Daten verarbeitet haben, gilt die Operation als erfolgreich. Wenn andere Threads ebenfalls Daten bearbeiten, versuchen Sie es im Allgemeinen kontinuierlich bis zum Erfolg. Diese optimistische Sperrstrategie erfordert keine blockierenden Threads und ist ein Mittel zur nicht blockierenden Synchronisierung.

CAS

Die optimistische Parallelitätsstrategie besteht hauptsächlich aus zwei wichtigen Phasen: Eine besteht darin, mit Daten zu arbeiten, und die andere darin, Konflikte zu erkennen, dh zu erkennen, ob Andere Threads haben ebenfalls Operationen an diesen Daten durchgeführt. Die Datenoperationen und die Konflikterkennung müssen hier atomar sein, sonst treten leicht ähnliche Probleme wie bei i++ auf.
CAS bedeutet Vergleichen und Tauschen. Derzeit unterstützen die meisten CPUs nativ atomare CAS-Anweisungen. In den Befehlssätzen von IA64 und x86 gibt es beispielsweise Anweisungen wie cmpxchg, um die CAS-Funktion zu vervollständigen die Hardware-Ebene.
Der CAS-Befehl erfordert im Allgemeinen drei Parameter, nämlich die Speicheradresse des Werts, den erwarteten alten Wert und den neuen Wert. Wenn der CAS-Befehl ausgeführt wird und der Wert an der Speicheradresse mit dem erwarteten alten Wert übereinstimmt, aktualisiert der Prozessor den Wert an der Speicheradresse mit dem neuen Wert, andernfalls wird er nicht aktualisiert. Dieser Vorgang ist innerhalb der CPU garantiert atomar.
Es gibt viele CAS-bezogene APIs in Java. Die häufigsten sind verschiedene atomare Klassen im java.util.concurrent-Paket, wie z. B. AtomicInteger, AtomicReference usw.
Diese Klassen unterstützen alle CAS-Operationen und basieren tatsächlich intern auf den Methoden "compareAndSwapInt()" und "compareAndSwapLong()" in sun.misc.Unsafe dieser Klasse.
CAS ist nicht perfekt, obwohl es Atomizität garantieren kann, leidet es unter dem berühmten ABA-Problem. Der Wert einer Variablen ist A, wenn sie zum ersten Mal gelesen wird, und er ist auch A, wenn sie erneut gelesen wird. Können wir erklären, dass sich diese Variable zwischen den beiden Lesevorgängen nicht geändert hat? kann nicht. Während dieses Zeitraums kann sich die Variable von A nach B und dann von B nach A ändern. Beim zweiten Lesen sehen Sie A, aber tatsächlich hat sich die Variable geändert. Die allgemeine Codelogik kümmert sich nicht um dieses ABA-Problem, da es gemäß der Codelogik keinen Einfluss auf die Sicherheit der Parallelität hat. Wenn Sie sich jedoch darum kümmern, können Sie die Verwendung von Blocksynchronisation anstelle von CAS in Betracht ziehen. Tatsächlich bietet das JDK selbst auch eine Lösung für dieses ABA-Problem, indem es die Klasse AtomicStampedReference bereitstellt, um Versionen zu Variablen hinzuzufügen, um das ABA-Problem zu lösen.

Spin-Sperre

Blockierte Synchronisierung, dargestellt durch synchronisiert, da der blockierte Thread den Thread-Betrieb wieder aufnimmt, der den Benutzermodus und den Kernelmodus auf Betriebssystemebene umfassen muss Der Wechsel zwischen ihnen hat einen großen Einfluss auf die Leistung des Systems. Die Strategie der Spin-Sperre besteht darin, dass ein Thread, wenn er eine Sperre erhält und feststellt, dass die Sperre bereits von einem anderen Thread belegt ist, die Ausführungszeitscheibe der CPU nicht sofort aufgibt, sondern in eine „bedeutungslose“ Schleife eintritt Sehen Sie nach, ob der Thread „Die Sperre“ aufgegeben wurde.
Aber die Spin-Sperre eignet sich für kritische Abschnitte und ist relativ klein. Wenn die Sperre zu lange gehalten wird, wird die Leistung des Systems durch den Spin-Vorgang selbst beeinträchtigt.

Das Folgende ist eine einfache Spin-Lock-Implementierung:

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 框架实现的核心原理。

Das obige ist der detaillierte Inhalt vonEine kurze Einführung in die Thread-Sicherheitsimplementierung in Java und das CLH-Warteschlangenprinzip (Codebeispiel). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn