ホームページ  >  記事  >  Java  >  Java の synchronized キーワードは、スレッド同期を実現するために使用されます。

Java の synchronized キーワードは、スレッド同期を実現するために使用されます。

WBOY
WBOY転載
2023-04-27 14:01:07791ブラウズ

1. 同期実装ロックの式

  1. インスタンス メソッドを変更します。通常の同期メソッドの場合、ロックは現在のインスタンス オブジェクトです。

  2. 静的メソッドを変更します。静的同期メソッドの場合、ロックは現在の Class オブジェクトです。

  3. メソッド コード ブロックを変更します。同期メソッド ブロックの場合、ロックは同期括弧内に設定されたオブジェクトです。

スレッドが同期されたコード ブロックにアクセスしようとすると、ロックを取得する必要があり、完了後 (または例外が発生した後)、ロックを解放する必要があります。では、ロックは正確にどこに存在するのでしょうか?一緒に探検しましょう!

ただし、この記事は誰でも見つけることができるので、その使用法については誰もがすでによく知っていると思います。その使用法と、マルチスレッド状況でデータが混乱する理由については詳しく説明しません。の解説!いくつかの使用方法のみを記載していますので、ご参考にしてください。

①インスタンス メソッドを変更します

インスタンス メソッドを変更します。通常の同期メソッドの場合、ロックは現在のインスタンス オブジェクトです

インスタンス メソッドを変更する必要はありませんこれを使用するには、同じインスタンスに同期を追加した後、アトミック操作を完了するためにスレッドをキューに入れる必要がありますが、これは 同じインスタンス が使用されている場合にのみ有効になることに注意してください。

正の例:

