web-dev-qa-db-fra.com

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

J'utilise actuellement dans mon code un ReentrantReadWriteLock pour synchroniser l'accès sur une structure arborescente. Cette structure est volumineuse et peut être lue par plusieurs threads à la fois, avec quelques modifications occasionnelles. Elle semble donc bien correspondre à l'idiome lecture-écriture. Je comprends qu'avec cette classe particulière, on ne peut pas élever un verrou en lecture à un verrou en écriture. Par conséquent, selon les Javadocs, il faut libérer le verrou en lecture avant d'obtenir le verrou en écriture. J'ai déjà utilisé ce modèle avec succès dans des contextes non réentrants.

Ce que je découvre cependant, c'est que je ne peux pas acquérir le verrou en écriture de manière fiable sans bloquer pour toujours. Comme le verrou de lecture est réentrant et que je l’utilise en tant que tel, le code simple

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

peut bloquer si j'ai acquis le readlock de manière réentrante. Chaque appel à déverrouiller réduit simplement le nombre de mises en garde, et le verrou n'est réellement libéré que lorsque le nombre de mises en garde atteint zéro. 

EDIT pour clarifier cela, car je ne pense pas l'avoir trop bien expliqué au début - je suis conscient qu'il n'y a pas d'escalade progressive dans cette classe et que je dois simplement relâcher le verrou en lecture et obtenez le verrou en écriture. Mon problème est/était que peu importe ce que font les autres threads, l'appel de getReadLock().unlock() ne libère pas réellement le blocage de this _ _ thread sur le verrou s'il l'a réintégré, auquel cas l'appel de getWriteLock().lock() bloquera pour toujours Le thread a toujours une prise sur le verrou de lecture et se bloque ainsi.

Par exemple, cet extrait de code n'atteindra jamais l'instruction println, même s'il est exécuté seul avec aucun autre thread qui accède au verrou:

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 demande, y at-il un idiome de Nice pour gérer cette situation? Plus précisément, lorsqu'un thread qui détient un verrou en lecture (éventuellement de manière réentrante) découvre qu'il doit écrire et qu'il souhaite donc "suspendre" son propre verrou en lecture afin de récupérer le verrou en écriture (le blocage nécessaire sur d'autres threads). relâchez leurs verrous sur le verrou de lecture), puis "décrochez" son maintien sur le verrou de lecture dans le même état après?

Étant donné que cette implémentation ReadWriteLock a été spécifiquement conçue pour être réentrant, il existe sûrement un moyen sensé d'élever un verrou en lecture en un verrou en écriture lorsque les verrous peuvent être acquis de manière réentrante? C'est la partie critique qui signifie que l'approche naïve ne fonctionne pas.

60
Andrzej Doyle

J'ai fait un petit progrès à ce sujet. En déclarant explicitement la variable lock en tant que ReentrantReadWriteLock au lieu de simplement un ReadWriteLock (pas idéal, mais probablement un mal nécessaire dans ce cas), je peux appeler la méthode getReadHoldCount() . Cela me permet d’obtenir le nombre de mises en attente pour le thread actuel, et donc je peux relâcher le readlock autant de fois (et le récupérer à nouveau avec le même nombre par la suite). Donc, cela fonctionne, comme le montre un test rapide:

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

Mais est-ce que ça va être le mieux que je puisse faire? Cela ne me semble pas très élégant et j'espère toujours qu'il y a un moyen de gérer cela de manière moins "manuelle".

27
Andrzej Doyle

Ce que vous voulez faire devrait être possible. Le problème est que Java ne fournit pas d'implémentation capable de mettre à niveau les verrous en lecture pour écrire des verrous. Plus précisément, le javadoc ReentrantReadWriteLock indique qu'il ne permet pas une mise à niveau d'un verrou en lecture à un verrou en écriture. 

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

Pourquoi il est nécessaire d'améliorer la lecture pour écrire des verrous

