Heim  >  Artikel  >  Java  >  Java-Thread (2) – Detaillierte Erläuterung der Thread-Synchronisation

Java-Thread (2) – Detaillierte Erläuterung der Thread-Synchronisation

黄舟
黄舟Original
2017-03-01 11:31:191255Durchsuche


Um die Ausführungsgeschwindigkeit des Codes zu beschleunigen, verwenden wir eine Multithreading-Methode. Die parallele Ausführung macht den Code zwar effizienter, das damit verbundene Problem besteht jedoch darin, dass im Programm viele Threads gleichzeitig ausgeführt werden. Wenn sie ein Objekt gleichzeitig ändern, kann dies zu einer Beschädigung führen Zeit Wir benötigen einen Synchronisationsmechanismus, um diese Threads zu verwalten.

(1) Wettbewerbsbedingungen

Ich erinnere mich, dass es im Betriebssystem ein Bild gab, das mich tief beeindruckte. Was oben gezeichnet ist, ist ein Prozessblock, der in diese Prozesse unterteilt ist. Alle diese Threads verweisen auf die Ressourcen des Prozesses. Das Gleiche gilt für Java. Ressourcen werden von Threads gemeinsam genutzt, anstatt dass jeder Thread über eine unabhängige Ressource verfügt. In dieser Sharing-Situation ist es sehr wahrscheinlich, dass mehrere Threads gleichzeitig auf eine Ressource zugreifen. Dieses Phänomen wird als Race Condition bezeichnet.

In einem Bankensystem verwaltet jeder Thread ein Konto, und diese Threads können Übertragungsvorgänge durchführen.
Wenn ein Thread eine Operation ausführt, speichert er zunächst den Kontostand im Register. Im zweiten Schritt reduziert er die Zahl im Register um den zu überweisenden Geldbetrag Gleichgewicht.
Das Problem besteht darin, dass, wenn dieser Thread die Schritte 1 und 2 abschließt, ein anderer Thread aktiviert wird und den Kontostandwert des ersten Threads ändert, der erste Thread jedoch zu diesem Zeitpunkt nichts davon weiß. Nachdem der erste Thread darauf gewartet hat, dass die Ausführung des zweiten Threads abgeschlossen ist, fährt er mit seinem dritten Schritt fort: dem Zurückschreiben des Ergebnisses in den Kontostand. Zu diesem Zeitpunkt wird der Vorgang des zweiten Threads gelöscht, sodass der gesamte Geldbetrag im gesamten System definitiv als Fehler gesendet wird.
Dies ist eine unerwünschte Situation, wenn Java-Race-Bedingungen auftreten.

(2) ReentrantLock-Klasse

Das obige Beispiel zeigt uns, dass unsere Operation unterbrochen wird, wenn sie keine atomare Operation ist Es wird definitiv passieren, auch wenn die Wahrscheinlichkeit manchmal wirklich sehr gering ist, kann man es nicht ausschließen. Wir können unseren Code nicht wie im Betriebssystem in atomare Operationen umwandeln, sondern können unseren Code sperren, um die Sicherheit zu gewährleisten. Wenn wir in einem gleichzeitigen Programm auf Daten zugreifen möchten, setzen wir zunächst eine Sperre für unseren Code. Während wir die Sperre verwenden, scheinen die an unserem Code beteiligten Ressourcen „gesperrt“ zu sein, bis wir darauf zugreifen können Öffne dieses Schloss.

In Java verfügen sowohl das synchronisierte Schlüsselwort als auch die ReentrantLock-Klasse über diese Sperrfunktion. Lassen Sie uns hier zunächst die Funktionen von ReentrantLcok besprechen.

1. ReentrantLock-Konstruktor

In dieser Klasse werden zwei Konstruktoren bereitgestellt, einer ist der Standardkonstruktor, es gibt nichts zu sagen, und der andere ist ein Fairness-Konstruktor für die Strategie. Diese faire Strategie ist erstens viel langsamer als normale Sperren und zweitens ist sie in manchen Fällen nicht wirklich fair. Und wenn wir wirklich ohne besonderen Grund eine faire Strategie brauchen, versuchen Sie, diese Strategie nicht zu studieren.

2. Erfassung und Freigabe

ReentrantLock myLock = new ReentrantLock();
//创建对象
myLock.lock();
//获取锁try{...}
finally{
myLock.unlock();
//释放锁
}

Denken Sie unbedingt daran, die Sperre endgültig aufzuheben! ! Wir haben bereits gesagt, dass ungeprüfte Fehler zur Thread-Beendigung führen können. Ein unerklärlicher Abbruch stoppt den Ablauf des Programms. Erfolgt die Freigabe nicht endgültig, wird die Sperre nie aufgehoben. Dieses Prinzip ist das gleiche wie bei der Verwendung des Pakets .close() in unserem täglichen Rahmen. Apropos Schließen: Wenn wir eine Sperre verwenden, können wir keine „Try-Anweisung mit Ressourcen“ verwenden, da die Sperre nicht durch Schließen geschlossen wird. Wenn Sie nicht wissen, was eine try-Anweisung mit Ressourcen ist, tun Sie einfach so, als hätte ich es nicht gesagt.

3. Sperren sind wiedereintrittsfähig

Wenn Sie Sperren in rekursiven oder zyklischen Programmen verwenden möchten, können Sie diese gerne verwenden. Die ReentrantLock-Sperre ist wiedereintrittsfähig. Sie speichert die Anzahl der Aufrufe bei jedem Aufruf von lock(). Jeder Sperrenaufruf muss mit unlock freigegeben werden.

(3) Bedingte Objekte

Normalerweise finden Threads ein Problem, nachdem sie den kritischen Abschnitt betreten haben. Die Ressourcen, die sie benötigen, werden in anderen Objekten verwendet Zu diesem Zeitpunkt müssen wir ein Bedingungsobjekt verwenden, um diese Threads zu verwalten, die eine Sperre erhalten haben, aber keine nützliche Arbeit leisten können.

if(a>b){    a.set(b-1);}

1. „Sie haben sich selbst gefangen“

Das Obige ist ein sehr einfaches bedingtes Urteil, aber wir können es in gleichzeitigen Programmen nicht so schreiben. Das Problem besteht darin, dass ein anderer Thread aktiviert wird, kurz nachdem dieser Thread die Entscheidung getroffen hat, und der andere Thread nach der Operation a kleiner als b macht (die Bedingung in der if-Anweisung ist nicht mehr korrekt).

那么这个时候我们可能想到,我们把整个if语句直接放在锁里面,确保自己的代码不会被打断。但是这样又存在一个问题,如果if判断是false,那么if中的语句不会被执行。但是如果我们需要去执行if中的语句,甚至我们要一直等待if判断变的正确之后去执行if中的语句,这时,我们突然发现,if语句再也不会变得正确了,因为我们的锁把这个线程锁死,其他的线程没办法访问临界区并修改a和b的值让if判断变得正确,这真的是非常尴尬,我们自己的锁把我们自己困住了,我们出不去,别人进不来。

2.Condition类

为了解决这种情况,我们用ReentrantLock类中的newCondition方法来获取一个条件对象。

Condition cd = myLock.newCondition();

获取了Condition对象之后,我们就应该来研究这个对象有什么方法和作用了。先不急于看API,我们回到主题发现现在亟待解决的就是if条件判断的问题,我们如何才能:在已经上锁的情况下,发现if判断错误时,给其他线程机会并自己一直等着if判断变回正确

Condition类就是为了解决这个难题而生的,有了Condition类之后,我们在if语句下面直接跟上await方法,这个方法表示这个线程被阻塞,并放弃了锁,等其他的线程来操作。

注意在这里我们用的名词是阻塞,我们之前也说过阻塞和等待有很大不同:等待获得锁时,一旦锁有了空闲,他可以自动的去获得锁,而阻塞获得锁时,即使有空闲的锁,也要等待线程调度器允许他去持有锁的时候才能获得锁。

其他的线程在顺利执行if语句内容之后,要去调用signalAll方法,这个方法将会重新去激活所有的因为这个条件被阻塞的线程,让这些线程重新获得机会,这些线程被允许从被阻塞的地方继续进行。此时,线程应该再次测试该条件,如果还是不能满足条件,需要再次重复上述操作。

ReentrantLock myLock = new ReentrantLock();
//创建锁对象myLock.lock();
//给下面的临界区上锁
Condition cd = myLock.newCondition();
//创建一个Condition对象,这个cd对象表示条件对象while(!(a>b))
    cd.await();
    //上面的while循环和await方法调用是标准写法
    //如果不能满足if的条件,那么他将进入阻塞状态,放弃锁,等待别人去激活它a.set(b-1);
    //一直等到从while循环出来,满足了判断的条件,我们执行自己的功能cd.signalAll();
    //最后一定不能忘记调用signalAll方法去激活其他的被阻塞的线程
    //如果所有的线程都在等待其他线程signalAll,则进入死锁

非常不妙的,如果所有的线程都在等待其他线程signalAll,则进入死锁的状态。死锁状态是指所有的线程需要的资源都被其他的线程形成环状结构而导致谁都不能执行的情况。最后调用signalAll方法激活其他因为cd而阻塞的“兄弟”是必须的,方便你我他,减少死锁的发生。

3.Condition对象和锁总结

总结来说,Condition对象和锁有这样几个特点。

  1. 锁可以用来保护代码片段,任何时刻只能有一个线程进入被保护的区域

  2. 锁可以管理试图进入临界区的线程

  3. 锁可以拥有一个或多个条件对象

  4. 每个条件对象管理那些因为前面所描述的原因而不能被执行但已经进入被保护代码段的线程

(四)synchronized关键字

我们上面介绍的ReentrantLock和Condition对象是一种用来保护代码片段的方法,在java中还有另外一种机制:通过使用关键字synchronized来修饰方法,从而给方法添加一个内部锁。从版本开始,java的每一个对象都有一个内部锁,每个内部锁会保护那些被synchronized修饰的方法。也就是说,如果想调用这个方法,首先要获得内部的对象锁。

1.synchronized与ReentrantLock比较

我们先拿出上面的代码:

public void function(){
    ReentrantLock myLock = new ReentrantLock();
    myLock.lock();

    Condition cd = myLock.newCondition();    while(!(a>b))
        cd.await();

    a.set(b-1);

    cd.signalAll();
}

如果我们用synchronized来实现这段代码,将会变成下面的样子:

public synchronized void function(){
    while(!(a>b))
        wait();

    a.set(b-1);

    notifyAll();
}

需要我们注意的是,在使用synchronized关键词时,无需再去用ReentrantLock和Condition对象,我们用wait方法替换了await方法,notifyAll方法替换了signalAll方法。这样写确实比之前的简单了很多。

2.静态方法的synchronized

将静态方法声明为synchronized也是合法的。如果调用这种方法,将会获取相关的类对象的内部锁。比如我们调用Test类中的静态方法,这时,Test.class对象的锁将被锁住。

3.内部锁和条件的局限性

内部锁虽然简便,但是他存在着很多限制:

  1. 不能中断一个正在试图获得锁的线程

  2. 试图获得锁时不能设定超时

  3. 因为不能通过Condition来实例化条件。每个锁仅有单一的条件,可能是不够的

在代码中应该使用这两种锁中的哪一种呢?Lock和Condition对象还是同步方法?在core java一书中有一些建议:

  1. Am besten verwenden Sie weder ReentrantLock noch das synchronisierte Schlüsselwort. In vielen Fällen können Sie das Paket java.util.concurrent verwenden

  2. Wenn synchronisiert Ihre Codeanforderungen erfüllt, verwenden Sie es bitte zuerst

  3. bis If Sie benötigen insbesondere ReentrantLcok, verwenden Sie es

Um die Ausführungsgeschwindigkeit des Codes zu beschleunigen, verwenden wir eine Multithreading-Methode. Die parallele Ausführung macht den Code zwar effizienter, das damit verbundene Problem besteht jedoch darin, dass im Programm viele Threads gleichzeitig ausgeführt werden. Wenn sie ein Objekt gleichzeitig ändern, kann dies zu einer Beschädigung führen Zeit Wir benötigen einen Synchronisationsmechanismus, um diese Threads zu verwalten.

Das Obige ist der Inhalt des Java-Threads (2) – ausführliche Erläuterung der Thread-Synchronisierung. Weitere verwandte Inhalte finden Sie auf der chinesischen PHP-Website (www.php.cn)!


Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn