Heim >Java >javaLernprogramm >Inventar verschiedener Sperren in Java
Einführung in die Klassifizierung von Schlössern
Optimistische Sperren und pessimistische Sperren
Eine Makroklassifizierung von Sperren sind optimistische Sperren und pessimistische Sperren. Optimistisches Sperren und pessimistisches Sperren beziehen sich nicht auf eine bestimmte Sperre (in Java gibt es keinen spezifischen Namen für die Sperrenimplementierung, der als optimistisches Sperren oder pessimistisches Sperren bezeichnet wird), sondern auf zwei unterschiedliche Strategien in gleichzeitigen Situationen.
Optimistic Lock ist sehr optimistisch. Jedes Mal, wenn Sie die Daten erhalten, denken Sie, dass andere sie nicht ändern werden. Es wird also nicht gesperrt. Wenn Sie die Daten jedoch aktualisieren möchten, prüfen Sie vor der Aktualisierung, ob andere die Daten zwischen dem Lesen und Aktualisieren geändert haben. Wenn es geändert wurde, lesen Sie es erneut, versuchen Sie erneut, die Aktualisierung durchzuführen, und wiederholen Sie die obigen Schritte, bis die Aktualisierung erfolgreich ist (natürlich darf der Thread, der die Aktualisierung fehlgeschlagen hat, den Aktualisierungsvorgang auch aufgeben).
Pessimistic Lock ist sehr pessimistisch. Jedes Mal, wenn Sie die Daten abrufen, denken Sie, dass andere sie ändern werden. Daher wird es bei jedem Datenabruf gesperrt.
Auf diese Weise werden andere, wenn sie die Daten erhalten, blockiert, bis die pessimistische Sperre aufgehoben wird. Der Thread, der die Daten abrufen möchte, erhält die Sperre und erhält dann die Daten.
Pessimistische Sperren blockieren Transaktionen und optimistische Sperren führen einen Rollback und erneute Versuche durch. Es gibt keinen guten oder schlechten Unterschied, sondern nur den Unterschied in der Anpassung an verschiedene Szenarien. Beispiel: Optimistisches Sperren eignet sich für Situationen, in denen relativ wenige Schreibvorgänge stattfinden, d. Wenn jedoch häufig Konflikte auftreten, versucht die Anwendung der oberen Ebene weiterhin, es erneut zu versuchen, was tatsächlich zu einer Leistungseinbuße führt. Daher ist pessimistisches Sperren für dieses Szenario besser geeignet.
Zusammenfassung: Optimistisches Sperren eignet sich für Szenarien, in denen es relativ wenige Schreibvorgänge gibt und selten Konflikte auftreten, und pessimistisches Sperren eignet sich für Szenarien, in denen viel geschrieben wird und viele Konflikte auftreten.
Die Grundlage des optimistischen Sperrens --- CAS
Bei der Implementierung des optimistischen Sperrens müssen wir ein Konzept verstehen: CAS.
Was ist CAS? Compare-and-Swap, also vergleichen und ersetzen oder vergleichen und festlegen.
Vergleich: Lesen Sie einen Wert A, bevor Sie ihn auf B aktualisieren, prüfen Sie, ob der ursprüngliche Wert A ist (nicht von anderen Threads geändert, ignorieren Sie hier das ABA-Problem).
Ersatz: Wenn ja, aktualisieren Sie A auf B, Ende. Wenn nicht, wird es nicht aktualisiert.
Die beiden oben genannten Schritte sind atomare Operationen, die als sofort abgeschlossen angesehen werden können. Aus Sicht der CPU handelt es sich um einstufige Operationen.
Mit CAS können Sie eine optimistische Sperre implementieren:
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 wird auch in Java über native Methoden implementiert.
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); ... }
Oben ist eine einfache und intuitive Implementierung des optimistischen Sperrvorgangs beschrieben (um genau zu sein, es sollte ein optimistischer Sperrvorgang sein, der das gleichzeitige Lesen mehrerer Threads ermöglicht (da es keinen Sperrvorgang gibt). Wenn die Daten aktualisiert werden, ist
dies der Fall und nur ein Thread kann die Daten erfolgreich aktualisieren, was dazu führt, dass andere Threads einen Rollback durchführen und es erneut versuchen müssen. CAS verwendet CPU-Anweisungen, um die Atomizität auf Hardwareebene sicherzustellen und einen sperrenähnlichen Effekt zu erzielen.
Aus dem gesamten Prozess des optimistischen Sperrens geht hervor, dass es keine Sperr- und Entriegelungsvorgänge gibt, daher wird die optimistische Sperrstrategie auch als sperrfreie Programmierung bezeichnet. Mit anderen Worten, optimistisches Sperren ist eigentlich keine „Sperre“.
Es handelt sich lediglich um einen CAS-Algorithmus, der Schleifen und Wiederholungsversuche durchführt.
Verwandte Empfehlungen: „Java-Entwicklungs-Tutorial“
Spin Lock
synchronisierte und gesperrte Schnittstelle
Es gibt zwei Möglichkeiten, das Sperren in Java zu implementieren: Eine besteht darin, das synchronisierte Schlüsselwort zu verwenden, und die andere darin, die Implementierungsklasse der Lock-Schnittstelle zu verwenden.
Ich habe einen guten Vergleich in einem Artikel gesehen. Es ist sehr anschaulich. Das synchronisierte Schlüsselwort ist wie ein Automatikgetriebe, das alle Fahranforderungen erfüllen kann.
Wenn Sie jedoch fortgeschrittenere Vorgänge wie Driften oder verschiedene fortgeschrittene Vorgänge ausführen möchten, benötigen Sie manuelles Getriebe, die Implementierungsklasse der Lock-Schnittstelle.
Und synchronisiert ist nach verschiedenen Optimierungen in jeder Java-Version sehr effizient geworden. Es ist nur nicht so bequem zu verwenden wie die Implementierungsklasse der Lock-Schnittstelle.
Der synchronisierte Sperren-Upgrade-Prozess ist der Kern seiner Optimierung: voreingenommene Sperre-> leichte Sperre-> Verwenden Sie das synchronisierte Schlüsselwort, um zu sperren Wenn Sie einen bestimmten Codeblock eingeben, ist das ursprünglich gesperrte Objekt (dh das Objekt im obigen Code) keine schwere Sperre, sondern eine voreingenommene Sperre.
Die wörtliche Bedeutung einer voreingenommenen Sperre ist eine Sperre, die „auf den ersten Thread ausgerichtet ist, der sie erhält“. Nachdem der Thread den synchronisierten Codeblock ausgeführt hat, wird die Bias-Sperre nicht aktiv freigegeben. Wenn der Thread zum zweiten Mal den synchronisierten Codeblock erreicht, bestimmt er, ob der Thread, der zu diesem Zeitpunkt die Sperre hält, er selbst ist (die Thread-ID, die die Sperre hält, wird im Objektheader gespeichert), und wenn ja, setzt er die Ausführung fort normalerweise. Da es noch nicht freigegeben wurde, besteht hier keine Notwendigkeit, die Sperre erneut zu sperren. Wenn ein Thread die Sperre vom Anfang bis zum Ende verwendet, ist es offensichtlich, dass die voreingenommene Sperre fast keinen zusätzlichen Overhead verursacht und die Leistung extrem hoch ist.一旦有第二个线程加入锁竞争,偏向锁转换为轻量级锁(自旋锁)。锁竞争:如果多个线程轮流获取一个锁,但是每次获取的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程获取锁的时候,发现锁已经被占用,需要等待其释放,则说明发生了锁竞争。
在轻量级锁状态上继续锁竞争,没有抢到锁的线程进行自旋操作,即在一个循环中不停判断是否可以获取锁。获取锁的操作,就是通过 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 控制
Das obige ist der detaillierte Inhalt vonInventar verschiedener Sperren in Java. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!