Was ist der Unterschied zwischen synchronisiert und ReentrantLock?

Vorwort

Software-Parallelität ist zu einer Grundfunktion moderner Softwareentwicklung geworden, und der sorgfältig konzipierte effiziente Parallelitätsmechanismus von Java ist eine der Grundlagen für die Erstellung umfangreicher Anwendungen.

Der Schwerpunkt dieses Blogbeitrags liegt auf der Frage: Was ist der Unterschied zwischen synchronisiert und ReentrantLock? Manche Leute sagen, dass synchronisiert am langsamsten sei. Stimmt das?  

Häufig gestellte Fragen

synchronizedEs handelt sich um den in Java integrierten Synchronisationsmechanismus, daher wird er von manchen auch als intrinsische Sperre bezeichnet. Er bietet sich gegenseitig ausschließende Semantik und Sichtbarkeit. Wenn ein Thread die aktuelle Sperre erhalten hat, können andere Threads, die versuchen, sie zu erwerben, dort nur warten oder blockieren.

Vor Java 5 war synchronisiert die einzige Synchronisationsmethode. Im Code kann synchronisiert zum Ändern von Methoden oder für bestimmte Codeblöcke verwendet werden. Im Wesentlichen entspricht die synchronisierte Methode dem Einschließen aller Anweisungen der Methode synchronisierte Blöcke. .

ReentrantLock, normalerweise als Wiedereintrittssperre übersetzt, ist die von Java 5 bereitgestellte Sperrimplementierung. Ihre Semantik ist im Wesentlichen dieselbe wie die von synchronisiert. Die Wiedereintrittssperre wird durch direkten Aufruf der lock()-Methode über den Code erreicht, wodurch das Schreiben von Code flexibler wird. Gleichzeitig bietet ReentrantLock viele praktische Methoden, mit denen viele detaillierte Kontrollen erreicht werden können, die synchronisiert nicht möglich sind, z. B. die Kontrolle der Fairness oder die Verwendung definierter Bedingungen usw. Beim Codieren muss jedoch auch darauf geachtet werden, dass zum Freigeben die Methode unlock () explizit aufgerufen werden muss, andernfalls bleibt die Sperre immer bestehen.

Die Leistung von synchronisiert und ReentrantLock kann nicht verallgemeinert werden. Die frühe Version von synchronisiert weist in vielen Szenarien einen großen Leistungsunterschied auf. In nachfolgenden Versionen wurden viele Verbesserungen vorgenommen, und in Szenarien mit geringem Wettbewerb ist die Leistung möglicherweise besser als die von ReentrantLock.  

Spezifische Analyse

In Bezug auf die gleichzeitige Programmierung haben verschiedene Unternehmen oder Interviewer unterschiedliche Interviewstile. Einige große Unternehmen fragen Sie gerne immer wieder nach den Erweiterungen oder zugrunde liegenden Mechanismen der relevanten Mechanismen, andere beginnen gerne aus einer praktischen Perspektive, sodass Sie ein gewisses Maß an Geduld benötigen bei der Vorbereitung auf die gleichzeitige Programmierung.

Als eines der grundlegenden Werkzeuge der Parallelität müssen Sperren mindestens beherrscht werden:

  • Verstehen Sie, was Thread-Sicherheit ist.
  • Grundlegende Verwendungen und Fälle von synchronisierten, ReentrantLock- und anderen Mechanismen.

Um noch einen Schritt weiter zu gehen, benötigen Sie:

  • Beherrschen Sie die zugrunde liegende Implementierung von synchronisiertem und ReentrantLock, verstehen Sie die Erweiterung und Verschlechterung von Sperren und verstehen Sie Konzepte wie Skew-Sperren, Spin-Sperren, leichte Sperren und schwere Sperren.
  • Beherrschen Sie die verschiedenen Implementierungen und Fallstudien von java.util.concurrent.lock im Concurrency-Paket.  

Praktische Analyse

Zuerst müssen wir verstehen, was Thread-Sicherheit ist.

In „Java Concurrency in Practice“, geschrieben von Experten wie Brain Goetz, ist Thread-Sicherheit ein Konzept der Korrektheit in einer Multithread-Umgebung , d Der hier im Programm wiedergegebene Status kann tatsächlich als Daten betrachtet werden.

Aus einer anderen Perspektive gibt es kein Thread-Sicherheitsproblem, wenn der Status nicht gemeinsam genutzt oder geändert werden kann, und es können zwei Methoden zur Gewährleistung der Thread-Sicherheit abgeleitet werden:

  • Kapselung: Durch Kapselung können wir den internen Zustand des Objekts verbergen und schützen.
  • Unveränderlichkeit: Dies gilt für endgültig und unveränderlich. Die Java-Sprache verfügt derzeit nicht über echte native Unveränderlichkeit, sie wird jedoch möglicherweise in Zukunft eingeführt.

Die Thread-Sicherheit muss mehrere grundlegende Eigenschaften gewährleisten:

  • Atomarität bedeutet einfach ausgedrückt, dass verwandte Vorgänge nicht von anderen Threads auf halbem Weg gestört werden und wird im Allgemeinen durch einen Synchronisationsmechanismus erreicht.
  • Sichtbarkeit bedeutet , dass, wenn ein Thread eine gemeinsam genutzte Variable ändert, deren Status anderen Threads sofort bekannt sein kann. Dies wird normalerweise so interpretiert, dass er den lokalen Status des Threads im Hauptspeicher widerspiegelt. Volatile ist für die Gewährleistung der Sichtbarkeit verantwortlich.
  • Ordnung gewährleistet die serielle Semantik innerhalb eines Threads und vermeidet eine Neuanordnung von Anweisungen.

Es mag etwas unklar sein, also werfen wir einen Blick auf den folgenden Codeausschnitt und analysieren, wo sich die Atomizitätsanforderung widerspiegelt. Dieses Beispiel simuliert zwei Operationen für den gemeinsamen Zustand, indem zwei Werte genommen und verglichen werden.

Sie können kompilieren und ausführen, und Sie können sehen, dass es bei nur geringer Parallelität zweier Threads sehr leicht zu einer Situation kommt, in der ersterer und letzterer nicht gleich sind. Dies liegt daran, dass andere Threads während der beiden Werterfassungsprozesse möglicherweise den SharedState geändert haben.

public class ThreadSafeSample {
  public int sharedState;
  public void nonSafeAction() {
      while (sharedState < 100000) {
          int former = sharedState++;
          int latter = sharedState;
          if (former != latter - 1) {
              System.out.printf("Observed data race, former is " +
                      former + ", " + "latter is " + latter);
          }
      }
  }

  public static void main(String[] args) throws InterruptedException {
      ThreadSafeSample sample = new ThreadSafeSample();
      Thread threadA = new Thread(){
          public void run(){
              sample.nonSafeAction();
          }
      };
      Thread threadB = new Thread(){
          public void run(){
              sample.nonSafeAction();
          }
      };
      threadA.start();
      threadB.start();
      threadA.join();
      threadB.join();
  }
}
复制代码

Das Folgende sind die Ergebnisse eines bestimmten Laufs:

Observed data race, former is 9851, latter is 9853
复制代码

Schützen Sie die beiden Zuweisungsprozesse mit synchronisiert und verwenden Sie diese als sich gegenseitig ausschließende Einheit, um zu verhindern, dass andere Threads sharedState gleichzeitig ändern.

synchronized (this) {
  int former = sharedState ++;
  int latter = sharedState;
  // …
}
复制代码

Wenn Sie Javap zum Dekompilieren verwenden, können Sie ähnliche Fragmente sehen, indem Sie das Monitorenter/Monitorexit-Paar verwenden, um eine Synchronisationssemantik zu erreichen:

11: astore_1
12: monitorenter
13: aload_0
14: dup
15: getfield    #2                // Field sharedState:I
18: dup_x1
56: monitorexit
复制代码

