Wenn wir gleichzeitige Programme schreiben, besteht eine sehr häufige Anforderung darin, sicherzustellen, dass nur ein Thread einen bestimmten Codeabschnitt zu einem bestimmten Zeitpunkt ausführt. Diese Art von Code wird normalerweise als kritischer Abschnitt bezeichnet Es ist garantiert, dass jeweils nur ein Thread vorhanden ist. Die Methode für Threads zum Ausführen von Code in kritischen Abschnitten sind Sperren. In diesem Artikel werden wir Spin-Sperren sorgfältig analysieren und lernen. Die sogenannte Spin-Sperre wird durch eine While-Schleife implementiert, die es dem Thread, der die Sperre erhalten hat, ermöglicht, in den kritischen Abschnitt einzutreten, um den Code auszuführen, und dem Thread ermöglicht Das hat keine Sperre erhalten, um die While-Schleife fortzusetzen. Dies ist eigentlich der Thread, der sich in der While-Schleife „spinnt“, daher wird diese Art von Sperre als Spin-Sperre bezeichnet.
Bevor wir über Spin-Locks sprechen, müssen wir über Atomizität sprechen. Die sogenannte Atomizität bedeutet einfach, dass jeder Vorgang entweder nicht ausgeführt wird oder dass er während des Vorgangs nicht unterbrochen werden kann. Das Hinzufügen eines Vorgangs zu den variablen Daten umfasst beispielsweise die folgenden drei Schritte:
Daten laden vom Gedächtnis zum Register.
Fügen Sie eins zum Wert der Daten hinzu.
Schreiben Sie das Ergebnis zurück ins Gedächtnis.
Atomizität bedeutet, dass ein Thread, wenn er eine Additionsoperation ausführt, nicht von anderen Threads unterbrochen werden kann. Erst wenn dieser Thread diese drei Prozesse abschließt, können andere Threads die Daten verarbeiten.
Lassen Sie es uns jetzt mit Code erleben. In Java können wir AtomicInteger verwenden, um atomare Operationen an ganzzahligen Daten durchzuführen:
import java.util.concurrent.atomic.AtomicInteger; public class AtomicDemo { public static void main(String[] args) throws InterruptedException { AtomicInteger data = new AtomicInteger(); data.set(0); // 将数据初始化位0 Thread t1 = new Thread(() -> { for (int i = 0; i < 100000; i++) { data.addAndGet(1); // 对数据 data 进行原子加1操作 } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 100000; i++) { data.addAndGet(1);// 对数据 data 进行原子加1操作 } }); // 启动两个线程 t1.start(); t2.start(); // 等待两个线程执行完成 t1.join(); t2.join(); // 打印最终的结果 System.out.println(data); // 200000 } }
Aus der obigen Codeanalyse können wir erkennen, dass zwei Threads gleichzeitig arbeiten, wenn es sich um eine allgemeine Ganzzahlvariable handelt Gleichzeitig wird das Endergebnis weniger als 200.000 betragen.
Simulieren wir nun den Prozess von Problemen mit allgemeinen Ganzzahlvariablen:
Der Anfangswert der Daten im Hauptspeicher ist gleich 0, und der Anfangswert der von beiden Threads erhaltenen Daten ist gleich 0.
Jetzt fügt Thread eins einen zu den Daten hinzu, und dann synchronisiert Thread eins den Wert der Daten zurück mit dem Hauptspeicher. Die Datenänderungen im gesamten Speicher sind wie folgt:
Jetzt fügt Thread zwei einen hinzu Daten und synchronisiert dann den Wert der Daten. Rückkehr zum Hauptspeicher (Überschreiben des ursprünglichen Hauptspeicherwerts):
Ursprünglich hatten wir gehofft, dass der Wert der Daten nach den oben genannten Änderungen 2 werden würde, aber Thread zwei hat unseren Wert überschrieben , also in einer Multithread-Situation Down, wird unser Endergebnis kleiner machen.
Aber im obigen Programm ist unser endgültiges Ausgabeergebnis gleich 20000. Dies liegt daran, dass die Operation von +1 für Daten atomar und unteilbar ist und andere Threads während der Operation nicht mit Daten arbeiten können. Dies ist der Vorteil der Atomizität.
AtomicInteger-Klasse
Da wir nun die Rolle der Atomizität verstanden haben, lernen wir nun eine weitere atomare Operation der AtomicInteger-Klasse kennen – CompareAndSet. Diese Operation heißt Compare And Swap (CAS). es ist atomar.
public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(); atomicInteger.set(0); atomicInteger.compareAndSet(0, 1); }
Die Bedeutung der Funktion „compareAndSet“: Zuerst vergleicht sie den ersten Parameter (der dem obigen Code entspricht, ist 0) und den Wert von atomicInteger. Wenn sie gleich sind, werden sie ausgetauscht, dh der Wert von atomicInteger wird auf den zweiten Parameter gesetzt (entsprechend dem obigen Code). Wenn diese Vorgänge erfolgreich sind, gibt die Funktion „true“ zurück Der erste Parameter (erwarteter Wert) ist nicht gleich atomicInteger. Wenn sie gleich sind, kann dies auch daran liegen, dass der Wert von atomicInteger fehlschlägt (weil möglicherweise mehrere Threads in Betrieb sind und Atomizität vorhanden ist). nur ein Thread kann erfolgreich betrieben werden).
Prinzip der Spin-Lock-Implementierung
Wir können die AtomicInteger-Klasse verwenden, um den Spin-Lock zu implementieren. Wir können den Wert 0 verwenden, um anzuzeigen, dass er nicht gesperrt ist, und den Wert 1, um anzuzeigen, dass er gesperrt ist.
Der Anfangswert der AtomicInteger-Klasse ist 0.
Beim Sperren können wir den Code atomicInteger.compareAndSet(0, 1) verwenden, um ihn zu implementieren. Wir haben bereits erwähnt, dass nur ein Thread diesen Vorgang abschließen kann, was bedeutet, dass nur ein Thread diese Codezeile aufrufen und dann zurückkehren kann true und andere Threads, die false zurückgeben, können den kritischen Abschnitt nicht betreten, daher müssen diese Threads bei atomicInteger.compareAndSet(0, 1) angehalten werden. Diese Codezeile kann nicht weiter ausgeführt werden Schleife, um diese Threads anzuhalten. Der Thread stoppt hier während (!value.compareAndSet(0, 1)); nur der Thread, der true zurückgibt, kann aus der Schleife ausbrechen, und die anderen Threads werden hier weiter schleifen Dieses Verhalten ist Spin, und diese Sperre wird daher auch Spin-Lock genannt.
线程在出临界区的时候需要重新将锁的状态调整为未上锁的上状态,我们使用代码value.compareAndSet(1, 0);就可以实现,将锁的状态还原为未上锁的状态,这样其他的自旋的线程就可以拿到锁,然后进入临界区了。
自旋锁代码实现
import java.util.concurrent.atomic.AtomicInteger; public class SpinLock { // 0 表示未上锁状态 // 1 表示上锁状态 protected AtomicInteger value; public SpinLock() { this.value = new AtomicInteger(); // 设置 value 的初始值为0 表示未上锁的状态 this.value.set(0); } public void lock() { // 进行自旋操作 while (!value.compareAndSet(0, 1)); } public void unlock() { // 将锁的状态设置为未上锁状态 value.compareAndSet(1, 0); } }
上面就是我们自己实现的自旋锁的代码,这看起来实在太简单了,但是它确实帮助我们实现了一个锁,而且能够在真实场景进行使用的,我们现在用代码对上面我们写的锁进行测试。
测试程序:
public class SpinLockTest { public static int data; public static SpinLock lock = new SpinLock(); public static void add() { for (int i = 0; i < 100000; i++) { // 上锁 只能有一个线程执行 data++ 操作 其余线程都只能进行while循环 lock.lock(); data++; lock.unlock(); } } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[100]; // 设置100个线程 for (int i = 0; i < 100; i ++) { threads[i] = new Thread(SpinLockTest::add); } // 启动一百个线程 for (int i = 0; i < 100; i++) { threads[i].start(); } // 等待这100个线程执行完成 for (int i = 0; i < 100; i++) { threads[i].join(); } System.out.println(data); // 10000000 } }
在上面的代码单中,我们使用100个线程,然后每个线程循环执行100000data++操作,上面的代码最后输出的结果是10000000,和我们期待的结果是相等的,这就说明我们实现的自旋锁是正确的。
可重入自旋锁
在上面实现的自旋锁当中已经可以满足一些我们的基本需求了,就是一个时刻只能够有一个线程执行临界区的代码。但是上面的的代码并不能够满足重入的需求,也就是说上面写的自旋锁并不是一个可重入的自旋锁,事实上在上面实现的自旋锁当中重入的话就会产生死锁。
我们通过一份代码来模拟上面重入产生死锁的情况:
public static void add(int state) throws InterruptedException { TimeUnit.SECONDS.sleep(1); if (state <= 3) { lock.lock(); System.out.println(Thread.currentThread().getName() + "\t进入临界区 state = " + state); for (int i = 0; i < 10; i++) data++; add(state + 1); // 进行递归重入 重入之前锁状态已经是1了 因为这个线程进入了临界区 lock.unlock(); } }
在上面的代码当中加入我们传入的参数state的值为1,那么在线程执行for循环之后再次递归调用add函数的话,那么state的值就变成了2。
if条件仍然满足,这个线程也需要重新获得锁,但是此时锁的状态是1,这个线程已经获得过一次锁了,但是自旋锁期待的锁的状态是0,因为只有这样他才能够再次获得锁,进入临界区,但是现在锁的状态是1,也就是说虽然这个线程获得过一次锁,但是它也会一直进行while循环而且永远都出不来了,这样就形成了死锁了。
可重入自旋锁思想
针对上面这种情况我们需要实现一个可重入的自旋锁,我们的思想大致如下:
在我们实现的自旋锁当中,我们可以增加两个变量,owner一个用于存当前拥有锁的线程,count一个记录当前线程进入锁的次数。
如果线程获得锁,owner = Thread.currentThread()并且count = 1。
当线程下次再想获取锁的时候,首先先看owner是不是指向自己,则一直进行循环操作,如果是则直接进行count++操作,然后就可以进入临界区了。
我们在出临界区的时候,如果count大于一的话,说明这个线程重入了这把锁,因此不能够直接将锁设置为0也就是未上锁的状态,这种情况直接进行count--操作,如果count等于1的话,说明线程当前的状态不是重入状态(可能是重入之后递归返回了),因此在出临界区之前需要将锁的状态设置为0,也就是没上锁的状态,好让其他线程能够获取锁。
可重入锁代码实现
实现的可重入锁代码如下:
public class ReentrantSpinLock extends SpinLock { private Thread owner; private int count; @Override public void lock() { if (owner == null || owner != Thread.currentThread()) { while (!value.compareAndSet(0, 1)); owner = Thread.currentThread(); count = 1; }else { count++; } } @Override public void unlock() { if (count == 1) { count = 0; value.compareAndSet(1, 0); }else count--; } }
下面我们通过一个递归程序去验证我们写的可重入的自旋锁是否能够成功工作。
测试程序:
import java.util.concurrent.TimeUnit; public class ReentrantSpinLockTest { public static int data; public static ReentrantSpinLock lock = new ReentrantSpinLock(); public static void add(int state) throws InterruptedException { TimeUnit.SECONDS.sleep(1); if (state <= 3) { lock.lock(); System.out.println(Thread.currentThread().getName() + "\t进入临界区 state = " + state); for (int i = 0; i < 10; i++) data++; add(state + 1); lock.unlock(); } } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { threads[i] = new Thread(new Thread(() -> { try { ReentrantSpinLockTest.add(1); } catch (InterruptedException e) { e.printStackTrace(); } }, String.valueOf(i))); } for (int i = 0; i < 10; i++) { threads[i].start(); } for (int i = 0; i < 10; i++) { threads[i].join(); } System.out.println(data); } }
上面程序的输出:
Thread-3 Eintritt in den kritischen Abschnittszustand = 1
Thread-3 Eintritt in den kritischen Abschnittszustand = 2
Thread-3 Eintritt in den kritischen Abschnittszustand = 3
Thread-0 Eintritt in den kritischen Abschnittszustand = 1
Thread-0 Eintritt der Zustand des kritischen Abschnitts = 2
Thread-0 Eintritt in den Zustand des kritischen Abschnitts = 3
Thread-9 Eintritt in den Zustand des kritischen Abschnitts = 1
Thread-9 Eintritt in den Zustand des kritischen Abschnitts = 2
Thread-9 Eintritt in den Zustand des kritischen Abschnitts = 3
Thread-4 Eintritt in den kritischen Abschnittszustand = 1
Thread-4 Eintritt in den kritischen Abschnittszustand = 2
Thread-4 Eintritt in den kritischen Abschnittszustand = 3
Thread-7 Eintritt in den kritischen Abschnittszustand = 1
Thread-7 Eintritt in den Zustand des kritischen Abschnitts = 2
Thread-7 Eintritt in den Zustand des kritischen Abschnitts = 3
Thread-8 Eintritt in den Zustand des kritischen Abschnitts = 1
Thread-8 Eintritt in den Zustand des kritischen Abschnitts = 2
Thread-8 Eintritt in den Zustand des kritischen Abschnitts = 3
Thread-5 Eintritt in den kritischen Abschnittszustand = 1
Thread-5 Eintritt in den kritischen Abschnittszustand = 2
Thread-5 Eintritt in den kritischen Abschnittszustand = 3
Thread-2 Eintritt in den kritischen Abschnittszustand = 1
Thread-2 Eintritt in den kritischen Abschnitt Abschnittsstatus = 2
Thread-2 Eintritt in den kritischen Abschnittsstatus = 3
Thread-6 Eintritt in den kritischen Abschnittsstatus = 1
Thread-6 Eintritt in den kritischen Abschnittsstatus = 2
Thread-6 Eintritt in den kritischen Abschnittsstatus = 3
Thread -1 Eintritt in den kritischen Abschnittszustand = 1
Thread-1 Eintritt in den kritischen Abschnittszustand = 2
Thread-1 Eintritt in den kritischen Abschnittszustand = 3
300
Aus den obigen Ausgabeergebnissen können wir erkennen, wann ein Thread dies kann Wenn Sie die Sperre erwerben, kann sie erneut eingegeben werden, und das endgültige Ausgabeergebnis ist ebenfalls korrekt. Daher wird überprüft, ob wir einen wiedereintrittsfähigen Spin geschrieben haben. Die Sperre funktioniert!
Das obige ist der detaillierte Inhalt vonSo implementieren Sie eine manuelle Spin-Sperre mit Java. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!