Maison  >  Article  >  Java  >  Quelles sont les méthodes d’implémentation du verrouillage en Java ?

Quelles sont les méthodes d’implémentation du verrouillage en Java ?

WBOY
WBOYavant
2023-05-12 08:37:051563parcourir

1. Verrouillage pessimiste

Comme son nom l'indique, il fait référence à une attitude conservatrice envers la modification des données et à la conviction que d'autres modifieront également les données. Par conséquent, lors de l'exploitation des données, les données seront verrouillées jusqu'à ce que l'opération soit terminée. Dans la plupart des cas, le verrouillage pessimiste s'appuie sur le mécanisme de verrouillage de la base de données pour garantir une exclusivité maximale des opérations. Si le temps de verrouillage est trop long, les autres utilisateurs ne pourront pas y accéder pendant une longue période, ce qui affectera l'accès simultané au programme. En même temps, cela aura également un impact important sur les performances de la base de données. pour les transactions longues, ces frais généraux sont souvent insupportables.

S'il s'agit d'un système autonome, nous pouvons utiliser le propre mot-clé synchronisé de JAVA pour verrouiller les ressources en l'ajoutant à la méthode ou au bloc synchronisé. S'il s'agit d'un système distribué, nous pouvons utiliser le propre mécanisme de verrouillage de la base de données pour y parvenir.

select * from 表名 where id= #{id} for update

Lors de l'utilisation du verrouillage pessimiste, nous devons faire attention au niveau de verrouillage. Lorsque MySQL innodb se verrouille, seule la clé primaire ou (champ d'index) est explicitement spécifiée, sinon le verrouillage de la table sera exécuté ; la table entière sera verrouillée en direct, les performances seront médiocres à ce moment-là. Lors de l'utilisation du verrouillage pessimiste, nous devons désactiver l'attribut autocommit de la base de données MySQL car mysql utilise le mode autocommit par défaut. Le verrouillage pessimiste convient aux scénarios dans lesquels il y a de nombreuses écritures et où les exigences de performances de concurrence ne sont pas élevées.

2. Verrouillage optimiste

Le verrouillage optimiste, comme vous pouvez le deviner au sens littéral, est très optimiste lors de l'exploitation des données, pensant que d'autres ne modifieront pas les données en même temps, donc le verrouillage optimiste ne se verrouillera pas uniquement lors de la soumission de mises à jour. . Les conflits de données seront formellement détectés. Si un conflit est détecté, un message d'erreur est renvoyé et l'utilisateur peut décider quoi faire, mécanisme de défaillance rapide. Sinon, effectuez cette opération.

Il est divisé en trois étapes : lecture des données, vérification de l'écriture et écriture des données.

S'il s'agit d'un système autonome, nous pouvons l'implémenter sur la base du CAS de JAVA. CAS est une opération atomique qui est implémentée à l'aide de la comparaison et de l'échange de matériel.

S'il s'agit d'un système distribué, nous pouvons ajouter un champ de numéro de version à la table de la base de données, tel que la version.

update 表 
set ... , version = version +1 
where id= #{id} and version = #{version}

Avant l'opération, lisez d'abord le numéro de version de l'enregistrement lors de la mise à jour, comparez les numéros de version via les instructions SQL pour voir s'ils sont cohérents. Si cela est cohérent, mettez à jour les données. Sinon, la version sera relue et l'opération ci-dessus sera réessayée.

3. Les verrous distribués

synchronisés, ReentrantLock, etc. en JAVA résolvent tous le problème d'exclusion mutuelle des ressources du déploiement d'applications uniques sur une seule machine. Avec le développement rapide des affaires, lorsqu'une seule application évolue vers un cluster distribué, les multi-threads et multi-processus sont distribués sur différentes machines, et la stratégie originale de verrouillage du contrôle de concurrence sur une seule machine devient invalide

À l'heure actuelle, nous avons besoin pour introduire des verrous distribués pour résoudre le problème du contrôle de concurrence entre machines. Le mécanisme d'exclusion mutuelle de la machine contrôle l'accès aux ressources partagées.

Quelles conditions sont requises pour les verrous distribués :

  • La même fonction d'exclusion mutuelle des ressources qu'un système autonome, qui est la base des verrous

  • Acquisition et libération de verrous haute performance

  • Haute disponibilité

  • Avec Reentrancy

  • Dispose d'un mécanisme de défaillance de verrouillage pour éviter les blocages

  • Non bloquant, que le verrou soit obtenu ou non, il doit pouvoir revenir rapidement

Il y a de nombreuses méthodes d'implémentation, basées sur la base de données, Redis et Zookeeper, etc., nous parlons ici de l'implémentation traditionnelle basée sur Redis :

