Heim >Java >javaLernprogramm >Gemeinsame Implementierung von Objekten in der gleichzeitigen Java-Programmierung
Normalerweise können wir nicht garantieren, dass der Thread, der den Lesevorgang ausführt, die von anderen Threads geschriebenen Werte sehen kann, da jeder Thread über seinen eigenen Caching-Mechanismus verfügt. Um die Sichtbarkeit von Speicherschreibvorgängen zwischen mehreren Threads sicherzustellen, muss ein Synchronisierungsmechanismus verwendet werden.
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } }
Der obige Code scheint 42 auszugeben, tatsächlich wird er jedoch möglicherweise überhaupt nicht beendet, da der Lese-Thread den Wert von „Ready“ nie sehen kann. Es ist sehr wahrscheinlich, dass er 0 ausgibt, da der Lese-Thread den Wert von „Ready“ sieht geschrieben, aber der Wert, der danach in die Zahl geschrieben wird, wird nicht gesehen. Dieses Phänomen wird als „Neuordnung“ bezeichnet. Ohne Synchronisierung können Compiler, Prozessor, Laufzeit usw. unerwartete Anpassungen an der Ausführungsreihenfolge von Vorgängen vornehmen.
Wenn also Daten zwischen mehreren Threads geteilt werden, sollten Sie eine ordnungsgemäße Synchronisierung verwenden.
Wenn keine Synchronisierung verwendet wird, ist es sehr wahrscheinlich, dass der ungültige Wert der Variablen erhalten wird. Die ungültigen Werte werden möglicherweise nicht gleichzeitig angezeigt, und ein Thread erhält möglicherweise den neuesten Wert einer Variablen und den ungültigen Wert einer anderen Variablen. Ungültige Daten können auch zu verwirrenden Fehlern wie unerwarteten Ausnahmen, beschädigten Datenstrukturen, ungenauen Berechnungen, Endlosschleifen usw. führen.
Für nichtflüchtige Long- und Double-Variablen ermöglicht die JVM die Zerlegung einer 64-Bit-Lese- oder Schreiboperation in zwei 32-Bit-Operationen. Daher ist es sehr wahrscheinlich, dass die oberen 32 Bits des neuesten Werts und die unteren 32 Bits des ungültigen Werts gelesen werden, was dazu führt, dass ein Zufallswert gelesen wird. Es sei denn, sie sind mit dem Schlüsselwort volatile deklariert oder mit einer Sperre geschützt.
Wenn ein Thread einen synchronisierten Codeblock ausführt, der durch eine Sperre geschützt ist, kann er die Ergebnisse aller vorherigen Vorgänge anderer Threads im selben synchronisierten Codeblock sehen. Ohne Synchronisation können die oben genannten Garantien nicht erreicht werden. Die Bedeutung von Sperren beschränkt sich nicht nur auf gegenseitiges Ausschlussverhalten, sondern umfasst auch Sichtbarkeit. Um sicherzustellen, dass alle Threads den neuesten Wert einer gemeinsam genutzten Variablen sehen, müssen alle Threads, die Lese- oder Schreibvorgänge ausführen, mit derselben Sperre synchronisiert werden.
Wenn eine Variable als flüchtiger Typ deklariert wird, ordnen weder der Compiler noch die Laufzeit die Operationen für die Variable zusammen mit anderen Speicheroperationen neu. Flüchtige Variablen werden nicht in Registern oder anderen für den Prozessor unsichtbaren Orten zwischengespeichert, sodass beim Lesen einer flüchtigen Variablen immer der zuletzt geschriebene Wert zurückgegeben wird. Der Sperrmechanismus kann sowohl Sichtbarkeit als auch Atomizität gewährleisten, während flüchtige Variablen nur Sichtbarkeit gewährleisten können.
Volatile Variablen sollten nur dann verwendet werden, wenn alle der folgenden Bedingungen erfüllt sind:
Der Schreibvorgang auf die Variable hängt nicht vom aktuellen Wert der Variablen ab oder es ist sichergestellt, dass nur ein einzelner Thread vorhanden ist Wird verwendet, um den Wert der Variablen zu aktualisieren.
Diese Variable wird nicht zusammen mit anderen Zustandsvariablen in die Invarianzbedingung einbezogen.
Beim Zugriff auf Variablen ist keine Sperre erforderlich.
Das Veröffentlichen eines Objekts bedeutet, dass das Objekt in Code außerhalb des aktuellen Bereichs verwendet werden kann. Zu den Methoden zum Veröffentlichen von Objekten gehören: Verweise auf nicht private Variablen, von Methodenaufrufen zurückgegebene Verweise, Veröffentlichen innerer Klassenobjekte und impliziter Verweise auf externe Klassen usw. Wenn ein Objekt freigegeben wird, das nicht freigegeben werden sollte, spricht man von einem Leck.
public class ThisEscape { private int status; public ThisEscape(EventSource source) { source.registerListener(new EventListener() { public void onEvent(Event e) { doSomething(e); } }); status = 1; } void doSomething(Event e) { status = e.getStatus(); } interface EventSource { void registerListener(EventListener e); } interface EventListener { void onEvent(Event e); } interface Event { int getStatus(); } }
Da Instanzen innerer Klassen implizite Verweise auf Instanzen äußerer Klassen enthalten, veröffentlicht ThisEscape beim Veröffentlichen des EventListener implizit auch die ThisEscape-Instanz selbst. Zu diesem Zeitpunkt wurde der Variablenstatus jedoch noch nicht initialisiert, was dazu führte, dass diese Referenz im Konstruktor verloren ging. Sie können einen privaten Konstruktor und eine öffentliche Factory-Methode verwenden, um fehlerhafte Konstruktionsprozesse zu vermeiden:
public class SafeListener { private int status; private final EventListener listener; private SafeListener() { listener = new EventListener() { public void onEvent(Event e) { doSomething(e); } }; status = 1; } public static SafeListener newInstance(EventSource source) { SafeListener safe = new SafeListener(); source.registerListener(safe.listener); return safe; } void doSomething(Event e) { status = e.getStatus(); } interface EventSource { void registerListener(EventListener e); } interface EventListener { void onEvent(Event e); } interface Event { int getStatus(); } }
Eine Möglichkeit, die Verwendung der Synchronisierung zu vermeiden, besteht darin, nicht zu teilen. Wenn auf Daten nur innerhalb eines einzelnen Threads zugegriffen wird, ist keine Synchronisierung erforderlich, was als Thread-Schließung bezeichnet wird. Die Thread-Eindämmung ist eine Überlegung bei der Programmierung und muss im Programm implementiert werden. Java bietet auch einige Mechanismen zur Aufrechterhaltung des Thread-Schließens, z. B. lokale Variablen und ThreadLocal.
Ad-hoc-Thread-Schließung bedeutet, dass die Verantwortung für die Aufrechterhaltung des Thread-Schließens vollständig von der Programmimplementierung getragen wird. Die Verwendung flüchtiger Variablen ist eine Möglichkeit, einen Ad-hoc-Thread-Schließung zu erreichen. Solange sichergestellt ist, dass nur ein einzelner Thread Schreibvorgänge für gemeinsam genutzte flüchtige Variablen ausführt, ist es sicher, „Lesen-Ändern-Schreiben“-Vorgänge für diese Variablen durchzuführen . Die Sichtbarkeit flüchtiger Variablen stellt sicher, dass andere Threads den neuesten Wert sehen können.
Ad-hoc-Thread-Schließung ist sehr fragil, also verwenden Sie sie so wenig wie möglich in Ihrem Programm. Verwenden Sie nach Möglichkeit andere Thread-Eindämmungstechniken wie Stack-Eindämmung und ThreadLocal.
Bei der Stapelschließung kann auf Objekte nur über lokale Variablen zugegriffen werden. Sie befinden sich auf dem Stapel des Ausführungsthreads und können von anderen Threads nicht aufgerufen werden. Auch wenn diese Objekte nicht threadsicher sind, sind sie dennoch threadsicher. Es ist jedoch zu beachten, dass nur die Person, die den Code schreibt, weiß, welche Objekte im Stapel eingeschlossen sind. Ohne klare Anweisungen können spätere Betreuer diese Objekte leicht versehentlich preisgeben.
使用ThreadLocal是一种更规范的线程封闭方式,它能是线程中的某个值与保存值的对象关联起来。如下代码,通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接:
public class ConnectionDispenser { static String DB_URL = "jdbc:mysql://localhost/mydatabase"; private ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { public Connection initialValue() { try { return DriverManager.getConnection(DB_URL); } catch (SQLException e) { throw new RuntimeException("Unable to acquire Connection, e"); } }; }; public Connection getConnection() { return connectionHolder.get(); } }
从概念上看,你可以将ThreadLocal8742468051c85b06f0a0af9e3e506b5c视为包含了Mapdd13f7ef6939263b34f16ddd764e4ff9对象,其中保存了特定于改线程的值,但ThreadLocal的实现并非如此。这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾被回收。
如果某个对象在被创建后其状态就不能被修改,那么这个对象就被称为不可变对象。满足同步需求的另一种方法就是使用不可变对象。不可变对象一定是线程安全的。当满足以下条件时,对象才是不可变的:
对象创建以后其状态就不能改变
对象的所有域都是final类型
对象是正确创建的,在对象创建期间,this引用没有泄露
public final class ThreeStooges { private final Set<String> stooges = new HashSet<String>(); public ThreeStooges() { stooges.add("Moe"); stooges.add("Larry"); stooges.add("Curly"); } public boolean isStooge(String name) { return stooges.contains(name); } }
上述代码中,尽管stooges对象是可变的,但在它构造完成后无法对其修改。stooges是一个final类型的引用变量,因此所有的对象状态都通过一个final域访问。在构造函数中,this引用不能被除了构造函数之外的代码访问到。
final类型的域是不能修改的,但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的。final域的对象在构造函数中不会被重排序,所以final域也能保证初始化过程的安全性。和“除非需要更高的可见性,否则应将所有的域都声明为私用域”一样,“除非需要某个域是可变的,否则应将其声明为final域”也是一个良好的编程习惯。
因式分解Sevlet将执行两个原子操作:
更新缓存
通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的结果
每当需要一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据:
public class OneValueCache { private final BigInteger lastNumber; private final BigInteger[] lastFactors; public OneValueCache(BigInteger i, BigInteger[] factors) { lastNumber = i; lastFactors = Arrays.copyOf(factors, factors.length); } public BigInteger[] getFactors(BigInteger i) { if (lastNumber == null || !lastNumber.equals(i)) return null; else return Arrays.copyOf(lastFactors, lastFactors.length); } }
当线程获取了不可变对象的引用后,不必担心另一个线程会修改对象的状态。如果要更新这些变量,可以创建一个新的容器对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。当一个线程将volatile类型的cache设置为引用一个新的OneValueCache时,其他线程就会立即看到新缓存的数据:
public class VolatileCachedFactorizer implements Servlet { private volatile OneValueCache cache = new OneValueCache(null, null); public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = cache.getFactors(i); if (factors == null) { factors = factor(i); cache = new OneValueCache(i, factors); } encodeIntoResponse(resp, factors); } }
像这样将对象引用保存到公有域中就是不安全的:
public Holder holder; public void initialize(){ holder = new Holder(42); }
由于存在可见性问题,其他线程看到的Holder对象将处于不一致的状态。除了发布对象的线程外,其他线程可以看到Holder域是一个失效值,因此将看到一个空引用或者之前的旧值。
public class Holder { private int n; public Holder(int n) { this.n = n; } public void assertSanity() { if (n != n) throw new AssertionError("This statement is false."); } }
上述代码,即使Holder对象被正确的发布,assertSanity也有可能抛出AssertionError。因为线程看到Holder引用的值是最新的,但由于重排序Holder状态的值却是时效的。
即使在发布不可变对象的引用时没有使用同步,也仍然可以安全地访问该对象。任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。在没有额外同步的情况下,也可以安全地访问final类型的域。然而,如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全发布:
在静态初始化函数里初始化一个对象引用。
将对象的引用保存到volatile类型的域或者AtomicReference对象中。
将对象的引用保存到某个正确构造对象的final类型域中。
将对象的引用保存到一个由锁保护的域中。
线程安全库中的容器类提供了以下的安全发布保证:
通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程。
通过将某个对象放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中,可以将该对象安全地发布到任何从这些容器中访问该对象的线程。
Indem Sie ein Objekt in eine BlockingQueue oder ConcurrentLinkedQueue stellen, können Sie das Objekt sicher in jedem Thread veröffentlichen, der über diese Warteschlangen auf das Objekt zugreift.
Wenn ein Objekt technisch veränderbar ist, sein Zustand sich jedoch nach seiner Freigabe nicht ändert, dann wird ein solches Objekt als de facto unveränderliches Objekt bezeichnet. Ein de facto unveränderliches Objekt, das sicher veröffentlicht wird, kann von jedem Thread ohne zusätzliche Synchronisierung sicher verwendet werden. Verwalten Sie beispielsweise ein Map-Objekt, das die letzte Anmeldezeit jedes Benutzers speichert:
public Map7c36a245a97922f78d3224cbec5818e1 lastLogin =
Collections.synchronizedMap(new HashMap
Für veränderliche Objekte ist die Synchronisierung nicht nur beim Veröffentlichen des Objekts erforderlich, sondern muss auch bei jedem Zugriff auf das Objekt verwendet werden, um die Sichtbarkeit nachfolgender Änderungsvorgänge sicherzustellen. Die Veröffentlichungsanforderungen eines Objekts hängen von seiner Veränderbarkeit ab:
Unveränderliche Objekte können durch jeden Mechanismus veröffentlicht werden.
Fakten Unveränderliche Objekte müssen auf sichere Weise veröffentlicht werden.
Veränderbare Objekte müssen auf sichere Weise freigegeben werden und müssen threadsicher oder mit einer Sperre geschützt sein.
Es gibt einige praktische Strategien, die Sie bei der Verwendung und Freigabe von Objekten in gleichzeitigen Programmen verwenden können, darunter:
Thread-Beschränkung. Ein in einem Thread eingeschlossenes Objekt kann nur einem Thread gehören. Das Objekt ist in diesem Thread eingeschlossen und kann nur von diesem Thread geändert werden.
Schreibgeschütztes Teilen. Ohne zusätzliche Synchronisierung können mehrere Threads gleichzeitig auf ein gemeinsam genutztes schreibgeschütztes Objekt zugreifen, es kann jedoch von keinem Thread geändert werden. Zu den gemeinsam genutzten schreibgeschützten Objekten gehören unveränderliche Objekte und de facto unveränderliche Objekte.
Thread-sicheres Teilen. Ein Thread-sicheres Objekt wird intern synchronisiert, sodass mehrere Threads ohne weitere Synchronisierung über die öffentliche Schnittstelle des Objekts darauf zugreifen können.
Objekt schützen. Auf geschützte Objekte kann nur mit einer bestimmten Sperre zugegriffen werden. Zu den geschützten Objekten gehören Objekte, die in anderen Thread-sicheren Objekten gekapselt sind, sowie Objekte, die durch eine bestimmte Sperre freigegeben und geschützt sind.
Das obige ist der detaillierte Inhalt vonGemeinsame Implementierung von Objekten in der gleichzeitigen Java-Programmierung. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!