Maison >Java >javaDidacticiel >Thread Java (2) - Explication détaillée de la synchronisation des threads
Afin d'accélérer la vitesse d'exécution du code, nous adoptons une méthode multi-threading. L'exécution parallèle rend le code plus efficace, mais le problème qui en découle est que de nombreux threads s'exécutent en même temps dans le programme. S'ils modifient un objet en même temps, cela risque de provoquer une corruption. temps où nous Un mécanisme de synchronisation est nécessaire pour gérer ces threads.
Je me souviens que dans le système d'exploitation, il y avait une image qui m'avait profondément impressionné. Ce qui est dessiné ci-dessus est un bloc de processus. Plusieurs threads sont divisés en ces processus. Tous ces threads pointent vers les ressources du processus de manière unifiée. La même chose est vraie en Java. Les ressources sont partagées entre les threads au lieu que chaque thread ait une ressource indépendante. Dans ce type de situation de partage, il est très probable que plusieurs threads accèdent à une ressource en même temps. Ce phénomène est appelé condition de concurrence.
Dans un système bancaire, chaque thread gère un compte, et ces threads peuvent effectuer des opérations de transfert.
Lorsqu'un thread effectue une opération, il stocke d'abord le solde du compte dans le registre. Dans la deuxième étape, il réduit le nombre dans le registre du montant d'argent à transférer. Dans la troisième étape, il réécrit le résultat dans le registre. équilibre.
Le problème est que lorsque ce thread termine les étapes 1 et 2, un autre thread est réveillé et modifie la valeur du solde du compte du premier thread, mais le premier thread n'en est pas informé pour le moment. Une fois que le premier thread attend que le deuxième thread termine son exécution, il passe à sa troisième étape : réécrire le résultat dans la balance. À ce stade, cela efface le fonctionnement du deuxième thread, de sorte que le montant total d'argent dans l'ensemble du système sera définitivement envoyé comme une erreur.
Il s'agit d'une situation indésirable lorsque des conditions de concurrence Java se produisent.
L'exemple ci-dessus nous dit que si notre opération n'est pas une opération atomique, être interrompue est Cela arrivera certainement, même si la probabilité est parfois très faible, cela ne peut être exclu. Nous ne pouvons pas transformer notre code en opérations atomiques comme dans le système d'exploitation, ce que nous pouvons faire est de verrouiller notre code pour garantir la sécurité. Dans un programme concurrent, si nous voulons accéder aux données, nous définissons d'abord un verrou sur notre code. Pendant que nous utilisons le verrou, les ressources impliquées dans notre code semblent être "verrouillées". Les autres threads ne peuvent y accéder. ouvrez cette serrure.
En Java, le mot-clé synchronisé et la classe ReentrantLock ont tous deux cette fonction de verrouillage. Discutons d'abord des fonctions de ReentrantLcok ici.
Dans cette classe, deux constructeurs sont fournis, l'un est le constructeur par défaut, il n'y a rien à dire, et l'autre est avec équité Constructeur pour la stratégie. Cette stratégie équitable est, tout d’abord, beaucoup plus lente que les verrouillages normaux, et deuxièmement, elle n’est pas vraiment équitable dans certains cas. Et si nous avons vraiment besoin d’une stratégie équitable sans raison particulière, essayez de ne pas étudier cette stratégie.
ReentrantLock myLock = new ReentrantLock(); //创建对象 myLock.lock(); //获取锁try{...} finally{ myLock.unlock(); //释放锁 }
N'oubliez pas de déverrouiller enfin ! ! Nous avons déjà dit que des erreurs non contrôlées peuvent entraîner l'arrêt du thread. Une interruption inexplicable empêchera le programme de s'exécuter vers le bas. Si la version n'est pas finalement placée, le verrou ne sera jamais libéré. Ce principe est le même que lorsque nous utilisons le forfait .close()
dans notre cadre quotidien. En parlant de fermeture, il convient de mentionner que lorsque nous utilisons un verrou, nous ne pouvons pas utiliser une "instruction try avec ressources" car le verrou n'est pas fermé avec close. Si vous ne savez pas ce qu'est une instruction try avec des ressources, faites comme si je ne l'avais pas dit.
Si vous souhaitez utiliser des verrous dans des programmes récursifs ou cycliques, n'hésitez pas à les utiliser. Le verrou ReentrantLock est réentrant. Il maintient un décompte pour enregistrer le nombre de fois qu'il est appelé à chaque fois que lock() est appelé. Chaque appel de verrou doit être libéré avec le déverrouillage.
Habituellement, les threads trouvent un problème après avoir verrouillé et entré dans la section critique. Les ressources dont ils ont besoin sont utilisées ou utilisées. ne remplissent pas les conditions de leur exécution. A ce stade, nous devons utiliser un objet condition pour gérer ces threads qui ont obtenu un verrou mais ne peuvent pas faire de travail utile.
if(a>b){ a.set(b-1);}
Ce qui précède est un jugement conditionnel très simple, mais nous ne pouvons pas l'écrire ainsi dans des programmes concurrents. Le problème est que si un autre thread est réveillé juste après que ce thread a rendu le jugement, et que l'autre thread fait a inférieur à b après l'opération (la condition dans l'instruction if n'est plus correcte).
那么这个时候我们可能想到,我们把整个if语句直接放在锁里面,确保自己的代码不会被打断。但是这样又存在一个问题,如果if判断是false,那么if中的语句不会被执行。但是如果我们需要去执行if中的语句,甚至我们要一直等待if判断变的正确之后去执行if中的语句,这时,我们突然发现,if语句再也不会变得正确了,因为我们的锁把这个线程锁死,其他的线程没办法访问临界区并修改a和b的值让if判断变得正确,这真的是非常尴尬,我们自己的锁把我们自己困住了,我们出不去,别人进不来。
为了解决这种情况,我们用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而阻塞的“兄弟”是必须的,方便你我他,减少死锁的发生。
总结来说,Condition对象和锁有这样几个特点。
锁可以用来保护代码片段,任何时刻只能有一个线程进入被保护的区域
锁可以管理试图进入临界区的线程
锁可以拥有一个或多个条件对象
每个条件对象管理那些因为前面所描述的原因而不能被执行但已经进入被保护代码段的线程
我们上面介绍的ReentrantLock和Condition对象是一种用来保护代码片段的方法,在java中还有另外一种机制:通过使用关键字synchronized来修饰方法,从而给方法添加一个内部锁。从版本开始,java的每一个对象都有一个内部锁,每个内部锁会保护那些被synchronized修饰的方法。也就是说,如果想调用这个方法,首先要获得内部的对象锁。
我们先拿出上面的代码:
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方法。这样写确实比之前的简单了很多。
将静态方法声明为synchronized也是合法的。如果调用这种方法,将会获取相关的类对象的内部锁。比如我们调用Test类中的静态方法,这时,Test.class对象的锁将被锁住。
内部锁虽然简便,但是他存在着很多限制:
不能中断一个正在试图获得锁的线程
试图获得锁时不能设定超时
因为不能通过Condition来实例化条件。每个锁仅有单一的条件,可能是不够的
在代码中应该使用这两种锁中的哪一种呢?Lock和Condition对象还是同步方法?在core java一书中有一些建议:
Il est préférable de n'utiliser ni ReentrantLock ni le mot-clé synchronisé. Dans de nombreux cas, vous pouvez utiliser le package java.util.concurrent
Si synchronisé répond à vos besoins en code, veuillez l'utiliser d'abord
jusqu'à ce que Si vous avez particulièrement besoin de ReentrantLcok, utilisez-le
Afin d'accélérer la vitesse d'exécution du code, nous adoptons une méthode multi-threading. L'exécution parallèle rend le code plus efficace, mais le problème qui en découle est que de nombreux threads s'exécutent en même temps dans le programme. S'ils modifient un objet en même temps, cela risque de provoquer une corruption. temps où nous Un mécanisme de synchronisation est nécessaire pour gérer ces threads.
Ce qui précède est le contenu du fil Java (2) - explication détaillée de la synchronisation des threads. Pour plus de contenu connexe, veuillez faire attention au site Web PHP chinois (www.php.cn) !