Seit Java 5 enthält das Paket java.util.concurrent.locks einige Sperrimplementierungen, sodass Sie keine eigenen Sperren implementieren müssen. Sie müssen jedoch noch verstehen, wie diese Schlösser verwendet werden.
Eine einfache Sperre
Beginnen wir mit einem synchronisierten Block in Java:
public class Counter{ private int count = 0; public int inc(){ synchronized(this){ return ++count; } } }
Sie können sehen, dass es in inc() ein synchronisiertes(this) gibt Methode) Codeblock. Dieser Codeblock garantiert, dass nur ein Thread gleichzeitig return ++count ausführen kann. Obwohl der Code im synchronisierten Synchronisationsblock komplexer sein kann, reicht eine einfache Operation wie ++count aus, um die Bedeutung der Thread-Synchronisation auszudrücken.
Die folgende Counter-Klasse verwendet Lock anstelle von synchronisiert, um den gleichen Zweck zu erreichen:
public class Counter{ private Lock lock = new Lock(); private int count = 0; public int inc(){ lock.lock(); int newCount = ++count; lock.unlock(); return newCount; } }
Die lock()-Methode sperrt das Lock-Instanzobjekt, sodass alle Aufrufe von lock() aktiviert sind Der Methodenthread des Objekts wird blockiert, bis die Methode unlock() des Lock-Objekts aufgerufen wird.
Hier ist eine einfache Implementierung der Lock-Klasse:
public class Counter{ public class Lock{ private boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } public synchronized void unlock(){ isLocked = false; notify(); } }
Achten Sie auf die while(isLocked)-Schleife, die auch „Spin Lock“ genannt wird. Wenn isLocked wahr ist, blockiert der Thread, der lock() aufruft, das Warten auf den Aufruf von wait(). Um zu verhindern, dass der Thread von wait() zurückkehrt, ohne den notify()-Aufruf zu erhalten (auch als falsches Aufwecken bezeichnet), überprüft der Thread die isLocked-Bedingung erneut, um festzustellen, ob die Fortsetzung der Ausführung sicher ist oder nicht Es wird nicht davon ausgegangen, dass der Thread nach dem Aufwecken die Ausführung sicher fortsetzen kann. Wenn isLocked „false“ ist, verlässt der aktuelle Thread die while(isLocked)-Schleife und setzt isLocked wieder auf „true“, sodass andere Threads, die die lock()-Methode aufrufen, die Lock-Instanz sperren können.
Wenn der Thread den Code im kritischen Abschnitt (zwischen lock() und unlock()) vervollständigt, wird unlock() aufgerufen. Durch die Ausführung von unlock() wird isLocked auf „false“ zurückgesetzt und einer der Threads (falls vorhanden) benachrichtigt (reaktiviert), der sich nach dem Aufruf der Funktion „wait()“ in der Methode „lock()“ im Wartezustand befand.
Wiedereintritt von Sperren
Synchronisierte synchronisierte Blöcke in Java sind wiedereintrittsfähig. Dies bedeutet, dass, wenn ein Java-Thread einen synchronisierten synchronisierten Block in den Code eingibt und somit die Sperre auf dem Monitor erhält, die dem vom synchronisierten Block verwendeten Synchronisationsobjekt entspricht, dieser Thread in einen anderen synchronisierten Block eintreten kann Java-Code. Hier ist ein Beispiel:
public class Reentrant{ public synchronized outer(){ inner(); } public synchronized inner(){ //do something } }
Beachten Sie, dass sowohl Outer() als auch Inner() als synchronisiert deklariert sind, was dem synchronisierten Block (this) in Java entspricht. Wenn ein Thread „outer()“ aufruft, gibt es kein Problem beim Aufrufen von „inner()“ innerhalb von „outer()“, da beide Methoden (Codeblöcke) durch dasselbe Monitorobjekt („this“) synchronisiert werden. Wenn ein Thread bereits eine Sperre für ein Monitorobjekt besitzt, hat er Zugriff auf alle vom Monitorobjekt synchronisierten Codeblöcke. Das ist Wiedereintritt. Ein Thread kann jeden Codeblock betreten, der durch eine Sperre synchronisiert wird, die er bereits besitzt.
Die zuvor angegebene Sperrimplementierung ist nicht wiedereintrittsfähig. Wenn wir die Reentrant-Klasse wie folgt umschreiben, wird der Thread, wenn er Outer() aufruft, bei lock.lock() der Inner()-Methode blockiert.
public class Reentrant2{ Lock lock = new Lock(); public outer(){ lock.lock(); inner(); lock.unlock(); } public synchronized inner(){ lock.lock(); //do something lock.unlock(); } }
Der Thread, der Outer() aufruft, sperrt zuerst die Lock-Instanz und ruft dann weiterhin Inner() auf. In der inner()-Methode versucht der Thread erneut, die Lock-Instanz zu sperren, aber die Aktion schlägt fehl (d. h. der Thread wird blockiert), da die Lock-Instanz bereits in der external()-Methode gesperrt wurde.
Unlock() wird zwischen den beiden lock()-Zeiten nicht aufgerufen und der zweite Aufruf von lock blockiert. Nachdem Sie sich die Implementierung von lock() angesehen haben, werden Sie feststellen, dass der Grund offensichtlich ist:
public class Lock{ boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } ... }
Ob ein Thread die lock()-Methode verlassen darf, wird durch die Bedingungen in der while-Schleife (Spin-Lock) bestimmt. Die aktuelle Beurteilungsbedingung ist, dass der Sperrvorgang nur dann zulässig ist, wenn isLocked falsch ist, unabhängig davon, welcher Thread ihn gesperrt hat.
Um diese Lock-Klasse wiedereintrittsfähig zu machen, müssen wir eine kleine Änderung daran vornehmen:
public class Lock{ boolean isLocked = false; Thread lockedBy = null; int lockedCount = 0; public synchronized void lock() throws InterruptedException{ Thread callingThread = Thread.currentThread(); while(isLocked && lockedBy != callingThread){ wait(); } isLocked = true; lockedCount++; lockedBy = callingThread; } public synchronized void unlock(){ if(Thread.curentThread() == this.lockedBy){ lockedCount--; if(lockedCount == 0){ isLocked = false; notify(); } } } ... }
Beachten Sie, dass auch die aktuelle while-Schleife (Spin-Lock) berücksichtigt wird Thread, der diese Lock-Instanz gesperrt hat. Wenn das aktuelle Sperrobjekt nicht gesperrt ist (isLocked = false) oder der aktuell aufrufende Thread die Lock-Instanz bereits gesperrt hat, wird die while-Schleife nicht ausgeführt und der Thread, der lock() aufruft, kann die Methode verlassen (Übersetzungshinweis: „Diese Methode verlassen dürfen“ bedeutet in der aktuellen Semantik, dass wait() nicht aufgerufen wird und eine Blockierung verursacht.
Außerdem müssen wir aufzeichnen, wie oft derselbe Thread wiederholt ein Sperrobjekt sperrt. Andernfalls wird ein einzelner unblock()-Aufruf die gesamte Sperre entsperren, selbst wenn die aktuelle Sperre mehrmals gesperrt wurde. Wir möchten nicht, dass die Sperre aufgehoben wird, bevor der unlock()-Aufruf die entsprechende Anzahl von lock()-Aufrufen erreicht hat.
Die Lock-Klasse ist jetzt wiedereintrittsfähig.
Sperrengerechtigkeit
Die synchronisierten Blöcke von Java garantieren nicht die Reihenfolge der Threads, die versuchen, sie zu betreten. Wenn daher mehrere Threads ständig um den Zugriff auf denselben synchronisierten Block konkurrieren, besteht die Gefahr, dass einer oder mehrere von ihnen niemals Zugriff erhalten – das heißt, der Zugriff wird immer anderen Threads zugewiesen. Diese Situation wird als Thread-Starvation bezeichnet. Um dieses Problem zu vermeiden, müssen Sperren fair sein. Die in diesem Artikel vorgestellten Sperren werden intern mithilfe synchronisierter Blöcke implementiert und garantieren daher keine Fairness.
在 finally 语句中调用 unlock()
如果用 Lock 来保护临界区,并且临界区有可能会抛出异常,那么在 finally 语句中调用 unlock()就显得非常重要了。这样可以保证这个锁对象可以被解锁以便其它线程能继续对其加锁。以下是一个示例:
lock.lock(); try{ //do critical section code, //which may throw exception } finally { lock.unlock(); }
这个简单的结构可以保证当临界区抛出异常时 Lock 对象可以被解锁。如果不是在 finally 语句中调用的 unlock(),当临界区抛出异常时,Lock 对象将永远停留在被锁住的状态,这会导致其它所有在该 Lock 对象上调用 lock()的线程一直阻塞。
以上就是关于 java 多线程锁的资料整理,后续继续补充相关资料,谢谢大家对本站的支持!
更多java 多线程-锁详解及示例代码相关文章请关注PHP中文网!