Depuis Java 5, le package java.util.concurrent.locks inclut certaines implémentations de verrous, vous n'avez donc pas besoin d'implémenter vos propres verrous. Mais encore faut-il comprendre comment utiliser ces verrous.
Un simple verrou
Commençons par un bloc synchronisé en java :
public class Counter{ private int count = 0; public int inc(){ synchronized(this){ return ++count; } } }
Vous pouvez voir qu'il y a un synchronisé(this) dans le inc() méthode ) bloc de code. Ce bloc de code garantit qu'un seul thread peut exécuter le nombre de retours en même temps. Bien que le code du bloc de synchronisation synchronisée puisse être plus complexe, une simple opération comme count suffit pour exprimer la signification de la synchronisation des threads.
La classe Counter suivante utilise Lock au lieu de synchronisé pour atteindre le même objectif :
public class Counter{ private Lock lock = new Lock(); private int count = 0; public int inc(){ lock.lock(); int newCount = ++count; lock.unlock(); return newCount; } }
La méthode lock() verrouillera l'objet d'instance Lock, donc tous les appels à lock() sur le thread de la méthode object ) sera bloqué jusqu'à ce que la méthode unlock() de l'objet Lock soit appelée.
Voici une implémentation simple de la classe Lock :
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(); } }
Faites attention à la boucle while(isLocked), également appelée "spin lock". Lorsque isLocked est vrai, le thread appelant lock() bloque l'attente de l'appel wait(). Afin d'empêcher le thread de revenir de wait() sans recevoir l'appel notify() (également appelé faux réveil), le thread revérifiera la condition isLocked pour déterminer s'il est sûr de poursuivre l'exécution ou s'il doit attendre à nouveau et il n'est pas considéré que le thread peut continuer son exécution en toute sécurité après son réveil. Si isLocked est faux, le thread actuel quittera la boucle while(isLocked) et redéfinira isLocked sur true, permettant ainsi aux autres threads qui appellent la méthode lock() de verrouiller l'instance de Lock.
Lorsque le thread complète le code dans la section critique (entre lock() et unlock()), unlock() sera appelé. L'exécution de unlock() réinitialisera isLocked à false et avertira (réveillera) l'un des threads (le cas échéant) qui était en état d'attente après avoir appelé la fonction wait() dans la méthode lock().
Réentrée des verrous
Les blocs synchronisés synchronisés en Java sont réentrants. Cela signifie que si un thread Java entre dans le code un bloc synchronisé et obtient ainsi le verrou sur le moniteur correspondant à l'objet de synchronisation utilisé par le bloc synchronisé, alors ce thread peut entrer un autre bloc synchronisé par le même objet moniteur. code java. Voici un exemple :
public class Reentrant{ public synchronized outer(){ inner(); } public synchronized inner(){ //do something } }
Notez que external() et inner() sont déclarés synchronisés, ce qui équivaut au bloc synchronisé(this) en Java. Si un thread appelle external(), il n'y a aucun problème à appeler inner() dans external(), car les deux méthodes (blocs de code) sont synchronisées par le même objet moniteur ("this"). Si un thread possède déjà un verrou sur un objet moniteur, il a accès à tous les blocs de code synchronisés par l'objet moniteur. C'est la réentrée. Un thread peut entrer dans n'importe quel bloc de code synchronisé par un verrou qu'il possède déjà.
L'implémentation du verrouillage donnée précédemment n'est pas réentrante. Si nous réécrivons la classe Reentrant comme suit, lorsque le thread appelle external(), il sera bloqué au niveau de lock.lock() de la méthode inner().
public class Reentrant2{ Lock lock = new Lock(); public outer(){ lock.lock(); inner(); lock.unlock(); } public synchronized inner(){ lock.lock(); //do something lock.unlock(); } }
Le thread appelant external() verrouillera d'abord l'instance de Lock, puis continuera à appeler inner(). Dans la méthode inner(), le thread tentera à nouveau de verrouiller l'instance de Lock, mais l'action échouera (c'est-à-dire que le thread sera bloqué) car l'instance de Lock a déjà été verrouillée dans la méthode external().
Unlock() n'est pas appelé entre les deux instants lock(), et le deuxième appel à lock bloquera Après avoir examiné l'implémentation de lock(), vous constaterez que la raison est évidente :
public class Lock{ boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } ... }
Le fait qu'un thread soit autorisé à quitter la méthode lock() est déterminé par les conditions de la boucle while (spin lock). La condition de jugement actuelle est que l'opération de verrouillage n'est autorisée que lorsque isLocked est faux, quel que soit le thread qui l'a verrouillé.
Afin de rendre cette classe Lock réentrante, nous devons y apporter une petite modification :
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(); } } } ... }
Notez que la boucle while actuelle (spin lock) est également prise en compte. thread qui a verrouillé cette instance de Lock. Si l'objet de verrouillage actuel n'est pas verrouillé (isLocked = false) ou si le thread appelant actuel a déjà verrouillé l'instance de Lock, alors la boucle while ne sera pas exécutée et le thread appelant lock() peut quitter la méthode (Remarque de traduction : "Être autorisé à quitter cette méthode" selon la sémantique actuelle signifie que wait() ne sera pas appelé et ne provoquera pas de blocage).
De plus, nous devons enregistrer le nombre de fois où le même thread verrouille à plusieurs reprises un objet de verrouillage. Sinon, un seul appel unblock() déverrouillera l'intégralité du verrou, même si le verrou actuel a été verrouillé plusieurs fois. Nous ne voulons pas que le verrou soit libéré avant que l'appel unlock() n'ait atteint le nombre correspondant d'appels lock().
La classe Lock est désormais réentrante.
Équité du verrouillage
Les blocs synchronisés de Java ne garantissent pas l'ordre des threads essayant d'y entrer. Par conséquent, si plusieurs threads sont constamment en concurrence pour l'accès au même bloc synchronisé, il existe un risque qu'un ou plusieurs d'entre eux n'obtiennent jamais l'accès, c'est-à-dire que l'accès sera toujours attribué à d'autres threads. Cette situation est appelée manque de threads. Pour éviter ce problème, les serrures doivent être équitables. Les verrous présentés dans cet article sont implémentés en interne à l'aide de blocs synchronisés, ils ne garantissent donc pas l'équité.
在 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中文网!