68 votes

Java ReentrantReadWriteLocks - Comment acquérir en toute sécurité un verrou en écriture?

J'utilise dans mon code en ce moment un ReentrantReadWriteLock pour synchroniser l'accès sur une structure en arbre. Cette structure est grande, et lu par de nombreux fils à la fois avec parfois des modifications de petites pièces de ce - de sorte qu'il semble correspondre à la lecture-écriture de l'idiome. Je comprends que cette classe particulière, on ne peut pas élever un verrou en lecture à un verrou d'écriture, ainsi que par les Javadocs on doit libérer le verrou de lecture avant d'obtenir le verrou d'écriture. J'ai utilisé ce modèle avec succès dans non réentrant des contextes avant de.

Ce que je suis la recherche cependant, c'est que je ne peut pas de manière fiable acquérir le verrou d'écriture sans bloquer pour toujours. Depuis le verrou en lecture est réentrant et je suis en train de l'utiliser en tant que tel, le code simple

lock.getReadLock().unlock();
lock.getWriteLock().lock()

peut bloquer si j'ai acquis la readlock reentrantly. Chaque appel à débloquer réduit simplement la tenir compte, et que le verrou n'est libéré lorsque le tenir compte atteint zéro.

EDIT pour préciser cela, car je ne pense pas que je vous l'explique trop bien au début - je suis conscient qu'il n'est pas intégré dans l'escalade de verrous dans cette classe, et que je dois il suffit de relâcher le verrou de lecture et d'obtenir le verrou d'écriture. Mon problème est que, indépendamment de ce que les autres threads sont en train de faire, en appelant getReadLock().unlock() peut pas réellement la libération de ce thread sur la serrure si elle a acquis il reentrantly, auquel cas l'appel à getWriteLock().lock() bloquera jamais que ce fil a encore une emprise sur le verrou en lecture et donc des blocs de lui-même.

Par exemple, cet extrait de code n'atteindra jamais la println déclaration, même lorsqu'il est exécuté single thread n'ayant pas d'autres threads d'accéder à la serrure:

final ReadWriteLock lock = new ReentrantReadWriteLock();
lock.getReadLock().lock();

// In real code we would go call other methods that end up calling back and
// thus locking again
lock.getReadLock().lock();

// Now we do some stuff and realise we need to write so try to escalate the
// lock as per the Javadocs and the above description
lock.getReadLock().unlock(); // Does not actually release the lock
lock.getWriteLock().lock();  // Blocks as some thread (this one!) holds read lock

System.out.println("Will never get here");

