シンクロナイズド根本的な最適化(軽量ロック、偏ったロック)

 

ヘビー級のロック

  前回の記事では同期の使用と実装の原則を紹介します。今、私たちが知っておくべき、同期を達成するために内部ターゲットによってロックモニター(モニター)と呼ばれています。しかし、モニターロックの本質は、ミューテックスのロックを達成するための基礎となるオペレーティングシステムに依存しています。カーネルモードために、ユーザーモードから変換する必要がスレッドを切り替えるには、オペレーティング・システムのための理由は、このコストは状態間の遷移はなぜ低い同期効率が比較的長い期間を必要と、非常に高いです。したがって、これは、ミューテックスロックロックは、私たちが呼んで実現し、オペレーティング・システムに依存し、「ヘビー級のロックを。」最適化のすべての種類を行うにはJDKに同期し、コアは、ヘビー級のロックの使用を減らすことです。JDK1.6の後、順番にパフォーマンスを向上させ、得られるとロックがパフォーマンスもたらした消費量を減らす解放する、「軽量ロック」との紹介「偏ったロック。」

第二に、軽量ロック 

  ノーロック状態、ロックする傾向があり、軽量で、ヘビー級のロックロック:4つのロックの状態があります。競争のロックを使用すると、ヘビー級のロックをアップグレードした後、偏った軽量ロックをロックするロックからアップグレードできます(ロックが、アップグレードは一方向である、それはローからハイにのみをアップグレードすると言うことです、ロックは表示されませんダウングレード)。JDK 1.6が偏っロックロックではデフォルトで有効になっており、軽量化され、我々はまた、-XXことができます。バイアスされ、ロック-UseBiasedLockingを無効にします。ロック状態がJDK実施例32にファイルヘッダーオブジェクトに格納されています。

ロック状態

25ビット

4ビット

1ビット

2ビット

23bit

2ビット

それは偏ったロックであるかどうか

ロックフラグ

軽量ロック

スタックポインタのレコードロックをポインティング

00

ヘビーロック

ミューテックスポインタ(ヘビーロック)の

10

GCマーク

空の

11

バイアスされたロック

スレッドID

エポック

世代年齢オブジェクト

1

01

いいえロックません

hashCodeオブジェクト

世代年齢オブジェクト

0

01

  「軽量」伝統的なロックを達成するために、オペレーティング・システム・ミューテックスの使用に関連しています。しかし、あなたが最初にそれはなしマルチスレッドの競争の前提の下で意図されたロック軽量ヘビー級ロックに代わるものではありませんことを強調する必要があり、消費の性能に伝統的なヘビー級のロックの使用を減らします。実装する前は、最初の軽量ロックの適応シナリオが同時に同じロックへのアクセスがあればケーススレッドが交互に、状況を同期化ブロックを実行していることを理解し、軽量ロックを説明し、それは軽量ロックインフレにつながりますヘビー級のロック。

1、軽量ロックロック手順

  (1)コードが同期ブロックを入力する場合、同期オブジェクトロック状態がないロック状態でない場合(フラグをロックすることがバイアスロッキング「0」であるか否か、「01」状態である)、現在のスレッド内の仮想マシン最初のスタックフレームロックと呼ばれるレコードを確立し、正式に変位マーク言葉として知られているマークのWordの現在のロック・オブジェクトのコピーを格納するためのスペース(レコードをロック)。この時点でスレッドスタックとオブジェクトヘッダの状態は、図2.1に示します。

  (2)コピー先ヘッダマークWordは、ロック・レコードにコピー。

  (3)コピーが成功し、仮想マシンがオブジェクトマークワードに所有者ポインタにマークWordがレコードのポインタをロックするように更新CASの操作対象、およびロックレコードを使用しようとします。更新が成功した場合、そうでない場合はステップ(4)、(3)に進みます。

  (4)更新アクションが成功した場合、スレッドは、オブジェクトのロックを所有し、そしてマークワードロックフラグのオブジェクトが「00」に設定され、それはこのオブジェクトがスレッド・スタックが、この時、軽量ロック状態であることを意味します図2.2に示すオブジェクトヘッドの状態。

  (5)この更新が失敗した場合、仮想マシンは最初、それが現在のスレッドがすでにこのオブジェクトのロックを所有している意味ならば、それは直接ブロックが続く同期を入力することができ、現在のスレッドのスタックフレームにマークのWordポイントの対象かどうかを確認します。そうでなければ、複数のスレッドがロック競合、軽量拡張ヘビーロックがロックしなければならない記載、ロックフラグの状態値は「10」となり、マークワードはヘビーロック(ミューテックス)はバックポインタ指し格納されていますロックを待っているスレッドがブロックされた状態を入力する必要があります。現在のスレッドがロックを獲得するためにスピンを使用しようとスピンスレッドを防ぐためにあるブロックされている、およびプロセスサイクルの使用がロックを取得します。

 

                     スタック状態オブジェクトの前に2.1 CAS軽量ロック操作を図

   

                      CAS 2.2ロックで軽量の動作後のオブジェクトの状態をスタック

