あなたは、シンクロナイズドアクションがロックを同期している知っているが、あなたはどのようにそれを達成するためにされたJVMでそれを知っていますか?

ここに画像を挿入説明

注目点は、迷子にしないでください。継続的に感熱紙のJava関連の技術や情報アーキテクチャを更新します!

シャープ目の友人も驚きはヨーヨーがあり、このブログページの左上隅に表示される場合があり

ヒント:

この記事では、基礎となるJVMに関連する記事は脳を焼き、慎重に読んでください!

私たちは、synchronizedキーワードをロックするプログラムを使用することができます。静的メソッドを直接標識またはインスタンスメソッドことができるコードの同期ブロックを宣言するために使用することができます。

それは、同期に来るとき、私たちは、バイトコード命令のmonitorenterとmonitorexitを理解する必要があります。

所望のロックがロックオブジェクトのロックを解除するように、これら2つのコマンドは、エレメントのオペランドスタック上の基準タイプ(すなわち引用さ括弧内のキーワードを同期される)を消費します。

ここでは、JVM内で同期の基本原則の深い理解を達成します。

次のコードをご覧ください:

コードがコンパイルされた後に、バイトコードを表示し、私はちょうどこのチャートは、以下の説明使用します。

PS:スクリーンショットは非常によく、それをカットし、より少し下にカットしていませんでした、私たちはやると見ます

もし、上記バイトコードmonitorenter指示に気付くと、複数のmonitorexit命令を含むことができます。

Java仮想マシンが取得したロックは、通常の実行パスと実行パスの異常でロックを解除することができることを保証する必要があるためです。

あなたは理解することができるはずです、自分自身について考える、私のノートを見ることができます。

標識法を同期した場合、あなたはアクセス方法バイトコードマーカーはACC_SYNCHRONIZED含ま表示されることに留意すべきです。

マークは、メソッドを入力するときに、Java仮想マシンがmonitorenter操作する必要があることを示して。

あなたはそれが正常復帰であるかどうか、方法を終了するか、呼び出し元に例外をスローする場合や、Java仮想マシンはmonitorexit操作を必要としています。

二つの絵で簡単に理解できます。

これは、リターンのバイトコード番号0で見ることができます。

一つは、ここに頼むかもしれない、と何のコールmonitorenter monitorexitコマンドああはありませんか?ロックを達成するためにどのように?

それはmonitorenterここで注目されており、操作に対応するmonitorexitロックオブジェクトが暗黙的です。

对于实例方法来说,这两个操作对应的锁对象是 this;对于静态方法来说,这两个操作对应的锁对象则是所在类的Class实例。

我们先来介绍Synchronized的重入的实现机理。

可以认为每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

当执行monitorenter时,如果目标锁对象的计数器为0,那么说明它没有被其他线程所持有。

Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

在目标锁对象的计数器不为 0 的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为0,代表锁已被释放。

这就是锁的重入的实现机理。

说完了这个实现机理,我们来探究具体的锁实现。

首先谈谈重量级锁,重量级锁是 Java 虚拟机中最为基础的锁实现。

在这种状态下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。在Linux中,这是通过pthread库的互斥锁来实现的。

此外,这些操作将涉及系统调用,需要从操作系统的用户态切换至内核态,其开销非常之大。

为了尽量避免昂贵的线程阻塞、唤醒操作,Java虚拟机会在线程进入阻塞状态之前,以及被唤醒后竞争不到锁的情况下,进入自旋状态,在处理器上空跑并且轮询锁是否被释放。

如果此时锁恰好被释放了,那么当前线程便无须进入阻塞状态,而是直接获得这把锁。

下面我将介绍自适应自旋的概念,刚才说了自旋是什么,但是自旋很耗费资源,所以我们可以根据以往自旋等待时是否能够获得锁,来动态调整自旋的时间(循环数目)。

所以Synchronized是否公平这个问题可以休矣,为什么呢?

处于阻塞状态的线程,并没有办法立刻竞争被释放的锁。然而,处于自旋状态的线程,则很有可能优先获得这把锁。所以Synchronized不是公平的。

我们再介绍轻量级锁,针对多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。

针对这种情形,Java 虚拟机采用了轻量级锁,来避免重量级锁的阻塞以及唤醒。

在介绍轻量级锁的原理之前,我们先来了解一下Java虚拟机是怎么区分轻量级锁和重量级锁的。

简单的说,对象头中有一个标记字段。它的最后两位便被用来表示该对象的锁状态,其中:

  • 00代表轻量级锁
  • 01代表无锁(或偏向锁)
  • 10代表重量级锁
  • 11则跟垃圾回收算法的标记有关。

当进行加锁操作时,Java虚拟机会判断是否已经是重量级锁。

如果不是,它会在当前线程的当前栈桢中划出一块空间,作为该锁的锁记录,并且将锁对象的标记字段复制到该锁记录中。

然后,Java 虚拟机会尝试用 CAS 操作替换锁对象的标记字段。

各位有兴趣可以了解一下JVM的CAS在X86机器上的实现,是汇编指令lock cmpxhcg。

这里我简单介绍一下,CAS 是一个原子操作,它会比较目标地址的值是否和期望值相等,如果相等,则替换为一个新的值。

假设当前锁对象的标记字段为 X…XYZ,Java 虚拟机会比较该字段是否为 X…X01。

如果是,则替换为刚才分配的锁记录的地址。由于内存对齐的缘故,它的最后两位为 00。此时,该线程已成功获得这把锁,可以继续执行了。

如果不是 X…X01,那么有两种可能:

  • 第一,该线程重复获取同一把锁。此时,Java 虚拟机会将0加入锁记录,以代表该锁被重复获取。
  • 第二,其他线程持有该锁。此时,Java 虚拟机会将这把锁膨胀为重量级锁,并且阻塞当前线程。

你可以将一个线程的所有锁记录想象成一个栈结构,每次加锁压入一条锁记录,解锁弹出一条锁记录,当前锁记录指的便是栈顶的锁记录。

当进行解锁操作时,如果当前锁记录的值为 0,则代表重复进入同一把锁,直接返回即可。

若当前锁记录不是0,Java 虚拟机会尝试用 CAS 操作,比较锁对象的标记字段的值是否为当前锁记录的地址。

如果是,则替换为锁记录中的值,也就是锁对象原本的标记字段。此时,该线程已经成功释放这把锁。

如果不是,则意味着这把锁已经被膨胀为重量级锁。此时,Java 虚拟机会进入重量级锁的释放过程,唤醒因竞争该锁而被阻塞了的线程。

下面我们介绍偏向锁,偏向锁针对的是从始至终只有一个线程请求某一把锁。是轻量级锁的更进一步的乐观情况。

在线程进行加锁时,如果该锁对象支持偏向锁,那么 Java 虚拟机会通过 CAS 操作,将当前线程的地址记录在锁对象的标记字段之中,并且将标记字段的最后三位设置为 101。

这里介绍一下epoch的概念,每个类中维护一个epoch值,你可以理解为这个类所有实例对象的第几代偏向锁。

当设置偏向锁时,Java 虚拟机需要将该epoch值复制到锁对象的标记字段中。我们规定,你加的偏向锁的代数高,是可以把代数低的PK下去的。

接下来我给你讲的过程,你就知道为什么要这么设计了。

我们先从偏向锁的撤销讲起。

当请求加锁的线程和锁对象标记字段保持的线程地址不匹配时(而且epoch即代数必须相等,如若不等,那么当前线程可以将该锁重偏向至自己,因为新的epoch的代数肯定要高于以前的代数),Java 虚拟机需要撤销该偏向锁。

这个撤销过程非常麻烦,它要求持有偏向锁的线程到达安全点,再将偏向锁替换成轻量级锁。

在宣布某个类的偏向锁失效时,Java 虚拟机实则将该类的epoch值加 1,表示之前那一代的偏向锁已经失效。而新设置的偏向锁则需要使用类中的最新epoch代数来加锁。

为了保证当前持有偏向锁并且已加锁的线程不至于因此丢锁,Java虚拟机需要遍历所有线程的Java栈,找出该类已加锁的实例,并且将它们标记字段中的 epoch值加1。该操作需要所有线程处于安全点状态。

所以有专家近年来提出,偏向锁在锁竞争激烈的情况下,非但不能优化性能,反而可能伤害应用性能。

如果总撤销数超过另一个阈值(对应 Java 虚拟机参数-XX:BiasedLockingBulkRevokeThreshold,默认值为 40),那么 Java 虚拟机会认为这个类已经不再适合偏向锁。

このとき、クラス偏ったロックのJava仮想マシン・インスタンスが取り消され、クラスのインスタンスの直後に手順をロック軽量ロックを設定します。

エンド
財布でBATアーキテクチャの経験

おすすめ

転載: blog.csdn.net/Java_No01/article/details/90671098