Alors je vous le demande, est-il un bel idiome pour gérer cette situation? Plus précisément, lorsqu'un thread qui détient un verrou en lecture (éventuellement reentrantly) découvre qu'il a besoin de faire un peu de l'écriture, et donc veut "suspendre" son propre verrou en lecture afin de ramasser le verrou d'écriture (blocage que nécessaire sur d'autres threads pour libérer leurs cales sur le verrou en lecture), et puis "pick up" de son emprise sur le verrou en lecture dans le même état après?

Depuis cette ReadWriteLock mise en œuvre a été spécifiquement conçu pour être réentrant, il y a sûrement une façon logique d'élever un verrou en lecture à un verrou en écriture lorsque les verrous peuvent être acquises reentrantly? C'est la partie critique que signifie l'approche naïve ne fonctionne pas.

29voto

Andrzej Doyle Points 52541

J'ai fait un peu de progrès sur ce point. En déclarant le verrouillage variable explicitement comme un ReentrantReadWriteLock au lieu de simplement un ReadWriteLock (moins que l'idéal, mais c'est probablement un mal nécessaire dans ce cas), je peux appeler le getReadHoldCount() méthode. Cela me permet d'obtenir le nombre de détient pour le thread en cours, et donc je peux libérer le readlock à de nombreuses reprises (et acquérir le même nombre par la suite). Si cela fonctionne, comme l'a montré un quick-and-dirty test:

final int holdCount = lock.getReadHoldCount();
for (int i = 0; i < holdCount; i++)
{
   lock.getReadLock().unlock;
}
lock.getWriteLock().lock()
try
{
   // Perform modifications
}
finally
{
   // Downgrade by reacquiring read lock before releasing write lock
   for (int i = 0; i < holdCount; i++)
   {
      lock.getReadLock().lock();
   }
   lock.getWriteLock().unlock();
}

Encore, est-ce que ça va être le meilleur que je peux faire? Il ne se sent pas très élégant, et j'espère toujours qu'il y a une façon de gérer cela est moins "manuel" de la mode.

28voto

npgall Points 975

C'est une vieille question, mais ici, c'est à la fois une solution au problème, et quelques informations de fond.

Comme d'autres l'ont souligné, un classique de lecteurs-graveurs de serrure (comme le JDK ReentrantReadWriteLock), intrinsèquement, ne prend pas en charge la mise à niveau d'un verrou en lecture à un verrou d'écriture, parce que cela est sensible à l'impasse.

Si vous avez besoin en toute sécurité acquérir un verrou en écriture sans d'abord de la libération d'un verrou en lecture, il est cependant une meilleure alternative: prendre un coup d'oeil à une lecture-écriture-mise à jour de verrouillage de la place.

J'ai écrit un ReentrantReadWrite_Update_Lock, et publié en open source sous une licence Apache 2.0 ici. J'ai aussi posté les détails de l'approche de la JSR166 simultanéité d'intérêts liste de diffusion, et l'approche a survécu à plusieurs allers et retours à l'examen par les membres sur la liste.

La démarche est assez simple, et comme je l'ai mentionné sur la simultanéité d'intérêt, l'idée n'est pas entièrement nouveau, comme il a été discuté sur le noyau Linux, liste de diffusion, au moins aussi loin que l'an 2000. Également la .Net plate-forme de l' ReaderWriterLockSlim prend en charge de verrouillage de la mise à niveau également. Donc, effectivement, ce concept n'avait tout simplement pas été mis en œuvre sur l'île de Java (AFAICT) jusqu'à maintenant.

L'idée est de fournir une mise à jour de verrouillage en plus de la lecture de verrouillage et l' écriture de verrouillage. Un verrou de mise à jour est un intermédiaire de type de verrou entre un verrou en lecture et un verrou en écriture. Comme le verrou d'écriture, un seul thread peut acquérir un verrou de mise à jour à la fois. Mais comme un verrou de lecture, il permet l'accès en lecture pour le fil qui le tient, et simultanément à d'autres threads qui organisent régulièrement des verrous en lecture. La caractéristique principale est que le verrou de mise à jour peuvent être mis à niveau à partir de sa lecture-seule statut, à un verrou en écriture, et ce n'est pas vulnérable à une impasse parce qu'un seul thread peut tenir un verrou de mise à jour et être en mesure de mettre à niveau à la fois.

Cette charge de verrouillage de la mise à niveau, et en plus c'est plus efficace qu'un classique des lecteurs-verrou en écriture dans les applications à lire avant d'écrire des modèles d'accès, car il bloque la lecture de threads pour de courtes périodes de temps.

Exemple d'utilisation est fourni sur le site. La bibliothèque dispose de 100% de couverture de test et est dans Maven central.

25voto

Bob Points 91

Ce que vous voulez faire doit être possible. Le problème, c'est que Java n'est pas de fournir une implémentation de la mise à niveau des verrous en lecture à des verrous en écriture. Plus précisément, la javadoc ReentrantReadWriteLock dit qu'il ne permet pas une mise à niveau de lecture serrure à verrou en écriture.

En tout cas, Jakob Jenkov décrit comment la mettre en œuvre. Voir http://tutorials.jenkov.com/java-concurrency/read-write-locks.html#upgrade pour plus de détails.

Pourquoi la mise à niveau en Lecture à des Verrous en Écriture Est Nécessaire

Une mise à niveau de lecture pour verrou d'écriture est valide (malgré les affirmations contraires dans les autres réponses). Un blocage peut se produire, et donc, une partie de la mise en œuvre du code de reconnaître les blocages et les briser par la levée d'une exception dans un thread pour sortir de l'impasse. Cela signifie que, dans le cadre de votre transaction, vous devez gérer le DeadlockException, par exemple, en faisant le travail une fois de plus. Un modèle typique est:

boolean repeat;
do {
  repeat = false;
  try {
   readSomeStuff();
   writeSomeStuff();
   maybeReadSomeMoreStuff();
  } catch (DeadlockException) {
   repeat = true;
  }
} while (repeat);

Sans cette capacité, la seule façon de mettre en œuvre une transaction sérialisable qui lit un tas de données de manière cohérente et écrit quelque chose sur la base de la lecture est à prévoir que l'écriture sera nécessaire avant de vous lancer, et donc d'obtenir des verrous en ÉCRITURE sur l'ensemble des données qui sont lues avant d'écrire ce qui doit être écrit. C'est une BIDOUILLE que Oracle utilise SÉLECTIONNEZ cette option POUR mettre à JOUR ...). En outre, elle réduit la concurrence parce que personne d'autre ne peut lire ou écrire des données alors que la transaction est en cours d'exécution!

En particulier, relâcher le verrou de lecture avant d'obtenir le verrou d'écriture permettra de produire des résultats incohérents. Considérer:

int x = someMethod();
y.writeLock().lock();
y.setValue(x);
y.writeLock().unlock();

Vous devez savoir si someMethod(), ou toute autre méthode qu'il appelle, crée un réentrant verrou en lecture sur y! Supposons que vous savez qu'il fait. Alors si vous relâchez le verrou en lecture de la première:

int x = someMethod();
y.readLock().unlock();
// problem here!
y.writeLock().lock();
y.setValue(x);
y.writeLock().unlock();

un autre thread peut changer y après la sortie de son verrou en lecture, et avant de vous obtenir le verrou en écriture sur elle. Si y est la valeur ne sera pas égale à x.

Le Code de Test: mise à niveau d'un verrou en lecture à un verrou d'écriture des blocs:

import java.util.*;
import java.util.concurrent.locks.*;

public class UpgradeTest {

    public static void main(String[] args) 
    {   
        System.out.println("read to write test");
        ReadWriteLock lock = new ReentrantReadWriteLock();

        lock.readLock().lock(); // get our own read lock
        lock.writeLock().lock(); // upgrade to write lock
        System.out.println("passed");
    }

}

De sortie à l'aide de la version 1.6 de Java:

read to write test
<blocks indefinitely>

11voto

Steffen Heil Points 1164

Ce que vous essayez de faire est tout simplement pas possible de cette façon.

Vous ne pouvez pas avoir une lecture/écriture de verrouillage que vous pouvez mettre à niveau à partir de lecture d'écrire sans problèmes. Exemple:

void test() {
    lock.readLock().lock();
    ...
    if ( ... ) {
        lock.writeLock.lock();
        ...
        lock.writeLock.unlock();
    }
    lock.readLock().unlock();
}

Supposons maintenant, les deux fils allait entrer dans cette fonction. (Et vous êtes en supposant que la concurrence, droit? Sinon vous ne seriez pas de soins sur les verrous dans la première place....)

Supposer que les deux fils de démarrer en même temps et de courir aussi vite. Ce qui signifie, à la fois permettrait d'acquérir un verrou en lecture, ce qui est parfaitement légal. Cependant, les deux se serait finalement de tenter d'acquérir le verrou d'écriture, qu'AUCUN d'entre eux ne sera jamais obtenir: Les autres threads tenir un verrou en lecture!

Les verrous qui permettent la mise à niveau des verrous en lecture écriture serrures sont sujettes à des blocages, par définition. Désolé, mais vous avez besoin de modifier votre approche.

4voto

falstro Points 16545

Ce que vous cherchez est un verrou de mise à niveau, et n'est pas possible (du moins pas de manière atomique) en utilisant le standard de java.simultanées ReentrantReadWriteLock. Votre meilleur coup est de déverrouiller/verrouiller, puis vérifier que personne ne fait des modifications dans l'intervalle.

Ce que tu essaies de faire, forçant tous les verrous en lecture hors de la voie n'est pas une très bonne idée. Verrous en lecture sont là pour une raison, que vous ne devriez pas écrire. :)

EDIT:
Que Ran Biron a souligné, si votre problème est la famine (lire les verrous sont créés et libéré tous les temps, ne jamais tomber à zéro), vous pouvez essayer de l'utiliser juste la queue. Mais votre question n'a pas l'air comme cela a été votre problème?

EDIT 2:
Je vois maintenant votre problème, vous avez réellement acquis de lecture multiples-des verrous sur la pile, et que vous souhaitez les convertir en une écriture de verrouillage (mise à niveau). C'est en fait impossible avec le JDK-mise en œuvre, car il ne garde pas trace des propriétaires de la lecture de verrouillage. Il pourrait y avoir d'autres détenteurs de lire les serrures que vous ne les verrez pas, et il n'a aucune idée de la façon dont beaucoup de la lecture verrous appartiennent à votre fil, pour ne pas mentionner votre courant de la pile des appels (c'est à dire votre boucle est en train de tuer tous les verrous en lecture, pas seulement sur vos propres, de sorte que votre verrou d'écriture ne vous attendra pas pour tout de lecteurs simultanés à la fin, et vous vous retrouverez avec un désordre sur vos mains)

En fait, j'ai eu un problème similaire, et j'ai fini par écrire mon propre cadenas à garder la trace de qui a de quoi lire les serrures et la mise à niveau de ces à écrire des serrures. Bien que ce fut aussi une Copie sur Écriture genre de lecture/écriture de verrouillage (permettant à un écrivain, le long de lecteurs), donc c'était un peu différent encore.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X