Verrouillage

SET key unique_value  [EX seconds] [PX milliseconds] [NX|XX]

Grâce aux commandes atomiques, si l'exécution est réussie et que 1 est renvoyé, cela signifie que le verrouillage est réussi. Remarque : unique_value est un identifiant unique généré par le client pour distinguer les opérations de verrouillage des différents clients. Portez une attention particulière au déverrouillage. Déterminez d'abord si unique_value est un client verrouillé. Si tel est le cas, le déverrouillage et la suppression sont autorisés. Après tout, nous ne pouvons pas supprimer les verrous ajoutés par d'autres clients.

Déverrouillage : le déverrouillage comporte deux opérations de commande, qui nécessitent l'utilisation de scripts Lua pour garantir l'atomicité.

// 先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

Grâce aux hautes performances de Redis, Redis implémente des verrous distribués qui sont également la méthode d'implémentation courante actuelle. Mais tout a des avantages et des inconvénients. Si le serveur verrouillé tombe en panne et que le nœud esclave n'a pas eu le temps de sauvegarder les données, d'autres clients peuvent également obtenir le verrou.

Afin de résoudre ce problème, Redis a officiellement conçu un verrou distribué Redlock.

Idée de base : laissez le client demander des verrous avec plusieurs nœuds Redis indépendants en parallèle. Si l'opération de verrouillage peut être effectuée avec succès sur plus de la moitié des nœuds, alors nous considérons que le client a réussi à obtenir le verrou distribué, sinon le verrouillage a échoué. .

4. Verrouillage réentrant

Le verrouillage réentrant, également appelé verrouillage récursif, signifie que lorsque le même thread appelle la méthode externe pour acquérir le verrou, il acquerra automatiquement le verrou lorsqu'il entrera dans la méthode interne.

Il y a un compteur à l'intérieur du verrou d'objet ou du verrou de classe. Chaque fois qu'un thread obtient le verrou, le compteur +1 lors du déverrouillage, le compteur -1 ;

Le nombre de verrous correspond au nombre de déverrouillages. Les verrous et les déverrouillages apparaissent par paires.

ReentrantLock et synchronisé en Java sont tous deux des verrous réentrants. L’un des avantages des verrous réentrants est que les blocages peuvent être évités dans une certaine mesure.

5、自旋锁

自旋锁是采用让当前线程不停地在循环体内执行,当循环的条件被其他线程改变时才能进入临界区。自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不断增加时,性能下降明显,因为每个线程都需要执行,会占用CPU时间片。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

自旋锁缺点:

  • 可能引发死锁。

  • 可能占用 CPU 的时间过长。

我们可以设置一个 循环时间 或 循环次数,超出阈值时,让线程进入阻塞状态,防止线程长时间占用 CPU 资源。JUC 并发包中的 CAS 就是采用自旋锁,compareAndSet 是CAS操作的核心,底层利用Unsafe对象实现的。

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

如果内存中 var1 对象的var2字段值等于预期的 var5,则将该位置更新为新值(var5 + var4),否则不进行任何操作,一直重试,直到操作成功为止。

CAS 包含了Compare和Swap 两个操作,如何保证原子性呢?CAS 是由 CPU 支持的原子操作,其原子性是在硬件层面进行控制。

特别注意,CAS 可能导致 ABA 问题,我们可以引入递增版本号来解决。

6、独享锁

独享锁,也有人叫它排他锁。无论读操作还是写操作,只能有一个线程获得锁,其他线程处于阻塞状态。

缺点:读操作并不会修改数据,而且大部分的系统都是 读多写少,如果读读之间互斥,大大降低系统的性能。下面的 共享锁 会解决这个问题。

像Java中的 ReentrantLock 和 synchronized 都是独享锁。

7、共享锁

共享锁是指允许多个线程同时持有锁,一般用在读锁上。读锁的共享锁可保证并发读是非常高效的。读写,写读 ,写写的则是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。

8、读锁/写锁

如果对某个资源是读操作,那多个线程之间并不会相互影响,可以通过添加读锁实现共享。如果有修改动作,为了保证数据的并发安全,此时只能有一个线程获得锁,我们称之为 写锁。读读是共享的;而 读写、写读 、写写 则是互斥的。

像 Java中的 ReentrantReadWriteLock 就是一种 读写锁。

9、公平锁/非公平锁

公平锁:多个线程按照申请锁的顺序去获得锁,所有线程都在队列里排队,先来先获取的公平性原则。

优点:所有的线程都能得到资源,不会饿死在队列中。

缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,CPU 唤醒下一个阻塞线程有系统开销。