<code>/**<br> * @author huangfu<br> */<br>public class ExploringSynchronized implements Runnable {<br>    /**<br>     * 共享资源(临界资源)<br>     */<br>    static int i=0;<br>    public synchronized void add(){<br>        i++;<br>    }<br><br>    @Override<br>    public void run() {<br>        for (int j = 0; j             add();<br>        }<br>    }<br><br>    public static void main(String[] args) throws InterruptedException {<br>        ExploringSynchronized exploringSynchronized = new ExploringSynchronized();<br>        Thread t1 = new Thread(exploringSynchronized);<br>        Thread t2 = new Thread(exploringSynchronized);<br>        t1.start();<br>        t2.start();<br>        //join 主线程需要等待子线程完成后在结束<br>        t1.join();<br>        t2.join();<br>        System.out.println(i);<br><br>    }<br>}</code>

反例:

<code>/**<br> * @author huangfu<br> */<br>public class ExploringSynchronized implements Runnable {<br>    /**<br>     * 共享资源(临界资源)<br>     */<br>    static int i=0;<br>    public synchronized void add(){<br>        i++;<br>    }<br><br>    @Override<br>    public void run() {<br>        for (int j = 0; j             add();<br>        }<br>    }<br><br>    public static void main(String[] args) throws InterruptedException {<br>        Thread t1 = new Thread(new ExploringSynchronized());<br>        Thread t2 = new Thread(new ExploringSynchronized());<br>        t1.start();<br>        t2.start();<br>        //join 主线程需要等待子线程完成后在结束<br>        t1.join();<br>        t2.join();<br>        System.out.println(i);<br><br>    }<br>}</code>

この場合、メソッドに synchronized を追加しても役に立ちません。 、ロックは現在のインスタンス オブジェクトです。インスタンス オブジェクトは異なるため、それらの間のロックは当然同じではありません。

②静的メソッドの変更

静的メソッドの変更静的同期メソッドの場合、ロックは現在の Class オブジェクトです

定義からわかります。つまり、彼のロックはクラス オブジェクトです。つまり、上記のクラスを例にとると、通常のメソッドのロック オブジェクトは new ExploringSynchronized() で、静的メソッドに対応するロック オブジェクトは次のとおりです。 ExploringSynchronized.classしたがって、静的メソッドに同期ロックを追加すると、インスタンスを再作成しても、取得されるロックは同じままになります。

<code>package com.byit.test;<br><br>/**<br> * @author huangfu<br> */<br>public class ExploringSynchronized implements Runnable {<br>    /**<br>     * 共享资源(临界资源)<br>     */<br>    static int i=0;<br>    public synchronized static void add(){<br>        i++;<br>    }<br><br>    @Override<br>    public void run() {<br>        for (int j = 0; j             add();<br>        }<br>    }<br><br>    public static void main(String[] args) throws InterruptedException {<br>        Thread t1 = new Thread(new ExploringSynchronized());<br>        Thread t2 = new Thread(new ExploringSynchronized());<br>        t1.start();<br>        t2.start();<br>        //join 主线程需要等待子线程完成后在结束<br>        t1.join();<br>        t2.join();<br>        System.out.println(i);<br><br>    }<br>}</code>

もちろん、結果は期待どおりです 200000

③変更されたメソッド コード ブロック

変更されたメソッド コード ブロック、同期されたメソッド ブロックの場合、ロックは内部にあります同期されたブラケット構成オブジェクト!

<code>package com.byit.test;<br><br>/**<br> * @author huangfu<br> */<br>public class ExploringSynchronized implements Runnable {<br>    /**<br>     * 锁标记<br>     */<br>    private static final String LOCK_MARK = "LOCK_MARK";<br>    /**<br>     * 共享资源(临界资源)<br>     */<br>    static int i=0;<br>    public void add(){<br>        synchronized (LOCK_MARK){<br>            i++;<br>        }<br>    }<br><br>    @Override<br>    public void run() {<br>        for (int j = 0; j             add();<br>        }<br>    }<br><br>    public static void main(String[] args) throws InterruptedException {<br>        Thread t1 = new Thread(new ExploringSynchronized());<br>        Thread t2 = new Thread(new ExploringSynchronized());<br>        t1.start();<br>        t2.start();<br>        //join 主线程需要等待子线程完成后在结束<br>        t1.join();<br>        t2.join();<br>        System.out.println(i);<br><br>    }<br>}</code>

同期されたコード ブロックの場合、括弧内はロック オブジェクトです。この文字列オブジェクトなどを使用できます。

2. synchronized の基になる実装

Java での synchronized の実装は、Monitor オブジェクトの入口と出口に基づいています。それは、明示的な同期 (明示的な monitorenter および monitorexit 命令を使用してコード ブロックを変更する) または暗黙的な同期 (メソッド本体を変更する) です。

コード ブロックを変更する場合のみ、monitorenter および monitorexit 命令に基づいて実装され、メソッドを変更する場合は、別の意味で達成されました!それについては後で話します!

実装全体の最下層を理解する前に、メモリ内のオブジェクトの構造の詳細について一般的に理解していただければ幸いです。

Java の synchronized キーワードは、スレッド同期を実現するために使用されます。

  • #インスタンス変数: 親クラスの属性情報を含む、クラスの属性データ情報を格納します。配列には、配列の長さも含まれます。メモリのこの部分は 4 バイトで整列されます。

  • データの入力: 仮想マシンでは、オブジェクトの開始アドレスが 8 バイトの整数倍である必要があるためです。パディング データは存在する必要はありません。単にバイト アラインメントのためのものです。これだけを理解してください。

これら 2 つの概念は簡単に理解できます。今日はオブジェクトの構成原理については説明しません。ロックを理解する上で特に重要なオブジェクト ヘッダーの調査に焦点を当ててみましょう。

一般的に、

synchronized で使用されるロックはオブジェクト ヘッダーに存在します。配列オブジェクトの場合、仮想マシンはオブジェクトの保存に 3 ワード幅を使用し、非配列オブジェクトの場合、オブジェクト ヘッダーの保存に 2 ワード幅を使用します。仮想マシンという単語では、1 ワード幅は 4 バイトに相当します。主な構造は、Mark WordClass Metadata Address で構成されており、その構造は次のとおりです:

#仮想マシン桁数##説明#32/64 ビット##Mark Word #Storage オブジェクトのハッシュコード、ロック情報や世代年齢、GCフラグなどの情報 32/64bitクラスメタデータアドレス が格納されます。構成タイプ データポインタ32/64bit (配列)A配列の長さ配列の長さ

上記の表から、Mark Word には ロック情報が存在することがわかります。では、Mark Word はどのように構成されているのでしょうか。

ヘッダー オブジェクトの構造
#ロック ステータス25bit4bit1bit は偏ったロックです2bit ロックフラグロック解除ステータスオブジェクトのハッシュコードオブジェクトの世代年齢001

在运行起见,mark Word 里存储的数据会随着锁的标志位的变化而变化。mark Word可能变化为存储一下四种数据

Java の synchronized キーワードは、スレッド同期を実現するために使用されます。

Java SE 1.6为了减少获得锁和释放锁带来的消耗,引入了偏向锁轻量级锁,从之前上来就是重量级锁到1.6之后,锁膨胀升级的优化,极大地提高了synchronized的效率;

锁一共有4中状态,级别从低到高:

Java の synchronized キーワードは、スレッド同期を実現するために使用されます。

这几个状态会随着锁的竞争,逐渐升级。锁可以升级,但是不能降级,其根本的原因就是为了提高获取锁和释放锁的效率!

那么,synchronized是又如何保证的线程安全的呢?或许我们需要从字节码寻找答案!

<code>package com.byit.test;<br><br>/**<br> * @author Administrator<br> */<br>public class SynText {<br>    private static String A = "a";<br>    public int i ;<br><br>    public void add(){<br>        synchronized (A){<br>            i++;<br>        }<br><br>    }<br>}</code>

反编译的字节码

<code>Compiled from "SynText.java"<br>public class com.byit.test.SynText {<br>  public int i;<br><br>  public com.byit.test.SynText();<br>    Code:<br>       0: aload_0<br>       1: invokespecial #1                  // Method java/lang/Object."<init>":()V<br>       4: return<br><br>  public void add();<br>    Code:<br>       0: getstatic     #2                  // Field A:Ljava/lang/String;<br>       3: dup<br>       4: astore_1<br>       5: monitorenter<br>       6: aload_0<br>       7: dup<br>       8: getfield      #3                  // Field i:I<br>      11: iconst_1<br>      12: iadd<br>      13: putfield      #3                  // Field i:I<br>      16: aload_1<br>      17: monitorexit<br>      18: goto          26<br>      21: astore_2<br>      22: aload_1<br>      23: monitorexit<br>      24: aload_2<br>      25: athrow<br>      26: return<br>    Exception table:<br>       from    to  target type<br>           6    18    21   any<br>          21    24    21   any<br><br>  static {};<br>    Code:<br>       0: ldc           #4                  // String a<br>       2: putstatic     #2                  // Field A:Ljava/lang/String;<br>       5: return<br>}<br></init></code>

省去不必要的,简化在简化

<code>   5: monitorenter<br>      ...<br>      17: monitorexit<br>      ...<br>      23: monitorexit</code>

从字节码中可知同步语句块的实现使用的是monitorentermonitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令的时候,线程将试图获取对象所所对应的monitor特权,当monitor的的计数器为0的时候,线程就可以获取monitor,并将计数器设置为1.去锁成功!如果当前线程已经拥有monitor特权,则可以直接进入方法(可重入锁),计数器+1;如果其他线程已经拥有了monitor特权,那么本县城将会阻塞!

拥有monitor特权的线程执行完成后释放monitor,并将计数器设置为0;同时执行monitorexit指令;不要担心出现异常无法执行monitorexit指令;为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

同步代码块的原理了解了,那么同步方法如何解释?不急,我们不妨来反编译一下同步方法的状态!

javap -verbose -p SynText > 3.txt

代码

<code>package com.byit.test;<br><br>/**<br> * @author huangfu<br> */<br>public class SynText {<br>    public int i ;<br><br>    public synchronized void add(){<br>        i++;<br><br>    }<br>}</code>

字节码

<code>Classfile /D:/2020project/byit-myth-job/demo-client/byit-demo-client/target/classes/com/byit/test/SynText.class<br>  Last modified 2020-1-6; size 382 bytes<br>  MD5 checksum e06926a20f28772b8377a940b0a4984f<br>  Compiled from "SynText.java"<br>public class com.byit.test.SynText<br>  minor version: 0<br>  major version: 52<br>  flags: ACC_PUBLIC, ACC_SUPER<br>Constant pool:<br>   #1 = Methodref          #4.#17         // java/lang/Object."<init>":()V<br>   #2 = Fieldref           #3.#18         // com/byit/test/SynText.i:I<br>   #3 = Class              #19            // com/byit/test/SynText<br>   #4 = Class              #20            // java/lang/Object<br>   #5 = Utf8               i<br>   #6 = Utf8               I<br>   #7 = Utf8               <init><br>   #8 = Utf8               ()V<br>   #9 = Utf8               Code<br>  #10 = Utf8               LineNumberTable<br>  #11 = Utf8               LocalVariableTable<br>  #12 = Utf8               this<br>  #13 = Utf8               Lcom/byit/test/SynText;<br>  #14 = Utf8               syncTask<br>  #15 = Utf8               SourceFile<br>  #16 = Utf8               SynText.java<br>  #17 = NameAndType        #7:#8          // "<init>":()V<br>  #18 = NameAndType        #5:#6          // i:I<br>  #19 = Utf8               com/byit/test/SynText<br>  #20 = Utf8               java/lang/Object<br>{<br>  public int i;<br>    descriptor: I<br>    flags: ACC_PUBLIC<br><br>  public com.byit.test.SynText();<br>    descriptor: ()V<br>    flags: ACC_PUBLIC<br>    Code:<br>      stack=1, locals=1, args_size=1<br>         0: aload_0<br>         1: invokespecial #1                  // Method java/lang/Object."<init>":()V<br>         4: return<br>      LineNumberTable:<br>        line 6: 0<br>      LocalVariableTable:<br>        Start  Length  Slot  Name   Signature<br>            0       5     0  this   Lcom/byit/test/SynText;<br><br>  public synchronized void syncTask();<br>    descriptor: ()V<br>    flags: ACC_PUBLIC, ACC_SYNCHRONIZED<br>    Code:<br>      stack=3, locals=1, args_size=1<br>         0: aload_0<br>         1: dup<br>         2: getfield      #2                  // Field i:I<br>         5: iconst_1<br>         6: iadd<br>         7: putfield      #2                  // Field i:I<br>        10: return<br>      LineNumberTable:<br>        line 10: 0<br>        line 11: 10<br>      LocalVariableTable:<br>        Start  Length  Slot  Name   Signature<br>            0      11     0  this   Lcom/byit/test/SynText;<br>}<br>SourceFile: "SynText.java"<br></init></init></init></init></code>

简化,在简化

<code> public synchronized void syncTask();<br>    descriptor: ()V<br>    flags: ACC_PUBLIC, ACC_SYNCHRONIZED<br>    Code:<br>      stack=3, locals=1, args_size=1<br>         0: aload_0<br>         1: dup</code>

我们能够看到 flags: ACC_PUBLIC, ACC_SYNCHRONIZED这样的一句话

从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。

那么在JAVA6之前,为什么synchronized会如此的慢?

那是因为,操作系统实现线程之间的切换需要系统内核从用户态切换到核心态!这个状态之间的转换,需要较长的时间,时间成本高!所以这也就是synchronized慢的原因!

三、锁膨胀的过程

在这之前,你需要知道什么是锁膨胀!他是JAVA6之后新增的一个概念!是一种针对之前重量级锁的一种性能的优化!他的优化,大部分是基于经验上的一些感官,对锁来进行优化!

①偏向锁

研究发现,大多数情况下,锁不仅不存在多线程竞争,而且还总是由一条线程获得!因为为了减少锁申请的次数!引进了偏向锁!在没有锁竞争的情况下,如果一个线程获取到了锁,那么锁就进入偏向锁的模式!当线程再一次请求锁时,无需申请,直接获取锁,进入方法!但是前提是没有锁竞争的情况,存在锁竞争,锁会立即膨胀,膨胀为轻量级锁!

②轻量级锁

偏向锁失败,那么锁膨胀为轻量级锁!此时锁机构变为轻量级锁结构!他的经验依据是:“绝大多数情况下,在整个同步周期内,不会存在锁的竞争”,故而,轻量级锁适合,线程交替进行的场景!如果在同一时间出现两条线程对同一把锁的竞争,那么此时轻量级锁就不会生效了!但是,jdk官方为了是锁的优化性能更好,轻量级锁失效后,并不会立即膨胀为重量级锁!而是将锁转换为自旋锁状态!

③自旋锁

轻量级锁失败后,为了是避免线程挂起,引起内核态的切换!为了优化,此时线程会进入自选状态!他可能会进行几十次,上百次的空轮训!为什么呢?又是经验之谈!他们认为,大多数情况下,线程持有锁的时间都不会太长!做几次空轮训,就能大概率的等待到锁!事实证明,这种优化方式确实有效!最后如果实在等不到锁!没办法,才会彻底升级为重量级锁!

④锁消除

jvm在进行代码编译时,会基于上下文扫描;将一些不可能存在资源竞争的的锁给消除掉!这也是JVM对于锁的一种优化方式!不得不感叹,jdk官方的脑子!举个例子!在方法体类的局部变量对象,他永远也不可能会发生锁竞争,例如:

<code>/**<br> * @author huangfu<br> */<br>public class SynText {<br>    public static void add(String name1 ,String name2){<br>        StringBuffer sb = new StringBuffer();<br>        sb.append(name1).append(name2);<br>    }<br><br>    public static void main(String[] args) {<br>        for (int i = 0; i             add("w"+i,"q"+i);<br>        }<br>    }<br>}</code>

不能否认,StringBuffer是线程安全的!但是他永远也不会被其他线程引用!故而,锁失效!故而,被消除掉!

以上がJava の synchronized キーワードは、スレッド同期を実現するために使用されます。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。