Cet article vous apporte des connaissances pertinentes sur java, qui présente principalement le contenu pertinent sur les verrous, y compris les verrous optimistes, les verrous pessimistes, les verrous exclusifs, les verrous partagés, etc.
Apprentissage recommandé : "tutoriel vidéo Java"
verrouillage pessimiste
correspond aux personnes pessimistes dans la vie, Pessimiste les gens pensent toujours que les choses vont dans la mauvaise direction. 悲观锁
对应于生活中悲观的人,悲观的人总是想着事情往坏的方向发展。
举个生活中的例子,假设厕所只有一个坑位了,悲观锁上厕所会第一时间把门反锁上,这样其他人上厕所只能在门外等候,这种状态就是「阻塞」了。
回到代码世界中,一个共享数据加了悲观锁,那线程每次想操作这个数据前都会假设其他线程也可能会操作这个数据,所以每次操作前都会上锁,这样其他线程想操作这个数据拿不到锁只能阻塞了。
在 Java 语言中 synchronized
和 ReentrantLock
等就是典型的悲观锁,还有一些使用了 synchronized 关键字的容器类如 HashTable
等也是悲观锁的应用。
乐观锁
乐观锁
对应于生活中乐观的人,乐观的人总是想着事情往好的方向发展。
举个生活中的例子,假设厕所只有一个坑位了,乐观锁认为:这荒郊野外的,又没有什么人,不会有人抢我坑位的,每次关门上锁多浪费时间,还是不加锁好了。你看乐观锁就是天生乐观!
回到代码世界中,乐观锁操作数据时不会上锁,在更新的时候会判断一下在此期间是否有其他线程去更新这个数据。
乐观锁可以使用版本号机制
和CAS算法
实现。在 Java 语言中 java.util.concurrent.atomic
包下的原子类就是使用CAS 乐观锁实现的。
两种锁的使用场景
悲观锁和乐观锁没有孰优孰劣,有其各自适应的场景。
乐观锁适用于写比较少(冲突比较小)的场景,因为不用上锁、释放锁,省去了锁的开销,从而提升了吞吐量。
如果是写多读少的场景,即冲突比较严重,线程间竞争激励,使用乐观锁就是导致线程不断进行重试,这样可能还降低了性能,这种场景下使用悲观锁就比较合适。
独占锁
独占锁
是指锁一次只能被一个线程所持有。如果一个线程对数据加上排他锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。
JDK中的synchronized
和java.util.concurrent(JUC)
包中Lock的实现类就是独占锁。
共享锁
共享锁
是指锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据。
在 JDK 中 ReentrantReadWriteLock
就是一种共享锁。
互斥锁
互斥锁
是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。
互斥锁一次只能一个线程拥有互斥锁,其他线程只有等待。
读写锁
读写锁
synchronized
et ReentrantLock
sont des verrous pessimistes typiques. Il existe également certaines classes de conteneurs qui utilisent le mot-clé synchronisé tel que HashTablecode code> etc. sont également des applications de verrouillage pessimiste. 🎜🎜🎜Optimistic Lock🎜🎜🎜<code>Optimistic Lock
correspond aux personnes optimistes dans la vie. Les personnes optimistes pensent toujours que les choses vont dans la bonne direction. 🎜🎜Pour donner un exemple dans la vie, supposons qu'il n'y ait qu'une seule fosse dans les toilettes. Optimistic Lock pense : Il n'y a pas beaucoup de monde dans ce désert, et personne ne m'arrachera ma fosse à chaque fois que je ferme la porte et la verrouille. c'est une perte de temps. Il vaut mieux ne pas le verrouiller. Vous voyez, les mèches optimistes naissent optimistes ! 🎜🎜De retour dans le monde du code, le verrouillage optimiste ne se verrouillera pas lors de l'exploitation des données. Lors de la mise à jour, il déterminera si d'autres threads mettront à jour les données pendant cette période. 🎜🎜🎜🎜Le verrouillage optimiste peut être implémenté à l'aide du mécanisme de numéro de version
et de l'algorithme CAS
. Dans le langage Java, la classe atomique sous le package java.util.concurrent.atomic
est implémentée à l'aide du verrouillage optimiste CAS. 🎜🎜🎜Les scénarios d'utilisation des deux types de verrous🎜🎜🎜Le verrouillage pessimiste et le verrouillage optimiste ne sont ni meilleurs ni pires, ils ont leurs propres scénarios appropriés. 🎜🎜Le verrouillage optimiste convient aux scénarios dans lesquels il y a relativement peu d'écritures (conflits relativement faibles). Comme il n'est pas nécessaire de verrouiller ou de libérer le verrou, la surcharge de verrouillage est éliminée, améliorant ainsi le débit. 🎜🎜S'il s'agit d'un scénario avec plus d'écriture et moins de lecture, c'est-à-dire que le conflit est sérieux et que la concurrence entre les threads est stimulée, l'utilisation du verrouillage optimiste entraînera de nouvelles tentatives du thread, ce qui peut également réduire les performances. il est plus approprié d'utiliser un verrou pessimiste. 🎜🎜Verrou exclusif et verrou partagé🎜🎜🎜Verrou exclusif🎜🎜🎜Verrou exclusif
signifie que le verrou ne peut être détenu que par un seul thread à la fois. Si un thread ajoute un verrou exclusif aux données, les autres threads ne peuvent plus ajouter aucun type de verrou aux données. Un thread qui obtient un verrou exclusif peut à la fois lire et modifier des données. 🎜🎜🎜🎜La classe d'implémentation de Lock dans les packages synchronized
et java.util.concurrent(JUC)
du JDK est un verrou exclusif. 🎜🎜🎜Verrouillage partagé🎜🎜🎜Verrou partagé
signifie que le verrou peut être détenu par plusieurs threads. Si un thread ajoute un verrou partagé aux données, les autres threads peuvent uniquement ajouter des verrous partagés aux données et ne peuvent pas ajouter de verrous exclusifs. Le thread qui obtient le verrou partagé peut uniquement lire les données et ne peut pas les modifier. 🎜🎜🎜🎜Dans JDK, ReentrantReadWriteLock
est une sorte de verrou partagé. 🎜🎜Verrouillage Mutex et verrouillage en lecture-écriture🎜🎜🎜Verrouillage Mutex🎜🎜🎜Verrouillage Mutex
est une implémentation conventionnelle du verrouillage exclusif, ce qui signifie qu'une certaine ressource ne permet qu'à un seul visiteur d'y accéder en même temps L'accès temporel est unique et exclusif. 🎜🎜🎜🎜Verrouillage mutex Un seul thread peut posséder le verrou mutex à la fois, et les autres threads ne peuvent qu'attendre. 🎜🎜🎜Verrouillage en lecture-écriture🎜🎜🎜Verrouillage en lecture-écriture
est une implémentation spécifique du verrouillage partagé. Les verrous en lecture-écriture gèrent un ensemble de verrous, l'un est un verrou en lecture seule et l'autre est un verrou en écriture. 🎜Le verrou en lecture peut être détenu par plusieurs threads en même temps lorsqu'il n'y a pas de verrou en écriture, et le verrou en écriture est exclusif. La priorité d'un verrou en écriture est supérieure à celle d'un verrou en lecture. Un thread qui obtient un verrou en lecture doit être capable de voir le contenu mis à jour par le verrou en écriture précédemment libéré.
Les verrous en lecture-écriture ont un degré de concurrence plus élevé que les verrous mutex. Il n'y a qu'un seul thread d'écriture à la fois, mais plusieurs threads peuvent lire simultanément.
définit une interface de verrouillage en lecture-écriture dans le JDK : ReadWriteLock
ReadWriteLock
public interface ReadWriteLock { /** * 获取读锁 */ Lock readLock(); /** * 获取写锁 */ Lock writeLock(); }
ReentrantReadWriteLock
实现了ReadWriteLock
接口,具体实现这里不展开,后续会深入源码解析。
公平锁
公平锁
是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买,后来的人在队尾排着,这是公平的。
在 java 中可以通过构造函数初始化公平锁
/** * 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁 */ Lock lock = new ReentrantLock(true);
非公平锁
非公平锁
是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转,或者饥饿的状态(某个线程一直得不到锁)。
在 java 中 synchronized 关键字是非公平锁,ReentrantLock默认也是非公平锁。
/** * 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁 */ Lock lock = new ReentrantLock(false);
可重入锁
又称之为递归锁
,是指同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁。
对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁。对于Synchronized而言,也是一个可重入锁。
敲黑板:可重入锁的一个好处是可一定程度避免死锁。
以 synchronized 为例,看一下下面的代码:
public synchronized void mehtodA() throws Exception{ // Do some magic tings mehtodB(); } public synchronized void mehtodB() throws Exception{ // Do some magic tings }
上面的代码中 methodA 调用 methodB,如果一个线程调用methodA 已经获取了锁再去调用 methodB 就不需要再次获取锁了,这就是可重入锁的特性。如果不是可重入锁的话,mehtodB 可能不会被当前线程执行,可能造成死锁。
自旋锁
是指线程在没有获得锁时不是被直接挂起,而是执行一个忙循环,这个忙循环就是所谓的自旋。
自旋锁的目的是为了减少线程被挂起的几率,因为线程的挂起和唤醒也都是耗资源的操作。
如果锁被另一个线程占用的时间比较长,即使自旋了之后当前线程还是会被挂起,忙循环就会变成浪费系统资源的操作,反而降低了整体性能。因此自旋锁是不适应锁占用时间长的并发情况的。
在 Java 中,AtomicInteger
类有自旋的操作,我们看一下代码:
public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); return v; }
CAS 操作如果失败就会一直循环获取当前 value 值然后重试。
另外自适应自旋锁也需要了解一下。
在JDK1.6又引入了自适应自旋,这个就比较智能了,自旋时间不再固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果虚拟机认为这次自旋也很有可能再次成功那就会次序较多的时间,如果自旋很少成功,那以后可能就直接省略掉自旋过程,避免浪费处理器资源。
分段锁
是一种锁的设计,并不是具体的一种锁。
分段锁设计目的是将锁的粒度进一步细化,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
在 Java 语言中 CurrentHashMap 底层就用了分段锁,使用Segment,就可以进行并发使用了。
JDK1.6 为了提升性能减少获得锁和释放锁所带来的消耗,引入了4种锁的状态:无锁
、偏向锁
、轻量级锁
和重量级锁
,它会随着多线程的竞争情况逐渐升级,但不能降级。
无锁
无锁
private static final Object LOCK = new Object(); for(int i = 0;i <code>ReentrantReadWriteLock</code> implémente l'interface <code>ReadWriteLock</code>, implémentation spécifique Nous avons gagné Sans nous développer ici, nous entrerons plus tard dans une analyse approfondie du code source. 🎜<h2>Verrouillage équitable et verrouillage injuste</h2>🎜<strong>Verrouillage équitable</strong>🎜🎜<code>Verrouillage équitable</code> signifie que plusieurs threads acquièrent des verrous dans l'ordre dans lequel ils demandent des verrous, ici Semblable à la file d'attente pour acheter des billets, ceux qui arrivent en premier achètent en premier, et ceux qui viennent plus tard font la queue à la fin. 🎜🎜<img alt="Explication détaillée avec images et texte ! Résumé des verrous en Java" src="https://img.php.cn/upload/article/000/000/067/5cab904b73cc3c2ec71a08b113232b72-6.png">🎜🎜En Java, les verrous équitables peuvent être initialisés via le constructeur🎜<pre class="brush:php;toolbar:false"> synchronized(LOCK){ for(int i = 0;i 🎜<strong>Verrouillage injuste</strong>🎜🎜<code>Verrouillage injuste</code> signifie que l'ordre dans lequel plusieurs threads acquièrent les verrous n'est pas Selon l'ordre d'application des verrous, il est possible que le thread qui a appliqué ultérieurement acquière le verrou avant le thread qui a appliqué en premier. Dans un environnement à forte concurrence, cela peut entraîner une inversion de priorité ou une famine (un certain thread n'obtient jamais le verrou). ). 🎜🎜<img alt="Explication détaillée avec images et texte ! Résumé des verrous en Java" src="https://img.php.cn/upload/article/000/000/067/5cab904b73cc3c2ec71a08b113232b72-7.png">🎜🎜En Java, le mot-clé synchronisé est un verrou injuste, et ReentrantLock est également un verrou injuste par défaut. 🎜<pre class="brush:php;toolbar:false">public String test(String s1, String s2){ StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(s1); stringBuffer.append(s2); return stringBuffer.toString(); }
Verrou réentrant
est également appelé verrouillage récursif
, ce qui signifie que le même thread acquiert le verrou dans la méthode externe, le verrou sera automatiquement acquis lors de la saisie de la méthode interne. 🎜🎜🎜🎜Pour Java ReentrantLock, son nom peut indiquer qu'il s'agit d'un verrou réentrant. Pour Synchronized, il s’agit également d’un verrou réentrant. 🎜🎜Touchez au tableau : l'un des avantages des verrous réentrants est que les blocages peuvent être évités dans une certaine mesure. 🎜🎜Prenons l'exemple de synchronisé, regardez le code suivant : 🎜StringBuffer.class // append 是同步方法 public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }🎜Dans le code ci-dessus, la méthodeA appelle la méthodeB Si un thread appelle la méthodeA et a acquis le verrou puis appelle la méthodeB, il n'a pas besoin d'acquérir à nouveau le verrou. Il s'agit des caractéristiques de verrouillage. S'il ne s'agit pas d'un verrou réentrant, mehtodB risque de ne pas être exécuté par le thread actuel, ce qui peut provoquer un blocage. 🎜
Spin lock
signifie que lorsque le thread n'obtient pas le verrou, il n'est pas suspendu directement, mais exécute une boucle occupée. Cette boucle occupée est la ainsi-. appelé auto-verrouillage. 🎜🎜🎜🎜Le but du spin lock est de réduire le risque de suspension du thread, car la suspension et le réveil du thread sont également des opérations gourmandes en ressources. 🎜🎜Si le verrou est occupé par un autre thread pendant une longue période, le thread actuel sera toujours suspendu même après l'exécution, et la boucle occupée deviendra un gaspillage de ressources système, ce qui réduira en fait les performances globales. Par conséquent, les verrous tournants ne conviennent pas aux situations de concurrence où le verrouillage prend beaucoup de temps. 🎜🎜En Java, la classe AtomicInteger
a une opération spin Jetons un coup d'œil au code : 🎜rrreee🎜Si l'opération CAS échoue, elle continuera à boucler pour obtenir la valeur actuelle, puis réessayera. . 🎜🎜De plus, vous devez également connaître le verrouillage rotatif adaptatif. 🎜🎜La rotation adaptative a été introduite dans JDK1.6, qui est plus intelligent. Le temps de rotation n'est plus fixe, mais est déterminé par le temps de rotation précédent sur la même serrure et le statut du propriétaire de la serrure. Si la machine virtuelle pense que cette rotation est susceptible de réussir à nouveau, cela prendra plus de temps. Si la rotation réussit rarement, elle peut directement omettre le processus de rotation à l'avenir pour éviter de gaspiller les ressources du processeur. 🎜La serrure segmentée
est une conception de serrure, pas une serrure spécifique. 🎜🎜L'objectif de la conception du verrouillage segmenté est d'affiner davantage la granularité du verrouillage. Lorsque l'opération n'a pas besoin de mettre à jour l'intégralité du tableau, un seul élément du tableau peut être verrouillé. 🎜🎜🎜🎜Dans le langage Java, la couche sous-jacente de CurrentHashMap utilise des verrous de segmentation, elle peut être utilisée simultanément. 🎜Verrouillage biaisé
, Verrouillage léger
et Verrouillage lourd
, cela suivra Une condition de concurrence multithread s'aggrave progressivement , mais ne peut pas se dégrader. 🎜🎜L'état Lock-free🎜🎜Lock-free
est en fait le verrouillage optimiste mentionné ci-dessus, qui ne sera pas décrit ici. 🎜偏向锁
Java偏向锁(Biased Locking)是指它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁。
偏向锁的实现是通过控制对象Mark Word
的标志位来实现的,如果当前是可偏向状态
,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入。
轻量级锁
当线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁
,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式
等待上一个线程释放锁。
重量级锁
如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁
,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。
升级到重量级锁其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态。
在 Java 中,synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。这一过程在后续讲解 synchronized 关键字的原理时会详细介绍。
锁粗化
锁粗化
就是将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求。
举个例子,一个循环体中有一个代码同步块,每次循环都会执行加锁解锁操作。
private static final Object LOCK = new Object(); for(int i = 0;i <p>经过<code>锁粗化</code>后就变成下面这个样子了:</p><pre class="brush:php;toolbar:false"> synchronized(LOCK){ for(int i = 0;i <p><strong>锁消除</strong></p><p><code>锁消除</code>是指虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除。</p><p>举个例子让大家更好理解。</p><pre class="brush:php;toolbar:false">public String test(String s1, String s2){ StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(s1); stringBuffer.append(s2); return stringBuffer.toString(); }
上面代码中有一个 test 方法,主要作用是将字符串 s1 和字符串 s2 串联起来。
test 方法中三个变量s1, s2, stringBuffer, 它们都是局部变量,局部变量是在栈上的,栈是线程私有的,所以就算有多个线程访问 test 方法也是线程安全的。
我们都知道 StringBuffer 是线程安全的类,append 方法是同步方法,但是 test 方法本来就是线程安全的,为了提升效率,虚拟机帮我们消除了这些同步锁,这个过程就被称为锁消除
。
StringBuffer.class // append 是同步方法 public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
Java 并发编程的知识非常多,同时也是 Java 面试的高频考点,面试官必问的,需要学习 Java 并发编程其他知识的小伙伴可以去下载『阿里师兄总结的Java知识笔记 总共 283 页,超级详细』。
前面讲了 Java 语言中各种各种的锁,最后再通过六个问题统一总结一下:
推荐学习:《java视频教程》
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!