Quelles sont les méthodes d’implémentation du verrouillage en Java ?

非公平锁:多个线程不按照申请锁的顺序去获得锁,而是同时以插队方式直接尝试获取锁,获取不到(插队失败),会进入队列等待(失败则乖乖排队),如果能获取到(插队成功),就直接获取到锁。

优点:可以减少 CPU 唤醒线程的开销,整体的吞吐效率会高点。

缺点:可能导致队列中排队的线程一直获取不到锁或者长时间获取不到锁,活活饿死。

Java 多线程并发操作,我们操作锁大多时候都是基于 Sync 本身去实现的,而 Sync 本身却是 ReentrantLock 的一个内部类,Sync 继承 AbstractQueuedSynchronizer。

像 ReentrantLock 默认是非公平锁,我们可以在构造函数中传入 true,来创建公平锁。

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

10、可中断锁/不可中断锁

可中断锁:指一个线程因为没有获得锁在阻塞等待过程中,可以中断自己阻塞的状态。不可中断锁:恰恰相反,如果锁被其他线程获取后,当前线程只能阻塞等待。如果持有锁的线程一直不释放锁,那其他想获取锁的线程就会一直阻塞。

内置锁 synchronized 是不可中断锁,而 ReentrantLock 是可中断锁。

ReentrantLock获取锁定有三种方式:

  • lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于阻塞状态,直到该线程获取锁。

  • tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false。

  • tryLock(long timeout,TimeUnit unit), si le verrou est obtenu, il retournera vrai immédiatement. Si un autre thread détient le verrou, il attendra le temps donné par le. paramètre Pendant le processus d'attente, si le verrou est acquis, il renvoie vrai, si le délai d'attente expire, il renvoie faux.

  • lockInterruptible(), si le verrou est acquis, il revient immédiatement ; si le verrou n'est pas acquis, le thread est bloqué jusqu'à ce que le verrou soit acquis ou que le thread soit interrompu par un autre fil.

11. Serrure segmentée

La serrure segmentée est en fait une conception de serrure, le but est d'affiner la granularité de la serrure, pas une conception spécifique. sorte de verrou, pour ConcurrentHashMap, sa mise en œuvre de la concurrence consiste à réaliser des opérations simultanées efficaces sous la forme de verrous segmentés.

