Java-Thread-Synchronisierung
Wenn zwei oder mehr Threads Ressourcen gemeinsam nutzen müssen, müssen sie auf irgendeine Weise feststellen, dass die Ressource zu einem bestimmten Zeitpunkt nur von einem Thread belegt ist. Der Prozess, um dies zu erreichen, wird als Synchronisation bezeichnet. Wie Sie sehen, bietet Java hierfür eine einzigartige Unterstützung auf Sprachebene.
Der Schlüssel zur Synchronisation ist das Konzept eines Monitors (auch Semaphor genannt). Ein Monitor ist ein sich gegenseitig ausschließendes gesperrtes Objekt oder Mutex. Nur ein Thread kann den Monitor zu einem bestimmten Zeitpunkt abrufen. Wenn ein Thread eine Sperre benötigt, muss er in den Monitor eintreten. Alle anderen Threads, die versuchen, in eine verschlossene Röhre einzudringen, müssen hängen bleiben, bis der erste Thread die Röhre verlässt. Diese anderen Threads werden Wartemonitore genannt. Ein Thread, der einen Monitor besitzt, kann bei Bedarf denselben Monitor erneut betreten.
Wenn Sie die Synchronisierung in anderen Sprachen wie C oder C++ verwendet haben, wissen Sie, dass die Verwendung etwas seltsam ist. Dies liegt daran, dass viele Sprachen selbst keine Synchronisierung unterstützen. Im Gegensatz dazu muss das Programm für synchronisierte Threads die Quellsprache des Betriebssystems verwenden. Glücklicherweise implementiert Java die Synchronisierung über Sprachelemente und der größte Teil der mit der Synchronisierung verbundenen Komplexität entfällt.
Sie können Code auf zwei Arten synchronisieren. Beide beinhalten die Verwendung des synchronisierten Schlüsselworts. Die beiden Methoden werden unten erläutert.
Synchronisierte Methoden verwenden
Die Synchronisierung in Java ist einfach, da alle Objekte über entsprechende implizite Monitore verfügen. Wenn Sie den Monitor eines Objekts betreten, rufen Sie die durch das synchronisierte Schlüsselwort geänderte Methode auf. Wenn sich ein Thread in einer synchronisierten Methode befindet, müssen alle anderen Threads derselben Instanz, die versuchen, diese Methode (oder andere synchronisierte Methoden) aufzurufen, warten. Um den Monitor zu verlassen und die Kontrolle über das Objekt an andere wartende Threads abzugeben, kehrt der Thread, dem der Monitor gehört, einfach von der synchronisierten Methode zurück.
Um die Notwendigkeit der Synchronisierung zu verstehen, beginnen wir mit einem einfachen Beispiel, bei dem die Synchronisierung verwendet werden sollte, dies aber nicht der Fall ist. Das folgende Programm hat drei einfache Klassen. Das erste ist Callme, das über eine einfache Methode call() verfügt. Die call()-Methode verfügt über einen String-Parameter namens msg. Diese Methode versucht, die Nachrichtenzeichenfolge in eckigen Klammern auszugeben. Das Interessante ist, dass nach dem Aufruf von call() zum Drucken der linken Klammer und des Nachrichtenstrings Thread.sleep(1000) aufgerufen wird, was den aktuellen Thread für 1 Sekunde anhält.
Der Konstruktor der nächsten Klasse, Caller, verweist auf eine Instanz von Callme und einen String, die jeweils in target und msg gespeichert sind. Der Konstruktor erstellt außerdem einen neuen Thread, der die run()-Methode des Objekts aufruft. Der Thread startet sofort. Die run()-Methode der Caller-Klasse ruft die call()-Methode des Callme-Instanzziels über den Parameter msg string auf. Schließlich erstellt die Synch-Klasse zunächst eine einfache Instanz von Callme und drei Instanzen von Caller mit unterschiedlichen Nachrichtenzeichenfolgen.
Die gleiche Instanz von Callme wird an jede Caller-Instanz übergeben.
// This program is not synchronized. class Callme { void call(String msg) { System.out.print("[" + msg); try { Thread.sleep(1000); } catch(InterruptedException e) { System.out.println("Interrupted"); } System.out.println("]"); } } class Caller implements Runnable { String msg; Callme target; Thread t; public Caller(Callme targ, String s) { target = targ; msg = s; t = new Thread(this); t.start(); } public void run() { target.call(msg); } } class Synch { public static void main(String args[]) { Callme target = new Callme(); Caller ob1 = new Caller(target, "Hello"); Caller ob2 = new Caller(target, "Synchronized"); Caller ob3 = new Caller(target, "World"); // wait for threads to end try { ob1.t.join(); ob2.t.join(); ob3.t.join(); } catch(InterruptedException e) { System.out.println("Interrupted"); } } }
Die Ausgabe dieses Programms ist wie folgt:
Hello[Synchronized[World] ] ]
In diesem Beispiel ermöglicht die call()-Methode den Übergang der Ausführung zu einem anderen Thread durch den Aufruf von sleep(). Das Ergebnis ist eine gemischte Ausgabe der drei Nachrichtenzeichenfolgen. In diesem Programm gibt es keine Methode, die verhindert, dass drei Threads gleichzeitig dieselbe Methode desselben Objekts aufrufen. Dies ist eine Racebedingung, da drei Threads um den Abschluss der Methode konkurrieren. Das Beispiel verwendet sleep( ), um diesen Effekt wiederholbar und offensichtlich zu machen. In den meisten Fällen sind Konflikte komplexer und unvorhersehbar, da Sie nicht sicher sein können, wann ein Kontextwechsel stattfindet. Dies führt dazu, dass das Programm manchmal einwandfrei läuft und manchmal fehlschlägt.
Um den Zweck des obigen Beispiels zu erreichen, müssen Sie das Recht haben, call() kontinuierlich zu verwenden. Das heißt, zu einem bestimmten Zeitpunkt darf es nur auf einen Thread beschränkt sein, der es dominieren kann. Dazu müssen Sie lediglich das Schlüsselwort synchronisiert vor der Definition von call() wie folgt hinzufügen:
class Callme { synchronized void call(String msg) { ...
Dies verhindert die Verwendung von call ( ), wenn andere Threads call() eingeben. Nachdem synchronisiert vor call() hinzugefügt wurde, lautet die Programmausgabe wie folgt:
[Hello] [Synchronized] [World]
Jeder Zeitpunkt in einer Multithread-Situation Eine oder mehrere Methoden Methoden, die den internen Zustand eines Objekts manipulieren, müssen das synchronisierte Schlüsselwort verwenden, um einen Zustandswettbewerb zu verhindern. Denken Sie daran: Sobald ein Thread eine synchronisierte Methode einer Instanz eingibt, kann kein anderer Thread eine synchronisierte Methode derselben Instanz eingeben. Es können jedoch weiterhin andere asynchrone Methoden der Instanz aufgerufen werden.
Synchronisationsanweisung
Obwohl das Erstellen einer Synchronisationsmethode innerhalb der erstellten Klasse eine einfache und effektive Möglichkeit ist, eine Synchronisation zu erreichen, funktioniert es nicht immer. Bitte denken Sie über die Gründe dafür nach. Angenommen, Sie möchten synchronisierten Zugriff auf ein Klassenobjekt erhalten, das nicht für den Multithread-Zugriff ausgelegt ist, d. h. die Klasse verwendet nicht die synchronisierte Methode. Darüber hinaus wurde die Klasse nicht von Ihnen, sondern von einem Dritten erstellt, und Sie können ihren Quellcode nicht erhalten. Auf diese Weise können Sie den synchronisierten Modifikator nicht vor der entsprechenden Methode hinzufügen. Wie kann ein Objekt dieser Klasse synchronisiert werden? Glücklicherweise ist die Lösung einfach: Sie fügen einfach die Aufrufe der von dieser Klasse definierten Methoden in einen synchronisierten Block ein.
Das Folgende ist die übliche Form einer synchronisierten Anweisung:
synchronized(object) { // statements to be synchronized }
其中,object是被同步对象的引用。如果你想要同步的只是一个语句,那么不需要花括号。一个同步块确保对object成员方法的调用仅在当前线程成功进入object管程后发生。
下面是前面程序的修改版本,在run( )方法内用了同步块:
// This program uses a synchronized block. class Callme { void call(String msg) { System.out.print("[" + msg); try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("Interrupted"); } System.out.println("]"); } } class Caller implements Runnable { String msg; Callme target; Thread t; public Caller(Callme targ, String s) { target = targ; msg = s; t = new Thread(this); t.start(); } // synchronize calls to call() public void run() { synchronized(target) { // synchronized block target.call(msg); } } } class Synch1 { public static void main(String args[]) { Callme target = new Callme(); Caller ob1 = new Caller(target, "Hello"); Caller ob2 = new Caller(target, "Synchronized"); Caller ob3 = new Caller(target, "World"); // wait for threads to end try { ob1.t.join(); ob2.t.join(); ob3.t.join(); } catch(InterruptedException e) { System.out.println("Interrupted"); } } }
这里,call( )方法没有被synchronized修饰。而synchronized是在Caller类的run( )方法中声明的。这可以得到上例中同样正确的结果,因为每个线程运行前都等待先前的一个线程结束。
Java线程间通信
多线程通过把任务分成离散的和合乎逻辑的单元代替了事件循环程序。线程还有第二优点:它远离了轮询。轮询通常由重复监测条件的循环实现。一旦条件成立,就要采取适当的行动。这浪费了CPU时间。举例来说,考虑经典的序列问题,当一个线程正在产生数据而另一个程序正在消费它。为使问题变得更有趣,假设数据产生器必须等待消费者完成工作才能产生新的数据。在轮询系统,消费者在等待生产者产生数据时会浪费很多CPU周期。一旦生产者完成工作,它将启动轮询,浪费更多的CPU时间等待消费者的工作结束,如此下去。很明显,这种情形不受欢迎。
为避免轮询,Java包含了通过wait( ),notify( )和notifyAll( )方法实现的一个进程间通信机制。这些方法在对象中是用final方法实现的,所以所有的类都含有它们。这三个方法仅在synchronized方法中才能被调用。尽管这些方法从计算机科学远景方向上来说具有概念的高度先进性,实际中用起来是很简单的:
wait( ) 告知被调用的线程放弃管程进入睡眠直到其他线程进入相同管程并且调用notify( )。
notify( ) 恢复相同对象中第一个调用 wait( ) 的线程。
notifyAll( ) 恢复相同对象中所有调用 wait( ) 的线程。具有最高优先级的线程最先运行。
这些方法在Object中被声明,如下所示:
final void wait( ) throws InterruptedException final void notify( ) final void notifyAll( )
wait( )存在的另外的形式允许你定义等待时间。
下面的例子程序错误的实行了一个简单生产者/消费者的问题。它由四个类组成:Q,设法获得同步的序列;Producer,产生排队的线程对象;Consumer,消费序列的线程对象;以及PC,创建单个Q,Producer,和Consumer的小类。
// An incorrect implementation of a producer and consumer. class Q { int n; synchronized int get() { System.out.println("Got: " + n); return n; } synchronized void put(int n) { this.n = n; System.out.println("Put: " + n); } } class Producer implements Runnable { Q q; Producer(Q q) { this.q = q; new Thread(this, "Producer").start(); } public void run() { int i = 0; while(true) { q.put(i++); } } } class Consumer implements Runnable { Q q; Consumer(Q q) { this.q = q; new Thread(this, "Consumer").start(); } public void run() { while(true) { q.get(); } } } class PC { public static void main(String args[]) { Q q = new Q(); new Producer(q); new Consumer(q); System.out.println("Press Control-C to stop."); } }
尽管Q类中的put( )和get( )方法是同步的,没有东西阻止生产者超越消费者,也没有东西阻止消费者消费同样的序列两次。这样,你就得到下面的错误输出(输出将随处理器速度和装载的任务而改变):
Put: 1 Got: 1 Got: 1 Got: 1 Got: 1 Got: 1 Put: 2 Put: 3 Put: 4 Put: 5 Put: 6 Put: 7 Got: 7
生产者生成1后,消费者依次获得同样的1五次。生产者在继续生成2到7,消费者没有机会获得它们。
用Java正确的编写该程序是用wait( )和notify( )来对两个方向进行标志,如下所示:
// A correct implementation of a producer and consumer. class Q { int n; boolean valueSet = false; synchronized int get() { if(!valueSet) try { wait(); } catch(InterruptedException e) { System.out.println("InterruptedException caught"); } System.out.println("Got: " + n); valueSet = false; notify(); return n; } synchronized void put(int n) { if(valueSet) try { wait(); } catch(InterruptedException e) { System.out.println("InterruptedException caught"); } this.n = n; valueSet = true; System.out.println("Put: " + n); notify(); } } class Producer implements Runnable { Q q; Producer(Q q) { this.q = q; new Thread(this, "Producer").start(); } public void run() { int i = 0; while(true) { q.put(i++); } } } class Consumer implements Runnable { Q q; Consumer(Q q) { this.q = q; new Thread(this, "Consumer").start(); } public void run() { while(true) { q.get(); } } } class PCFixed { public static void main(String args[]) { Q q = new Q(); new Producer(q); new Consumer(q); System.out.println("Press Control-C to stop."); } }
内部get( ), wait( )被调用。这使执行挂起直到Producer 告知数据已经预备好。这时,内部get( ) 被恢复执行。获取数据后,get( )调用notify( )。这告诉Producer可以向序列中输入更多数据。在put( )内,wait( )挂起执行直到Consumer取走了序列中的项目。当执行再继续,下一个数据项目被放入序列,notify( )被调用,这通知Consumer它应该移走该数据。
下面是该程序的输出,它清楚的显示了同步行为:
Put: 1 Got: 1 Put: 2 Got: 2 Put: 3 Got: 3 Put: 4 Got: 4 Put: 5 Got: 5
更多深入解析Java的线程同步以及线程间通信相关文章请关注PHP中文网!