#ロックの分類の概要
楽観的ロックと悲観的ロック
ロックのマクロ分類は、楽観的ロックと悲観的ロックです。楽観的ロックと悲観的ロックは特定のロックを参照するのではなく (Java には楽観的ロックまたは悲観的ロックと呼ばれる特定のロック実装名はありません)、同時状況における 2 つの異なる戦略を参照します。 Optimistic Lock (オプティミスティック ロック) は非常に楽観的で、データを取得しに行くたびに、他の人がそのデータを変更しないだろうと考えます。なのでロックはかかりません。ただし、データを更新する場合は、更新する前に、読み取りと更新の間に他の人がデータを変更したかどうかを確認します。変更されている場合は、再度読み取り、更新を再試行し、更新が成功するまで上記の手順をループします (もちろん、更新に失敗したスレッドは更新操作を放棄することもできます)。 悲観的ロック (悲観的ロック) は非常に悲観的で、データを取得しに行くたびに、他の人がデータを変更するのではないかと考えてしまいます。したがって、データが取得されるたびにロックされます。 このように、他の人がデータを取得すると、悲観的ロックが解除されるまでブロックされ、データを取得したいスレッドは再度ロックを取得してからデータを取得します。 悲観的ロックはトランザクションをブロックし、楽観的ロックはロールバックと再試行を行います。それぞれに長所と短所があります。良いか悪いかの区別はなく、異なるシナリオへの適応の違いのみです。例: オプティミスティック ロックは、書き込みが比較的少ない状況、つまり競合がほとんど発生しないシナリオに適しており、これによりロックのコストが節約され、システム全体のスループットが向上します。ただし、競合が頻繁に発生する場合、上位層のアプリケーションは再試行を続けるため、実際にはパフォーマンスが低下するため、このシナリオには悲観的ロックの方が適しています。 概要: 楽観的ロックは書き込みが比較的少なく、競合がほとんど発生しないシナリオに適していますが、書き込みが多く競合が多いシナリオは悲観的ロックに適しています。オプティミスティック ロックの基礎 --- CAS
オプティミスティック ロックの実装では、CAS という概念を理解する必要があります。 CASとは何ですか? Compare-and-Swap、つまり、比較して置換、または比較して設定します。 比較: 値 A を読み取り、それを B に更新する前に、元の値が A であるかどうかを確認します (他のスレッドによって変更されていないため、ここでは ABA の問題を無視します)。 置換: 「はい」の場合、A を B に更新し、終了します。そうしないと更新されません。 上記の 2 つのステップはアトミックな操作であり、瞬時に完了すると理解できますが、CPU の観点からは 1 ステップの操作です。 CAS を使用すると、オプティミスティック ロックを実装できます。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 指令。 * * */ }
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); ... }
#1 つのスレッドだけがデータを正常に更新できるため、他のスレッドはロールバックして再試行する必要があります。 CAS は CPU 命令を使用してハードウェア レベルからアトミック性を確保し、ロックのような効果を実現します。
オプティミスティック ロックのプロセス全体から、ロック操作とロック解除操作がないことがわかるため、オプティミスティック ロック戦略はロックフリー プログラミングとも呼ばれます。つまり、オプティミスティック ロックは実際には「ロック」ではなく、ループ内で再試行する単なる CAS アルゴリズムです。
関連する推奨事項: 「
java 開発チュートリアル」
同期およびロック インターフェイス Java でロックを実装するには 2 つの方法があります。1 つは synchronized キーワードを使用する方法、もう 1 つは Lock インターフェースの実装クラスを使用する方法です。
記事で良い比較を見ました。非常に鮮明です。同期キーワードは、あらゆる運転ニーズに対応できるオートマチック トランスミッションのようなものです。
ただし、ドリフトやさまざまな高度な操作など、より高度な操作を実行したい場合は、Lock インターフェイスの実装クラスであるマニュアル ギアが必要です。
そして、Java の各バージョンでさまざまな最適化が行われた結果、同期は非常に効率的になりました。 Lock インターフェースの実装クラスほど使い勝手が良くないというだけです。
同期ロックのアップグレード プロセスは最適化の中核です: バイアスされたロック -> 軽量ロック -> ヘビーウェイト ロックclass Test{ private static final Object object = new Object(); public void test(){ synchronized(object) { // do something } } }
いつロックする場合は synchronized キーワードを使用します。特定のコード ブロックに入ると、最初にロックされたオブジェクト (つまり、上記のコードのオブジェクト) は重量ロックではなく、バイアスされたロックになります。 バイアスされたロックの文字通りの意味は、「ロックを取得する最初のスレッドに偏った」ロックです。スレッドが同期されたコード ブロックを実行した後、バイアス ロックは積極的に解放されません。 2 回目に同期コード ブロックに到達すると、スレッドはその時点でロックを保持しているスレッドがそれ自体であるかどうかを判断し (ロックを保持しているスレッド ID はオブジェクト ヘッダーに格納されます)、そうである場合は実行を続行します。通常は。以前に解放されていないため、ここで再ロックする必要はありません。1 つのスレッドが最初から最後までロックを使用している場合、バイアスされたロックには追加のオーバーヘッドがほとんどなく、パフォーマンスが非常に高いことがわかります。
一旦有第二个线程加入锁竞争,偏向锁转换为轻量级锁(自旋锁)。锁竞争:如果多个线程轮流获取一个锁,但是每次获取的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程获取锁的时候,发现锁已经被占用,需要等待其释放,则说明发生了锁竞争。
在轻量级锁状态上继续锁竞争,没有抢到锁的线程进行自旋操作,即在一个循环中不停判断是否可以获取锁。获取锁的操作,就是通过 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 控制
以上がJava のさまざまなロックのインベントリの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。