Le verrou de segment dans ConcurrentHashMap est appelé Segment, qui est une structure similaire à HashMap (l'implémentation de HashMap dans JDK7), c'est-à-dire qu'il a un tableau Entry en interne, et chaque élément du tableau est une liste chaînée ; En même temps, c'est un ReentrantLock (le segment hérite de ReentrantLock).

Lorsque vous devez mettre un élément, il ne verrouille pas l'intégralité du HashMap, mais sait d'abord dans quel segment le mettre via le hashcode, puis verrouille ce segment, donc lorsqu'il est multithread Lors de la mise , l'insertion parallèle est prise en charge tant qu'ils ne sont pas placés dans le même segment.

12. Mise à niveau du verrouillage (pas de verrouillage|verrouillage biaisé|verrouillage léger|verrouillage lourd)

Avant JDK 1.6, le verrouillage synchronisé était encore un verrouillage lourd avec une efficacité relativement faible. Cependant, après JDK 1.6, la JVM a été optimisée et synchronisée afin d'améliorer l'efficacité de l'acquisition et de la libération des verrous, et a introduit des verrous biaisés et des verrous légers. À partir de ce moment-là, il existe quatre états de verrouillage : pas de verrouillage, verrouillage biaisé et verrouillage de niveau léger. , serrure lourde. Ces quatre États seront progressivement revalorisés avec la concurrence et ne pourront pas être déclassés.

Quelles sont les méthodes d’implémentation du verrouillage en Java ?

无码

Lock-free ne verrouille pas les ressources. Tous les threads peuvent accéder et modifier la même ressource, mais uniquement. un thread peut le modifier avec succès en même temps. C'est ce que nous appelons souvent le verrouillage optimiste.

biased lock

est biaisé vers le premier thread à accéder au verrou Lorsque le bloc de code synchronisé est exécuté pour la première fois, l'indicateur de verrouillage dans l'en-tête de l'objet est modifié. via CAS, et l'objet de verrouillage devient un verrou de biais.

Lorsqu'un thread accède à un bloc de code synchronisé et acquiert un verrou, l'ID du thread du biais de verrouillage sera stocké dans Mark Word. Lorsque le thread entre et sort du bloc synchronisé, il ne se verrouille et ne se déverrouille plus via les opérations CAS, mais détecte si le Mark Word stocke un verrou de biais pointant vers le thread actuel. L'acquisition et la libération de verrous légers reposent sur plusieurs instructions atomiques CAS, tandis que les verrous biaisés n'ont besoin que d'une seule instruction atomique CAS lors du remplacement de ThreadID.

Après avoir exécuté le bloc de code de synchronisation, le thread ne libérera pas activement le verrou de biais. Lorsque le thread exécute le bloc de code de synchronisation pour la deuxième fois, le thread déterminera si le thread détenant le verrou à ce moment-là est lui-même (l'ID du thread détenant le verrou est également dans l'en-tête de l'objet), et si c'est le cas, il exécutera normalement. Étant donné que le verrou n'a pas été libéré auparavant, il n'est pas nécessaire de le reverrouiller ici. Le verrou polarisé n'entraîne pratiquement aucune surcharge supplémentaire et présente des performances extrêmement élevées.

Verrouillage du biais Ce n'est que lorsque d'autres fils tentent de rivaliser pour le verrouillage du biais que le fil qui maintient le verrouillage du biais libérera le verrou. Le fil ne libérera pas activement le verrouillage du biais. Concernant la révocation des verrous biaisés, vous devez attendre le point de sécurité global, c'est-à-dire que lorsqu'aucun bytecode n'est exécuté à un certain moment, il mettra d'abord en pause le thread qui possède le verrou biaisé, puis déterminera si le L'objet verrouillé est verrouillé. Si le thread n'est pas actif, l'en-tête de l'objet est défini sur un état sans verrouillage et le verrou biaisé est révoqué, revenant à l'état sans verrouillage (le bit d'indicateur est 01) ou de verrouillage léger (le bit d'indicateur est 00).

Le verrouillage biaisé signifie que lorsqu'un morceau de code synchronisé est toujours accédé par le même thread, c'est-à-dire lorsqu'il n'y a pas de concurrence entre plusieurs threads, alors le thread acquiert automatiquement le verrou lors des accès ultérieurs, réduisant ainsi le coût d'acquisition d'une serrure.

Verrou léger

Le verrou actuel est un verrou de biais Si plusieurs threads sont en compétition pour le verrou en même temps, le verrou de biais sera mis à niveau vers un verrou léger. Les verrous légers pensent que même si la concurrence existe, idéalement, le degré de concurrence est très faible et que le verrou s'acquiert par rotation.

Il existe deux situations pour obtenir des mèches légères :

  • Lorsque la fonction de verrouillage de biais est désactivée.

  • Plusieurs fils en compétition pour le verrou de biais entraînent la mise à niveau du verrou de biais vers un verrou léger. Une fois qu'un deuxième thread rejoint la compétition de verrouillage, le verrou biaisé est mis à niveau vers un verrou léger (spin lock).

Continuez la compétition de verrouillage dans l'état de verrouillage léger. Les fils qui n'ont pas saisi le verrou tourneront et boucleront continuellement pour déterminer si le verrou peut être acquis avec succès. L'opération d'acquisition du verrou consiste en fait à modifier l'indicateur de verrouillage dans l'en-tête de l'objet via CAS. Comparez d'abord si l'indicateur de verrouillage actuel est "libéré", et si c'est le cas, définissez-le sur "verrouillé". Ce processus est atomique. Si le verrou est saisi, le thread modifie les informations actuelles du détenteur du verrou.

重量级锁

如果线程的竞争很激励,线程的自旋超过了一定次数(默认循环10次,可以通过虚拟机参数更改),将轻量级锁升级为重量级锁(依然是 CAS  修改锁标志位,但不修改持有锁的线程ID),当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。

重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资。

13、锁优化技术(锁粗化、锁消除)

锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

举个例子:有个循环体,内部。

for(int i=0;i<size;i++){
    synchronized(lock){
        ...业务处理,省略
    }
}

经过锁粗化的代码如下:

synchronized(lock){
    for(int i=0;i<size;i++){
        ...业务处理,省略
    }
}

锁消除指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。

锁消除的依据是逃逸分析的数据支持,如 StringBuffer 的 append() 方法,或 Vector 的 add() 方法,在很多情况下是可以进行锁消除的,比如以下这段代码:

public String method() {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < 10; i++) {
        sb.append("i:" + i);
    }
    return sb.toString();
}

以上代码经过编译之后的字节码如下:

Quelles sont les méthodes d’implémentation du verrouillage en Java ?

从上述结果可以看出,之前我们写的线程安全的加锁的 StringBuffer 对象,在生成字节码之后就被替换成了不加锁不安全的 StringBuilder 对象了,原因是 StringBuffer 的变量属于一个局部变量,并且不会从该方法中逃逸出去,所以我们可以使用锁消除(不加锁)来加速程序的运行。

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer