Introduction à la classification des serrures
Verrous optimistes et verrous pessimistes
Une macro classification des verrous est constituée de verrous optimistes et de verrous pessimistes. Le verrouillage optimiste et le verrouillage pessimiste ne font pas référence à un verrou spécifique (il n'existe pas de nom d'implémentation de verrouillage spécifique en Java appelé verrouillage optimiste ou verrouillage pessimiste), mais à deux stratégies différentes dans des situations concurrentes.
Optimistic Lock est très optimiste. Chaque fois que vous obtenez les données, vous pensez que les autres ne les modifieront pas. Il ne sera donc pas verrouillé. Mais si vous souhaitez mettre à jour les données, vous vérifierez si d'autres ont modifié les données entre la lecture et la mise à jour avant la mise à jour. S'il a été modifié, relisez-le, essayez à nouveau de mettre à jour et répétez les étapes ci-dessus jusqu'à ce que la mise à jour réussisse (bien sûr, le thread dont la mise à jour a échoué est également autorisé à abandonner l'opération de mise à jour).
Pessimistic Lock est très pessimiste. Chaque fois que vous allez chercher les données, vous pensez que d'autres vont les modifier. Il est donc verrouillé à chaque fois que des données sont récupérées.
De cette façon, lorsque d'autres obtiendront les données, ils seront bloqués jusqu'à ce que le verrou pessimiste soit libéré. Le thread qui souhaite obtenir les données acquerra le verrou puis obtiendra les données.
Le verrouillage pessimiste bloque les transactions et le verrouillage optimiste annule et réessaye. Ils présentent chacun des avantages et des inconvénients. Il n'y a pas de bonne ou de mauvaise distinction, seulement la différence d'adaptation aux différents scénarios. Par exemple : le verrouillage optimiste convient aux situations dans lesquelles il y a relativement peu d'écritures, c'est-à-dire aux scénarios dans lesquels des conflits se produisent rarement. Cela peut réduire le coût du verrouillage et augmenter le débit global du système. Cependant, si des conflits surviennent souvent, l'application de couche supérieure continuera à réessayer, ce qui réduira en réalité les performances. Le verrouillage pessimiste est donc plus adapté à ce scénario.
Résumé : le verrouillage optimiste convient aux scénarios dans lesquels il y a relativement peu d'écritures et où les conflits se produisent rarement, tandis que les scénarios dans lesquels il y a beaucoup d'écriture et de nombreux conflits conviennent au verrouillage pessimiste.
La base du verrouillage optimiste --- CAS
Dans la mise en œuvre du verrouillage optimiste, il faut comprendre un concept : CAS.
Qu'est-ce que le CAS ? Comparer et échanger, c'est-à-dire comparer et remplacer, ou comparer et définir.
Comparaison : lisez une valeur A, avant de la mettre à jour en B, vérifiez si la valeur d'origine est A (non modifiée par d'autres threads, ignorez le problème ABA ici).
Remplacement : Si oui, mettez à jour A vers B, terminez. Sinon, il ne sera pas mis à jour.
Les deux étapes ci-dessus sont des opérations atomiques, qui peuvent être comprises comme étant terminées instantanément. Du point de vue du processeur, ce sont des opérations en une seule étape.
Avec CAS, vous pouvez implémenter un verrou optimiste :
public class OptimisticLockSample{ public void test(){ int data = 123; // 共享数据 // 更新数据的线程会进行如下操作 for (;;) { int oldData = data; int newData = doSomething(oldData); // 下面是模拟 CAS 更新操作,尝试更新 data 的值 if (data == oldData) { // compare data = newData; // swap break; // finish } else { // 什么都不敢,循环重试 } } } /** * * 很明显,test() 里面的代码根本不是原子性的,只是展示了下 CAS 的流程。 * 因为真正的 CAS 利用了 CPU 指令。 * * */ }
CAS est également implémenté en Java via des méthodes natives.
public final class Unsafe { ... public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6); ... }
Écrit ci-dessus est une implémentation simple et intuitive du verrouillage optimiste (pour être précis, il devrait s'agir d'un processus de verrouillage optimiste. Il permet à plusieurs threads de lire en même temps (car il n'y a pas d'opération de verrouillage). tous). Si les données sont mises à jour, si c'est le cas,
il y en a et un seul thread peut mettre à jour les données avec succès, ce qui oblige les autres threads à revenir en arrière et à réessayer. CAS utilise les instructions du processeur pour garantir l'atomicité au niveau matériel afin d'obtenir un effet de type verrouillage.
L'ensemble du processus de verrouillage optimiste montre qu'il n'y a pas d'opérations de verrouillage et de déverrouillage, c'est pourquoi la stratégie de verrouillage optimiste est également appelée programmation sans verrouillage. En d'autres termes, le verrouillage optimiste n'est pas réellement un "verrouillage".
C'est juste un algorithme CAS qui boucle et réessaye.
Recommandations associées : "Tutoriel de développement Java"
Verrouillage rotatif
interface synchronisée et verrouillée
Il existe deux manières d'implémenter le verrouillage en Java : l'une consiste à utiliser le mot-clé synchronisé et l'autre consiste à utiliser la classe d'implémentation de l'interface Lock.
J'ai vu une bonne comparaison dans un article. Elle est très vivante. Le mot-clé synchronisé est comme une transmission automatique, qui peut répondre à tous les besoins de conduite.
Mais si vous souhaitez effectuer des opérations plus avancées, telles que la dérive ou diverses opérations avancées, vous avez besoin d'un équipement manuel, qui est la classe d'implémentation de l'interface Lock.
Et synchronisé est devenu très efficace après diverses optimisations dans chaque version de Java. C'est juste que ce n'est pas aussi pratique à utiliser que la classe d'implémentation de l'interface Lock.
Le processus de mise à niveau du verrouillage synchronisé est au cœur de son optimisation : verrouillage biaisé-> verrouillage léger-> verrouillage lourd
class Test{ private static final Object object = new Object(); public void test(){ synchronized(object) { // do something } } }
Utilisez le mot-clé synchronisé pour verrouiller Quand en entrant un certain bloc de code, l'objet initialement verrouillé (l'objet dans le code ci-dessus) n'est pas un verrou lourd, mais un verrou biaisé.
Le sens littéral d'un verrou biaisé est un verrou qui est "biaisé vers le premier thread à l'acquérir". Une fois que le thread a exécuté le bloc de code synchronisé, il ne libère pas activement le verrou de biais. Lorsqu'il atteint le bloc de code synchronisé 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 stocké dans l'en-tête de l'objet), et si tel est le cas, il poursuivra l'exécution. normalement. Puisqu'il n'a pas été publié auparavant, il n'est pas nécessaire de reverrouiller ici si un thread utilise le verrou du début à la fin, il est évident que le verrou biaisé n'a presque pas de surcharge supplémentaire et les performances sont extrêmement élevées.
一旦有第二个线程加入锁竞争,偏向锁转换为轻量级锁(自旋锁)。锁竞争:如果多个线程轮流获取一个锁,但是每次获取的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程获取锁的时候,发现锁已经被占用,需要等待其释放,则说明发生了锁竞争。
在轻量级锁状态上继续锁竞争,没有抢到锁的线程进行自旋操作,即在一个循环中不停判断是否可以获取锁。获取锁的操作,就是通过 CAS 操作修改对象头里的锁标志位。先比较当前锁标志位是否为释放状态,如果是,将其设置为锁定状态,比较并设置是原子性操作,这个是 JVM 层面保证的。当前线程就算持有了锁,然后线程将当前锁的持有者信息改为自己。
假如我们获取到锁的线程操作时间很长,比如会进行复杂的计算,数据量很大的网络传输等;那么其它等待锁的线程就会进入长时间的自旋操作,这个过程是非常耗资源的。其实这时候相当于只有一个线程在有效地工作,其它的线程什么都干不了,在白白地消耗 CPU,这种现象叫做忙等。(busy-waiting)。所以如果多个线程使用独占锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized 就是轻量级锁,允许短时间的忙等现象。这是一种择中的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
显然,忙等是有限度的(JVM 有一个计数器记录自旋次数,默认允许循环 10 次,可以通过虚拟机参数更改)。如果锁竞争情况严重,
达到某个最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是通过 CAS 修改锁标志位,但不修改持有锁的线程 ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是上面说的忙等,即不会自旋),等待释放锁的线程去唤醒。在 JDK1.6 之前, synchronized直接加重量级锁,很明显现在通过一系列的优化过后,性能明显得到了提升。
JVM 中,synchronized 锁只能按照偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有把这个称为锁膨胀的过程),不允许降级。
可重入锁(递归锁)
可重入锁的字面意思是"可以重新进入的锁",即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归函数里这个锁会阻塞自己么?
如果不会,那么这个锁就叫可重入锁(因为这个原因可重入锁也叫做递归锁)。
Java 中以 Reentrant 开头命名的锁都是可重入锁,而且 JDK 提供的所有现成 Lock 的实现类,包括 synchronized 关键字锁都是可重入的。
如果真的需要不可重入锁,那么就需要自己去实现了,获取去网上搜索一下,有很多,自己实现起来也很简单。
如果不是可重入锁,在递归函数中就会造成死锁,所以 Java 中的锁基本都是可重入锁,不可重入锁的意义不是很大,我暂时没有想到什么场景下会用到;
注意:有想到需要不可重入锁场景的小伙伴们可以留言一起探讨。
下图展示一下 Lock 的相关实现类:
公平锁和非公平锁
如果多个线程申请一把公平锁,那么获得锁的线程释放锁的时候,先申请的先得到,很公平。如果是非公平锁,后申请的线程可能先获得锁,是随机获取还是其它方式,都是根据实现算法而定的。
对 ReentrantLock 类来说,通过构造函数可以指定该锁是否是公平锁,默认是非公平锁。因为在大多数情况下,非公平锁的吞吐量比公平锁的大,如果没有特殊要求,优先考虑使用非公平锁。
而对于 synchronized 锁而言,它只能是一种非公平锁,没有任何方式使其变成公平锁。这也是 ReentrantLock 相对于 synchronized 锁的一个优点,更加的灵活。
以下是 ReentrantLock 构造器代码:
/** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
ReentrantLock 内部实现了 FairSync 和 NonfairSync 两个内部类来实现公平锁和非公平锁。
可中断锁
字面意思是"可以响应中断的锁"。
首先,我们需要理解的是什么是中断。 Java 中并没有提供任何可以直接中断线程的方法,只提供了中断机制。那么何为中断机制呢?
线程 A 向线程 B 发出"请你停止运行"的请求,就是调用 Thread.interrupt() 的方法(当然线程 B 本身也可以给自己发送中断请求,
即 Thread.currentThread().interrupt()),但线程 B 并不会立即停止运行,而是自行选择在合适的时间点以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java 的中断不能直接终止线程,只是设置了状态为响应中断的状态,需要被中断的线程自己决定怎么处理。这就像在读书的时候,老师在晚自习时叫学生自己复习功课,但学生是否复习功课,怎么复习功课则完全取决于学生自己。
回到锁的分析上来,如果线程 A 持有锁,线程 B 等待持获取该锁。由于线程 A 持有锁的时间过长,线程 B 不想继续等了,我们可以让线程 B 中断。
自己或者在别的线程里面中断 B,这种就是 可中段锁。
在 Java 中, synchronized 锁是不可中断锁,而 Lock 的实现类都是 可中断锁。从而可以看出 JDK 自己实现的 Lock 锁更加的灵活,这也就是有了 synchronized 锁后,为什么还要实现那么些 Lock 的实现类。
Lock 接口的相关定义:
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
其中 lockInterruptibly 就是获取可中断锁。
共享锁
字面意思是多个线程可以共享一个锁。一般用共享锁都是在读数据的时候,比如我们可以允许 10 个线程同时读取一份共享数据,这时候我们可以设置一个有 10 个凭证的共享锁。
在 Java 中,也有具体的共享锁实现类,比如 Semaphore。
互斥锁
字面意思是线程之间互相排斥的锁,也就是表明锁只能被一个线程拥有。
在 Java 中, ReentrantLock、synchronized 锁都是互斥锁。
读写锁
读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)。
在 Java 中, ReadWriteLock 接口只规定了两个方法,一个返回读锁,一个返回写锁。
public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing */ Lock writeLock(); }
文章前面讲过[乐观锁策略](#乐观锁的基础 --- CAS),所有线程可以随时读,仅在写之前判断值有没有被更改。
读写锁其实做的事情是一样的,但是策略稍有不同。很多情况下,线程知道自己读取数据后,是否是为了更改它。那么为何不在加锁的时候直接明确。
这一点呢?如果我读取值是为了更新它(SQL 的 for update 就是这个意思),那么加锁的时候直接加写锁,我持有写锁的时候,别的线程。
无论是读还是写都需要等待;如果读取数据仅仅是为了前端展示,那么加锁时就明确加一个读锁,其它线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器加 1)。
虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略。因为读写锁并没有在更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁。乐观锁特指无锁编程。
JDK 内部提供了一个唯一一个 ReadWriteLock 接口实现类是 ReentrantReadWriteLock。通过名字可以看到该锁提供了读写锁,并且也是可重入锁。
总结
Java 中使用的各种锁基本都是悲观锁,那么 Java 中有乐观锁么?结果是肯定的,那就是 java.util.concurrent.atomic 下面的原子类都是通过乐观锁实现的。如下:
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; }
通过上述源码可以发现,在一个循环里面不断 CAS,直到成功为止。
参数介绍
-XX:-UseBiasedLocking=false 关闭偏向锁 JDK1.6 -XX:+UseSpinning 开启自旋锁 -XX:PreBlockSpin=10 设置自旋次数 JDK1.7 之后 去掉此参数,由 JVM 控制
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!