Je me souviens quand j'ai commencé à apprendre Java, chaque fois que je rencontrais le multi-threading, la synchronisation était utilisée. Par rapport à nous à cette époque, la synchronisation était si magique et puissante qu'à cette époque, nous lui donnions un nom "synchronisé". qui est également devenu notre solution éprouvée aux situations multithread. Cependant, au fur et à mesure que notre étude progresse, nous savons que le verrouillage synchronisé est un verrou lourd. Par rapport au verrouillage, il apparaîtra si encombrant que nous le penserons peu efficace et l'abandonnerons lentement.
C'est vrai qu'avec les différentes optimisations du synchronisé dans Javs SE 1.6, le synchronisé ne paraîtra pas si lourd. Suivons LZ pour explorer le mécanisme de mise en œuvre de synchronisé, comment Java l'optimise, le mécanisme d'optimisation des verrous, la structure de stockage des verrous et le processus de mise à niveau
synchronisé Il peut ; assurez-vous que lorsqu'une méthode ou un bloc de code est en cours d'exécution, une seule méthode peut accéder à la section critique en même temps. Cela peut également garantir la visibilité de la mémoire des variables partagées
Chaque objet en Java peut être utilisé. en tant que verrou, c'est la base de la mise en œuvre synchronisée de la synchronisation :
1. Méthode de synchronisation ordinaire, le verrou est l'objet d'instance actuel
2 Méthode de synchronisation statique, le verrou est l'objet de classe de la classe actuelle<.>3. Bloc de méthode de synchronisation, le verrou est Objets entre parenthèses
public class SynchronizedTest { public synchronized void test1(){ } public void test2(){ synchronized (this){ } } }
Comme le montre Comme ci-dessus, le bloc de code de synchronisation est implémenté à l'aide des instructions monitorenter et monitorexit, la méthode de synchronisation (il n'est pas évident ici que vous deviez regarder l'implémentation sous-jacente de la JVM) repose sur l'implémentation ACC_SYNCHRONIZED sur le modificateur de méthode.
Bloc de code synchronisé : L'instruction Monitorenter est insérée au début du bloc de code synchronisé et l'instruction Monitorexit est insérée à la fin du bloc de code synchronisé. La JVM doit s'assurer que chaque Monitorenter. a une sortie de moniteur qui lui correspond. Tout objet est associé à un moniteur. Lorsqu'un moniteur est maintenu, il sera dans un état verrouillé. Lorsque le thread exécute l'instruction monitorenter, il tentera d'obtenir la propriété du moniteur correspondant à l'objet, c'est-à-dire tentera d'obtenir le verrou de l'objet
Méthode synchronisée : La méthode synchronisée sera traduite en ; un appel et un retour de méthode normaux. Les instructions telles que : les instructions invocationvirtual et areturn n'ont pas d'instructions spéciales au niveau du bytecode de la VM pour implémenter la méthode modifiée par synchronisé. Au lieu de cela, la position de l'indicateur synchronisé dans le champ access_flags de la méthode est définie sur 1. dans la table des méthodes du fichier Class. Indique que la méthode est une méthode synchronisée et utilise l'objet qui appelle la méthode ou la classe à laquelle la méthode appartient pour représenter Klass comme objet de verrouillage dans l'objet interne de la JVM. (Extrait de : http://www.php.cn/)
Marquer le mot.
Mark Word est utilisé pour stocker les données d'exécution de l'objet lui-même, telles que le code de hachage (HashCode), l'âge de génération GC, l'indicateur d'état de verrouillage, le verrou détenu par le thread, l'ID de thread biaisé, l'horodatage biaisé, etc. Les en-têtes d'objet Java occupent généralement deux codes machine (dans une machine virtuelle 32 bits, 1 code machine équivaut à 4 octets, soit 32 bits), mais si l'objet est de type tableau, trois codes machine sont nécessaires, car le virtuel JVM machine peut La taille de l'objet Java est déterminée par les informations de métadonnées de l'objet Java, mais la taille du tableau ne peut pas être confirmée à partir des métadonnées du tableau, un bloc est donc utilisé pour enregistrer la longueur du tableau. La figure suivante est la structure de stockage de l'en-tête de l'objet Java (machine virtuelle 32 bits) :
Les informations d'en-tête de l'objet sont un coût de stockage supplémentaire qui n'a rien à voir avec les données définies par l'objet lui-même, mais compte tenu de l'efficacité spatiale de la machine virtuelle, Mark Word est conçu comme une structure de données non fixe pour stocker autant de données que possible dans un très petit espace. Il réutilisera son propre espace de stockage en fonction de l'état de l'objet. . En d'autres termes, Mark Word changera au fur et à mesure de l'exécution du programme Change, l'état du changement est le suivant (machine virtuelle 32 bits) :
Une brève introduction à l'en-tête de l'objet Java. , regardons ensuite le moniteur.
Qu'est-ce que Monitor ? Nous pouvons le comprendre comme un outil de synchronisation, ou il peut être décrit comme un mécanisme de synchronisation. Il est généralement décrit comme un objet.
Tout comme tout est un objet, tous les objets Java naissent des moniteurs. Chaque objet Java a le potentiel de devenir un moniteur, car dans la conception de Java, chaque objet Java sort de l'utérus avec une poignée de moniteurs. le verrou manquant est appelé verrou interne ou verrou du moniteur.
Monitor est une structure de données privée par thread. Chaque thread a une liste d'enregistrements de surveillance disponibles, et il existe également une liste globale disponible. Chaque objet verrouillé est associé à un moniteur (le LockWord dans le MarkWord de l'en-tête de l'objet pointe vers l'adresse de départ du moniteur. En même temps, il y a un champ Propriétaire dans le moniteur qui stocke l'identifiant unique du thread qui). possède le verrou, indiquant que le verrou appartient à ce thread occupé. Sa structure est la suivante :
Propriétaire : Initialement NULL signifie qu'aucun thread ne possède actuellement l'enregistrement de surveillance. Lorsque le thread possède avec succès le verrou, l'identité unique du thread est enregistrée. Lorsque le verrou est Il est défini sur NULL lorsqu'il est libéré ;
EntryQ : associe un verrouillage mutex système (sémaphore), bloquant tous les threads qui ne parviennent pas à verrouiller l'enregistrement du moniteur.
RcThis : Indique le nombre de tous les threads bloqués ou en attente sur l'enregistrement du moniteur.
Nest : Utilisé pour implémenter le comptage des verrous de réentrée.
HashCode : enregistre la valeur HashCode copiée à partir de l'en-tête de l'objet (peut également inclure l'âge GC).
Candidat : utilisé pour éviter un blocage inutile ou l'attente du réveil des threads, car un seul thread peut posséder le verrou à la fois, si le thread précédent qui libère le verrou réveille tous les threads Les threads bloquants ou en attente entraîneront des changements de contexte inutiles (de bloqué à prêt, puis à nouveau bloqués en raison de l'échec de verrous concurrents), entraînant une grave dégradation des performances. Candidate n'a que deux valeurs possibles : 0 signifie qu'aucun thread ne doit être réveillé ; 1 signifie qu'un thread successeur doit être réveillé pour concourir pour le verrou.
Extrait de : Principes d'implémentation et applications de synchronisé en Java)
Nous savons que synchronisé est un verrou lourd et n'est pas très efficace. En même temps, ce concept a toujours été dans nos esprits, mais la mise en œuvre de. synchronisé dans jdk 1.6 a été révisé. Diverses optimisations ont été apportées pour le rendre moins lourd. Alors, quelles méthodes d'optimisation la JVM a-t-elle adoptées ?
jdk1.6 a introduit un grand nombre d'optimisations dans la mise en œuvre des verrous, telles que les verrous rotatifs, les verrous rotatifs adaptatifs, l'élimination des verrous, le grossissement des verrous, les verrous biaisés et la légèreté. Des techniques telles que le verrouillage par niveau sont utilisées pour réduire la surcharge des opérations de verrouillage.
Les verrous existent principalement dans quatre états, à savoir : l'état sans verrouillage, l'état de verrouillage biaisé, l'état de verrouillage léger et l'état de verrouillage lourd. Ils seront progressivement mis à niveau avec la concurrence féroce. Notez que les verrous peuvent être mis à niveau mais pas dégradés. Cette stratégie vise à améliorer l’efficacité de l’acquisition et de la libération des verrous.
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。
何谓自旋锁?
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。
自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;
如果通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。
JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。
为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。
如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法:
public void vectorTest(){ Vector<String> vector = new Vector<String>(); for(int i = 0 ; i < 10 ; i++){ vector.add(i + ""); } System.out.println(vector); }
在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。
我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
在大多数的情况下,上述观点是正确的,LZ也一直坚持着这个观点。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。
L'objectif principal de l'introduction de verrous légers est de réduire la consommation de performances causée par les verrous lourds traditionnels utilisant des mutex du système d'exploitation sans concurrence multithread. Lorsque la fonction de verrouillage de biais est désactivée ou que plusieurs threads sont en compétition pour le verrouillage de biais et que le verrouillage de biais est mis à niveau vers un verrou léger, une tentative sera effectuée pour acquérir le verrou léger. Les étapes sont les suivantes :
. Acquérir le verrou
1 . Déterminez si l'objet actuel est dans un état sans verrouillage (hashcode, 0, 01). Si tel est le cas, la JVM créera d'abord un espace nommé Lock Record dans le cadre de pile du. fil actuel pour stocker l'état actuel de l'objet de verrouillage. Une copie du mot de marque (le fonctionnaire ajoute un préfixe déplacé à cette copie, c'est-à-dire le mot de marque déplacé, sinon, effectuez l'étape (3) ; La JVM utilise l'opération CAS pour essayer de mettre à jour le mot de marque de l'objet afin qu'il pointe vers la correction de l'enregistrement de verrouillage. Si elle réussit, cela signifie que le verrou est en compétition, puis l'indicateur de verrouillage sera remplacé par 00 (indiquant que cet objet est dans un état de verrouillage léger), et l'opération de synchronisation sera effectuée ; si elle échoue, l'étape (3) sera effectuée
3 Déterminer si le mot de marque pointe vers le cadre de pile du thread actuel ; si tel est le cas, cela signifie que le thread actuel détient déjà le verrou de l'objet actuel, et le bloc de code de synchronisation sera exécuté directement, sinon cela signifie uniquement que l'objet verrou a été préempté par d'autres threads, et il est léger à ce niveau ; le temps. Le verrou de niveau doit être étendu en un verrou lourd, l'indicateur de verrouillage devient 10 et le thread en attente plus tard entrera dans l'état de blocage
Relâchez le verrou Le il en va de même pour la libération des verrous légers. Elle s'effectue via l'opération CAS. Les principales étapes sont les suivantes :
1. Récupérer les données enregistrées dans le mot de marque déplacé après avoir obtenu le verrou léger ; l'opération CAS pour remplacer le mot de marque de l'objet actuel par les données récupérées, si elle réussit, cela signifie que le verrou est libéré avec succès, sinon exécutez (3) ; que d'autres threads tentent d'acquérir le verrou et que le thread suspendu doit être réveillé lors de la libération du verrou.
Pour les serrures légères, la base de l'amélioration des performances est "pour la plupart des serrures, il n'y aura pas de concurrence pendant tout le cycle de vie". Si cette base est rompue, en plus des frais généraux d'exclusion mutuelle. il existe des opérations CAS supplémentaires, donc dans le cas d'une concurrence multithread, les verrous légers sont plus lents que les verrous lourds
La figure suivante montre le processus d'acquisition et de libération des verrous légers
Verrouillage biaisé
L'objectif principal de l'introduction du verrouillage biaisé est de minimiser les chemins d'exécution de verrous légers inutiles sans concurrence multithread. Comme mentionné ci-dessus, les opérations de verrouillage et de déverrouillage des verrous légers nécessitent plusieurs instructions atomiques CAS. Alors, comment le verrouillage biaisé réduit-il les opérations CAS inutiles ? Nous pouvons le comprendre en examinant la structure du travail de Mark. Il vous suffit de vérifier s'il s'agit d'un verrou biaisé, l'ID du verrou et le ThreadID. Le flux de traitement est le suivant :
Vérifiez si le mot Mark est présent. un état biaisable, c'est-à-dire s'il s'agit d'un verrou biaisé. 1. L'indicateur de verrouillage est 01
1 s'il est dans l'état polarisé, testez si l'ID du thread est l'ID du thread actuel. étape (5), sinon effectuez l'étape (3) ; 1. Si l'ID de fil n'est pas l'ID de fil actuel, rivalisez pour le verrouillage via l'opération CAS. Si la compétition réussit, remplacez l'ID de fil de Mark Word par. l'ID de thread actuel, sinon exécutez le thread (4); 4. Échec de la compétition pour le verrou via CAS , prouvant qu'il existe actuellement une situation de concurrence multithread. Lorsque le point de sécurité global est atteint, le thread qui a obtenu. le verrou biaisé est suspendu, le verrou biaisé est mis à niveau vers un verrou léger, puis le thread bloqué au point sûr continue d'exécuter le bloc de code de synchronisation
5 Exécutez le bloc de code synchronisé
<.>Relâchez le verrou
La libération du verrou de biais adopte un mécanisme selon lequel seule la concurrence libérera le verrou. Le fil ne prendra pas l'initiative de libérer le biais. Le verrou doit attendre que d'autres fils soient en compétition. La révocation du verrou biaisé doit attendre le point de sécurité global (ce point temporel correspond au moment où il n'y a pas de code en cours d'exécution). Les étapes sont les suivantes :
2. Annulez le verrouillage biaisé et revenez à l'état sans verrouillage (01) ou. Statut du verrou léger ;
La figure suivante montre le processus d'acquisition et de libération des verrous biaisés
Ce qui précède est [Deadly Java Concurrency] ----- une analyse approfondie des principes de mise en œuvre de la synchronisation. Pour plus de contenu connexe, veuillez prêter attention au site Web PHP chinois (www.php.cn. )!