Die Verwendung von synchronisiert im Code ist sehr praktisch. Wenn es zum Ändern einer statischen Methode verwendet wird, entspricht dies der Verwendung des folgenden Codes zum Einschließen des Methodenkörpers:

synchronized (ClassName.class) {}
复制代码

Werfen wir einen Blick auf ReentrantLock. Sie fragen sich vielleicht, was ein Wiedereintritt ist? Dies bedeutet, dass, wenn ein Thread versucht, eine Sperre zu erhalten, die er bereits erworben hat, die Erfassungsaktion automatisch erfolgreich ist. Dies ist ein Konzept der Granularität der Sperrenerfassung, das heißt, die Sperre wird in Einheiten von Threads gehalten und nicht auf der Grundlage der Anzahl von Anrufen. Die Java-Sperrimplementierung betont den Wiedereintritt, um ihn vom Verhalten von Pthreads zu unterscheiden.

Wiedereintrittssperren können auf Fairness (Fairness) eingestellt werden, und wir können beim Erstellen einer Wiedereintrittssperre auswählen, ob dies fair ist.

ReentrantLock fairLock = new ReentrantLock(true);
复制代码

Die sogenannte Fairness bedeutet hier, dass in einem Wettbewerbsszenario, wenn Fairness wahr ist, die Sperre tendenziell an den Thread vergeben wird, der am längsten gewartet hat. Fairness ist eine Möglichkeit, das Auftreten von Thread-„Hunger“ zu reduzieren (bei dem einzelne Threads lange auf eine Sperre warten, diese aber nie erhalten können).

Wenn wir synchronisiert verwenden, können wir überhaupt keine faire Wahl treffen . Das ist immer unfair. Dies ist auch die Thread-Planungswahl gängiger Betriebssysteme. In allgemeinen Szenarien ist Fairness möglicherweise nicht so wichtig wie gedacht, und die Standardplanungsrichtlinie von Java führt selten dazu, dass es zu einem „Hunger“ kommt. Gleichzeitig führt die Gewährleistung der Fairness zu einem zusätzlichen Overhead, der natürlich zu einer gewissen Verringerung des Durchsatzes führt. Deshalb schlage ich vor, dass es nur dann notwendig ist, es anzugeben, wenn Ihr Programm wirklich ein Bedürfnis nach Fairness hat.

Lassen Sie uns aus der Perspektive der täglichen Codierung lernen, bevor wir die Sperre betreten. Um die Freigabe der Sperre sicherzustellen, empfehle ich, dass jede lock()-Aktion sofort einem Try-Catch-Finly entspricht. Die typische Codestruktur ist wie folgt. Das ist eine gute Angewohnheit.

ReentrantLock fairLock = new ReentrantLock(true);// 这里是演示创建公平锁,一般情况不需要。
fairLock.lock();
try {
  // do something
} finally {
   fairLock.unlock();
}
复制代码

Im Vergleich zu synchronisiert kann ReentrantLock wie ein gewöhnliches Objekt verwendet werden, sodass Sie die verschiedenen praktischen Methoden verwenden können, die es bietet, um feine Synchronisierungsvorgänge durchzuführen und sogar Anwendungsfälle zu implementieren, die mit synchronisiert nur schwer auszudrücken sind, wie zum Beispiel:

  • Versuchen Sie, eine Sperre mit Zeitüberschreitung zu erhalten.
  • Sie können feststellen, ob ein Thread oder ein bestimmter Thread in der Schlange steht, um die Sperre zu erhalten.
  • Kann auf Unterbrechungsanfragen reagieren.
  • ...

Hier möchte ich besonders die Bedingungsvariable (java.util.concurrent.Condition) hervorheben . Wenn ReentrantLock eine Alternative zu synchronisiert ist, konvertiert Condition Warte-, Benachrichtigungs-, NotifyAll- und andere Vorgänge in entsprechende Objekte, wodurch komplexe und undurchsichtige Synchronisierungsvorgänge in intuitive umgewandelt werden und kontrollierbares Objektverhalten.