Une mise à niveau de verrou en lecture à écriture est valide (malgré les affirmations contraires dans d'autres réponses). Une impasse peut survenir et une partie de l'implémentation consiste donc en un code permettant de reconnaître les impasses et de les rompre en lançant une exception dans un thread afin de résoudre l'impasse. Cela signifie que, dans le cadre de votre transaction, vous devez gérer l'exception DeadlockException, par exemple en effectuant une nouvelle opération. Un modèle typique est:

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

Sans cette possibilité, le seul moyen d'implémenter une transaction sérialisable qui lit un tas de données de manière cohérente, puis écrit quelque chose en fonction de ce qui a été lu est de prévoir que l'écriture sera nécessaire avant de commencer, et donc d'obtenir des verrous WRITE sur toutes les données stockées. lisez avant d'écrire ce qui doit être écrit. C'est un KLUDGE utilisé par Oracle (SELECT FOR UPDATE ...). En outre, cela réduit réellement les accès simultanés, car personne d'autre ne peut lire ni écrire les données pendant l'exécution de la transaction!

En particulier, le relâchement du verrou de lecture avant l'obtention du verrou d'écriture produira 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 une méthode appelée, crée un verrou de lecture réentrant sur y! Supposons que vous le sachiez. Ensuite, si vous relâchez d'abord le verrou de lecture:

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 que vous avez relâché son verrou en lecture et avant d’obtenir le verrou en écriture dessus. Donc, la valeur de y ne sera pas égale à x.

Code de test: La mise à niveau d'un verrou en lecture en un verrou en écriture bloque:

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");
    }

}

Sortie utilisant Java 1.6:

read to write test
<blocks indefinitely>
25
Bob

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

Comme d'autres l'ont fait remarquer, un classique verrou de lecteur/rédacteur (comme le JDK ReentrantReadWriteLock ) ne prend pas en charge, de manière inhérente, la mise à niveau d'un verrou de lecture vers un verrou d'écriture, car cela est susceptible. à l'impasse.

Si vous devez acquérir en toute sécurité un verrou en écriture sans le libérer au préalable, il existe cependant une meilleure alternative: jetez un œil à une lecture-écriture - update verrouille plutôt.

J'ai écrit un ReentrantReadWrite_Update_Lock et je l'ai publié en tant que source ouverte sous une licence Apache 2.0 ici . J'ai également publié des détails sur l'approche de la liste de diffusion JSR166 liste de diffusion sur intérêts concurrents , et cette approche a survécu à un examen approfondi de la part des membres figurant sur cette liste.

L’approche est assez simple et, comme je l’ai mentionné à propos de l’accès simultané à la concurrence, l’idée n’est pas tout à fait nouvelle, car elle a été discutée sur la liste de diffusion du noyau Linux au moins dès l’an 2000. En outre, la plate-forme .Net ReaderWriterLockSlim prend également en charge la mise à niveau par verrouillage. Si efficacement, ce concept n’avait tout simplement pas été mis en œuvre sur Java (AFAICT) jusqu’à présent.

L'idée est de fournir un verrou pdate en plus du verrou lire et du verrou write. Un verrou de mise à jour est un type de verrouillage intermédiaire entre un verrou en lecture et un verrou en écriture. Comme le verrou en écriture, un seul thread peut acquérir un verrou de mise à jour à la fois. Mais comme un verrou de lecture, il permet un accès en lecture au thread qui le détient et, simultanément, aux autres threads qui détiennent des verrous de lecture standard. La fonctionnalité clé est que le verrou de mise à jour peut être mis à niveau de son statut en lecture seule à un verrou en écriture, ce qui ne risque pas un blocage, car un seul thread peut détenir un verrou de mise à jour et être en mesure de procéder à la mise à niveau à la fois.

Cela prend en charge la mise à niveau de verrouillage et est en outre plus efficace qu'un verrou classique de lecture/écriture dans les applications avec les modèles d'accès read-before-write, car il bloque la lecture des threads pendant des périodes plus courtes.

Un exemple d'utilisation est fourni sur le site . La bibliothèque a une couverture de test de 100% et se trouve dans Maven central.

22
npgall

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

Vous ne pouvez pas avoir un verrou en lecture/écriture que vous pouvez mettre à niveau de lecture en écriture sans problèmes. Exemple:

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