2、軽量ロックのアンロック処理:

  (1)CAS操作によって、現在のスレッドマークワード変位マークWordのコピー・オブジェクトを交換することを試みます。

  交換が成功した場合(2)、全体の同期プロセスは完了です。

  (3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

三、偏向锁

  引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

1、偏向锁获取过程:

  (1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。

  (2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。

  (3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。

  (4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

  (5)执行同步代码。

2、偏向锁的释放:

  偏向锁的撤销在上述第四步骤中有提到偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

3、重量级锁、轻量级锁和偏向锁之间转换

 

                                        图 2.3三者的转换图

  该图主要是对上述内容的总结,如果对上述内容有较好的了解的话,该图应该很容易看懂。

四、其他优化 

1、适应性自旋(Adaptive Spinning):从轻量级锁获取的流程中我们知道当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

2、锁粗化(Lock Coarsening):锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:

package com.paddx.test.string;

public class StringBufferTest {
    StringBuffer stringBuffer = new StringBuffer();

    public void append(){
        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("c");
    }
}

这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

3、锁消除(Lock Elimination):锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。看下面这段程序:

package com.paddx.test.concurrent;

public class SynchronizedTest02 {

    public static void main(String[] args) {
        SynchronizedTest02 test02 = new SynchronizedTest02();
        //启动预热
        for (int i = 0; i < 10000; i++) {
            i++;
        }
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            test02.append("abc", "def");
        }
        System.out.println("Time=" + (System.currentTimeMillis() - start));
    }

    public void append(String str1, String str2) {
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }
}

虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,可以将锁消除。下面是我本地执行的结果:

  为了尽量减少其他因素的影响,这里禁用了偏向锁(-XX:-UseBiasedLocking)。通过上面程序,可以看出消除锁以后性能还是有比较大提升的。

  注:可能JDK各个版本之间执行的结果不尽相同,我这里采用的JDK版本为1.6。

五、总结 

  本文重点介绍了JDk中采用轻量级锁和偏向锁等对Synchronized的优化,但是这两种锁也不是完全没缺点的,比如竞争比较激烈的时候,不但无法提升效率,反而会降低效率,因为多了一个锁升级的过程,这个时候就需要通过-XX:-UseBiasedLocking来禁用偏向锁。下面是这几种锁的对比:

优点

缺点

适用场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。

如果线程间存在锁竞争,会带来额外的锁撤销的消耗。

适用于只有一个线程访问同步块场景。

轻量级锁

スレッドには、競争をブロックするプログラムの応答速度を改善しません。

スピンロック競合のスレッドを取得することはまだできない場合は、CPUを消費します。

応答時間の追求。

シンクブロックは非常に高速に実行します。

ヘビーロック

スピンを使用せずにスレッドの競争は、CPUを消費しません。

スレッドがブロックされ、応答時間が遅いです。

スループットの追求。

シンクブロックの長い実行速度。

 

 

 

 

 

 

 

投稿者:  https://www.cnblogs.com/paddix/p/5405678.html

 

おすすめ

転載: www.cnblogs.com/JonaLin/p/11490430.html