Das typischste Anwendungsszenario für Bedingungsvariablen ist ArrayBlockingQueue in der Standardklassenbibliothek.

Sehen Sie sich den folgenden Quellcode an. Rufen Sie zunächst die Bedingungsvariable über die Wiedereintrittssperre ab:


/** Condition for waiting takes */
private final Condition notEmpty;

/** Condition for waiting puts */
private final Condition notFull;
 
public ArrayBlockingQueue(int capacity, boolean fair) {
  if (capacity <= 0)
      throw new IllegalArgumentException();
  this.items = new Object[capacity];
  lock = new ReentrantLock(fair);
  notEmpty = lock.newCondition();
  notFull =  lock.newCondition();
}
复制代码

Zwei Bedingungsvariablen werden aus derselben wiedereintretenden Sperre erstellt und dann in bestimmten Vorgängen verwendet, z. B. in der folgenden Take-Methode, um die Erfüllung der Bedingung zu ermitteln und darauf zu warten:

public E take() throws InterruptedException {
  final ReentrantLock lock = this.lock;
  lock.lockInterruptibly();
  try {
      while (count == 0)
          notEmpty.await();
      return dequeue();
  } finally {
      lock.unlock();
  }
}
复制代码

Wenn die Warteschlange leer ist, sollte das korrekte Verhalten des Threads, der versucht, sie zu übernehmen, darin bestehen, auf das Einreihen in die Warteschlange zu warten, anstatt direkt zurückzukehren. Dies ist die Semantik von BlockingQueue. Diese Logik kann mithilfe der Bedingung notEmpty elegant implementiert werden.

Wie kann also sichergestellt werden, dass der Beitritt zur Warteschlange nachfolgende Take-Vorgänge auslöst? Bitte schauen Sie sich die Enqueue-Implementierung an:

private void enqueue(E e) {
  // assert lock.isHeldByCurrentThread();
  // assert lock.getHoldCount() == 1;
  // assert items[putIndex] == null;
  final Object[] items = this.items;
  items[putIndex] = e;
  if (++putIndex == items.length) putIndex = 0;
  count++;
  notEmpty.signal(); // 通知等待的线程,非空条件已经满足
}
复制代码

Durch die Kombination von Signal/Warten, Zustandsbeurteilung und Benachrichtigung werden Wartethreads abgeschlossen und der Statusfluss verläuft sehr reibungslos. Beachten Sie, dass es sehr wichtig ist, Signal und Wait paarweise aufzurufen. Andernfalls wartet der Thread unter der Annahme, dass es nur eine Warteaktion gibt, bis er unterbrochen wird.

Aus Sicht der Leistung war die frühe Implementierung von synchronisiert relativ ineffizient. Im Vergleich zu ReentrantLock ist die Leistung in den meisten Szenarien recht unterschiedlich. In Java 6 wurden jedoch viele Verbesserungen vorgenommen. Sie können sich auf den Leistungsvergleich beziehen. In Situationen mit hohem Wettbewerb hat ReentrantLock immer noch gewisse Vorteile. Meine detaillierte Analyse in der nächsten Vorlesung wird hilfreicher sein, um die zugrunde liegenden Gründe für die Leistungsunterschiede zu verstehen. In den meisten Fällen besteht kein Grund zur Sorge um die Leistung, aber bedenken Sie die Bequemlichkeit und Wartbarkeit der Code-Schreibstruktur.  

Nachwort

Das war's für  Java: Was ist der Unterschied zwischen synchronisiert und ReentrantLock?  der gesamte Inhalt;

Es stellt vor, was Thread-Sicherheit ist, vergleicht und analysiert synchronisierte und ReentrantLock und führt Bedingungsvariablen und andere Aspekte mit Fallcode ein.

Supongo que te gusta

Origin blog.csdn.net/2301_76607156/article/details/130525557
Recomendado
Clasificación