Supposons maintenant que deux threads entrent dans cette fonction. (Et vous supposez qu'il y a concurrence, n'est-ce pas? Sinon, vous ne vous souciez pas des verrous en premier lieu ....)

Supposons que les deux threads démarrent en même temps et s'exécutent à la même vitesse . Cela signifierait que les deux obtiendraient un verrou de lecture, ce qui est parfaitement légal. Cependant, les deux essaieront éventuellement d'acquérir le verrou en écriture, que NONE d'entre eux n'aura jamais: Les autres threads respectifs détiennent un verrou en lecture!

Les verrous qui permettent la mise à niveau des verrous de lecture pour écrire des verrous sont par définition sujets à des blocages. Désolé, mais vous devez modifier votre approche.

8
Steffen Heil

Java 8 a maintenant un Java.util.concurrent.locks.StampedLock avec une API tryConvertToWriteLock(long)

Plus d'infos sur http://www.javaspecialists.eu/archive/Issue215.html

4
qwerty

Ce que vous recherchez, c'est une mise à niveau de verrouillage et n'est pas possible (du moins pas de manière atomique) avec le standard Java.concurrent ReentrantReadWriteLock. Votre meilleur coup est déverrouillé/verrouillé, puis vérifiez que personne n’a apporté de modifications entre les deux.

Ce que vous essayez de faire, en forçant tous les verrous de lecture, n’est pas une très bonne idée. Les verrous de lecture existent pour une raison que vous ne devriez pas écrire. :)

MODIFIER:
Comme le faisait remarquer Ran Biron, si votre problème est en famine (les verrous de lecture sont définis et libérés tout le temps, jamais à zéro), vous pouvez essayer d’utiliser la file d’attente normale. Mais votre question n'a pas semblé que c'était votre problème?

EDIT 2:
Je vois maintenant votre problème, vous avez en fait acquis plusieurs verrous de lecture sur la pile et vous souhaitez les convertir en verrouillage en écriture (mise à niveau). Ceci est en fait impossible avec l'implémentation JDK, car il ne garde pas la trace des propriétaires du verrou en lecture. Il pourrait y avoir d'autres détenteurs de verrous de lecture que vous ne verriez pas, et il n'a aucune idée du nombre de verrous de lecture qui appartiennent à votre fil, sans parler de votre pile d'appels actuelle (votre boucle tue tous lit les verrous, pas seulement les vôtres, pour que votre verrou d'écriture n'attende pas que des lecteurs simultanés soient terminés, et vous vous retrouverez avec un désordre sur les mains)

En fait, j'ai eu un problème similaire et j'ai fini par écrire mon propre verrou en gardant une trace de qui possédait ce type de verrou en lecture et de le mettre à niveau en un verrou en écriture. Bien que ce soit aussi un verrou de type lecture/écriture Copy-on-Write (permettant à un seul écrivain d’accompagner les lecteurs), c’est donc un peu différent.

4
falstro

Qu'en est-il quelque chose comme ça? 

class CachedData
{
    Object data;
    volatile boolean cacheValid;

    private class MyRWLock
    {
        private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        public synchronized void getReadLock()         { rwl.readLock().lock(); }
        public synchronized void upgradeToWriteLock()  { rwl.readLock().unlock();  rwl.writeLock().lock(); }
        public synchronized void downgradeToReadLock() { rwl.writeLock().unlock(); rwl.readLock().lock();  }
        public synchronized void dropReadLock()        { rwl.readLock().unlock(); }
    }
    private MyRWLock myRWLock = new MyRWLock();

    void processCachedData()
    {
        myRWLock.getReadLock();
        try
        {
            if (!cacheValid)
            {
                myRWLock.upgradeToWriteLock();
                try
                {
                    // Recheck state because another thread might have acquired write lock and changed state before we did.
                    if (!cacheValid)
                    {
                        data = ...
                        cacheValid = true;
                    }
                }
                finally
                {
                    myRWLock.downgradeToReadLock();
                }
            }
            use(data);
        }
        finally
        {
            myRWLock.dropReadLock();
        }
    }
}
2
Henry HO

Je suppose que la ReentrantLock est motivée par une traversée récursive de l'arbre:

public void doSomething(Node node) {
  // Acquire reentrant lock
  ... // Do something, possibly acquire write lock
  for (Node child : node.childs) {
    doSomething(child);
  }
  // Release reentrant lock
}

Ne pouvez-vous pas refactoriser votre code pour déplacer le traitement des verrous en dehors de la récursion?

public void doSomething(Node node) {
  // Acquire NON-reentrant read lock
  recurseDoSomething(node);
  // Release NON-reentrant read lock
}

private void recurseDoSomething(Node node) {
  ... // Do something, possibly acquire write lock
  for (Node child : node.childs) {
    recurseDoSomething(child);
  }
}
1
Olivier

à OP:

boolean needWrite = false;
readLock.lock()
try{
  needWrite = checkState();
}finally{
  readLock().unlock()
}

//the state is free to change right here, but not likely
//see who has handled it under the write lock, if need be
if (needWrite){
  writeLock().lock();
  try{
    if (checkState()){//check again under the exclusive write lock
   //modify state
    }
  }finally{
    writeLock.unlock()
  }
}

dans le verrou d’écriture comme tout programme simultané de respect de soi, vérifiez l’état requis.

HoldCount ne doit pas être utilisé au-delà de la détection de débogage/surveillance/échec rapide.

1
xss

Donc, attendons-nous que Java incrémente le nombre de sémaphores de lecture uniquement si ce thread n'a pas encore contribué au readHoldCount? Ce qui signifie que contrairement à la simple gestion d'un readholdCount ThreadLocal de type int, il convient de conserver un ensemble ThreadLocal de type Integer (en conservant le hasCode du thread actuel). Si cela vous convient, je suggérerais (du moins pour le moment) de ne pas appeler plusieurs appels de lecture dans la même classe, mais d'utiliser plutôt un indicateur pour vérifier si le verrou de lecture est déjà obtenu par l'objet actuel ou non.

private volatile boolean alreadyLockedForReading = false;

public void lockForReading(Lock readLock){
   if(!alreadyLockedForReading){
      lock.getReadLock().lock();
   }
}
1
Vishal Angrish

Trouvé dans la documentation de ReentrantReadWriteLock . Il est clairement indiqué que les threads de lecture ne réussiront jamais en essayant d’obtenir un verrou en écriture. Ce que vous essayez de réaliser n’est tout simplement pas pris en charge. Vous devez relâcher le verrou de lecture avant l'acquisition du verrou d'écriture. Une dégradation est toujours possible.

Réentrance

Ce verrou permet aux lecteurs et aux rédacteurs d’acquérir à nouveau la lecture ou l’écriture se verrouille dans le style d'un {@link ReentrantLock}. Lecteurs non réentrants ne sont pas autorisés jusqu'à ce que tous les verrous d'écriture détenus par le fil d'écriture aient été libéré.

De plus, un écrivain peut acquérir le verrou de lecture, mais pas l'inverse . Parmi les autres applications, la réentrance peut être utile lors du verrouillage en écriture sont conservés pendant les appels ou les rappels de méthodes exécutant des lectures sous lire les serrures. Si un lecteur essaie d'acquérir le verrou en écriture, il ne le fera jamais réussir.

Exemple d'utilisation de la source ci-dessus:

 class CachedData {
   Object data;
   volatile boolean cacheValid;
   ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

   void processCachedData() {
     rwl.readLock().lock();
     if (!cacheValid) {
        // Must release read lock before acquiring write lock
        rwl.readLock().unlock();
        rwl.writeLock().lock();
        // Recheck state because another thread might have acquired
        //   write lock and changed state before we did.
        if (!cacheValid) {
          data = ...
          cacheValid = true;
        }
        // Downgrade by acquiring read lock before releasing write lock
        rwl.readLock().lock();
        rwl.writeLock().unlock(); // Unlock write, still hold read
     }

     use(data);
     rwl.readLock().unlock();
   }
 }
0
phi