浠液洛花 發表於 2025-6-22 09:54:00

3. Java JUC源码分析系列笔记-Synchronized

<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>1. 是什么</li><li>2. 什么时候使用<ul><li>2.1. 多线程访问共享资源时的并发问题<ul><li>2.1.1. 究其原因</li><li>2.1.2. 解决的方法</li></ul></li></ul></li><li>3. 如何使用<ul><li>3.1. 修饰 static 方法。使用的锁是当前类对象</li><li>3.2. 修饰普通方法。使用的锁是当前实例对象</li><li>3.3. 修饰代码块。使用的锁是()里指定的对象</li></ul></li><li>4. sychronized 代码块原理分析<ul><li>4.1. 字节码实验<ul><li>4.1.1. monitor 是个啥玩意<ul><li>4.1.1.1. JVM 对象组成</li></ul></li></ul></li><li>4.2. 汇编代码实验<ul><li>4.2.1. 下载编译 hsdis-amd64.dll</li></ul></li><li>4.3. 放入 JRE bin 目录下<ul><li>4.3.1. 对比实验</li><li>4.3.2. 加上 jvm 参数运行</li><li>4.3.3. 输出结果对比</li></ul></li><li>4.4. 原子性</li><li>4.5. 可见性</li><li>4.6. 有序性</li></ul></li><li>5. sychronized 方法原理分析</li><li>6. 参考</li></ul></div><p></p>
<h2 id="1-是什么">1. 是什么</h2>
<p>Java 中悲观锁的一种实现,相比于 volatile 是重量级锁,可以保证原子性、有序性、可见性</p>
<ul>
<li>
<p>重量级<br>
会引起上下文切换(会造成线程阻塞)</p>
</li>
<li>
<p>原子性<br>
synchronized 方法、synchronized 代码块被视作原子的</p>
</li>
<li>
<p>有序性<br>
线程 A 对于锁 X 的释放发生于线程 B 对于锁 X 的申请之前。<br>
也就是说线程 A 在释放锁之前的所有写操作造成的更新,之后线程 B 在申请锁之后的读操作都可以看到这些更新结果</p>
</li>
<li>
<p>可见性<br>
synchronized 方法或代码块里修改的共享变量,在退出临界区时会写回主内存</p>
</li>
</ul>
<h2 id="2-什么时候使用">2. 什么时候使用</h2>
<h3 id="21-多线程访问共享资源时的并发问题">2.1. 多线程访问共享资源时的并发问题</h3>
<p>当我们进行多线程开发的时候,需要在多个线程之间进行通信,而通信一般都是通过读写共享变量实现的,如果操作的顺序不当就会出现异常的结果。<br>
举个例子,如下一段程序</p>
<pre><code class="language-java">public class MultiThread
{
    private static int val = 0;

    public static void main(String[] args) throws InterruptedException
    {
      Thread thread1 = new Thread(()-&gt;{
            for (int i = 0; i &lt; 100000; i++)
            {
                val++;
            }
      });

      Thread thread2 = new Thread(()-&gt;{
            for (int i = 0; i &lt; 100000; i++)
            {
                val--;
            }
      });


      thread1.start();
      thread2.start();

      thread1.join();
      thread2.join();
      System.out.println(val);
    }
}
</code></pre>
<p>thread1 对 val 执行 100000 次加操作,而 thread2 对 val 执行 100000 此减操作,最终的结果应该是 0,但实际得出的结果却是不确定的。</p>
<h4 id="211-究其原因">2.1.1. 究其原因</h4>
<p>假设这两个线程为 thread1 和 thread2,操作如下:</p>
<ul>
<li>thread1</li>
</ul>
<pre><code>第1步:thread1读取内存中的val到工作内存中,值为0
第2步:thread1对val+1,写回工作内存,此时工作内存中的值为1
第3步:thread1失去cpu
第8步:thread1把工作内存中的1写回主内存 //此时主内存中的值为1!!!
</code></pre>
<ul>
<li>thread2</li>
</ul>
<pre><code>第4步:thread2读取内存中的val到工作内存中,值为0
第5步:thread2对val-1,写回工作内存
第6步:thread2把工作内存中的值写回主内存 //此时主内存中的值为-1
第7步:thread2失去cpu
</code></pre>
<p>由上面的步骤可以看出最后内存中的 val 为-1,但是正确的结果应该是 0 才对。</p>
<h4 id="212-解决的方法">2.1.2. 解决的方法</h4>
<p>也很简单,就是加锁,如下使用了 synchronized 代码块</p>
<pre><code class="language-java">public class MultiThread
{
    private static int val = 0;

    public static void main(String[] args) throws InterruptedException
    {
      Thread thread1 = new Thread(() -&gt; {

            for (int i = 0; i &lt; 100000; i++)
            {
                synchronized (MultiThread.class)
                {
                  val++;
                }

            }
      });

      Thread thread2 = new Thread(() -&gt; {

            for (int i = 0; i &lt; 100000; i++)
            {
                synchronized (MultiThread.class)
                {
                  val--;
                }
            }
      });


      thread1.start();
      thread2.start();

      thread1.join();
      thread2.join();
      System.out.println(val);
    }
}

</code></pre>
<h2 id="3-如何使用">3. 如何使用</h2>
<p>Synchronize 有三种用法</p>
<h3 id="31-修饰-static-方法使用的锁是当前类对象">3.1. 修饰 static 方法。使用的锁是当前类对象</h3>
<pre><code class="language-java">public class SychronizedTest1
{
    private static StringBuilder stringBuilder = new StringBuilder();

    public static void main(String[] args) throws InterruptedException
    {

      Thread addThread = new Thread(() -&gt; {
            for (int j = 0; j &lt; 5000; j++)
            {

                append("aaaa");
            }
      });

      Thread decrThread = new Thread(() -&gt; {
            for (int j = 0; j &lt; 5000; j++)
            {
                append("aaaa");

            }
      });

      addThread.start();
      decrThread.start();
      addThread.join();
      decrThread.join();


      String str = stringBuilder.toString();
      System.out.println(str);
      System.out.println(str.length());
      System.out.println(str.contains("a"));
      System.out.println(str.length() == 5000 * 2 * 4);//true
    }

    private synchronized static void append(String val)
    {
      stringBuilder.append(val);
    }


}
</code></pre>
<h3 id="32-修饰普通方法使用的锁是当前实例对象">3.2. 修饰普通方法。使用的锁是当前实例对象</h3>
<pre><code class="language-java">public class SychronizedTest2
{
    private static StringBuilder stringBuilder = new StringBuilder();

    public static void main(String[] args) throws InterruptedException
    {
      SychronizedTest2 sychronizedTest2 = new SychronizedTest2();

      Thread addThread = new Thread(() -&gt; {
            for (int j = 0; j &lt; 5000; j++)
            {

                sychronizedTest2.append("aaaa");
            }
      });

      Thread decrThread = new Thread(() -&gt; {
            for (int j = 0; j &lt; 5000; j++)
            {
                sychronizedTest2.append("aaaa");

            }
      });

      addThread.start();
      decrThread.start();
      addThread.join();
      decrThread.join();


      String str = stringBuilder.toString();
      System.out.println(str);
      System.out.println(str.length());
      System.out.println(str.contains("a"));
      System.out.println(str.length() == 5000 * 2 * 4);//true
    }

    private synchronized void append(String val)
    {
      stringBuilder.append(val);
    }


}
</code></pre>
<p>因为使用的是当前实例对象,如果创建两个实例对象,那么肯定是线程不安全了,如下:</p>
<pre><code class="language-java">public class SychronizedTest2
{
    private static StringBuilder stringBuilder = new StringBuilder();

    public static void main(String[] args) throws InterruptedException
    {
      SychronizedTest2 sychronizedTest2 = new SychronizedTest2();
      SychronizedTest2 sychronizedTest3 = new SychronizedTest2();

      Thread addThread = new Thread(() -&gt; {
            for (int j = 0; j &lt; 5000; j++)
            {

                sychronizedTest2.append("aaaa");
            }
      });

      Thread decrThread = new Thread(() -&gt; {
            for (int j = 0; j &lt; 5000; j++)
            {
                sychronizedTest3.append("aaaa");

            }
      });

      addThread.start();
      decrThread.start();
      addThread.join();
      decrThread.join();


      String str = stringBuilder.toString();
      System.out.println(str);
      System.out.println(str.length());
      System.out.println(str.contains("a"));
      System.out.println(str.length() == 5000 * 2 * 4);//false
    }

    private synchronized void append(String val)
    {
      stringBuilder.append(val);
    }


}
</code></pre>
<h3 id="33-修饰代码块使用的锁是里指定的对象">3.3. 修饰代码块。使用的锁是()里指定的对象</h3>
<pre><code class="language-java">public class SychronizedTest3
{
    private static StringBuilder stringBuilder = new StringBuilder();

    public static void main(String[] args) throws InterruptedException
    {

      Thread addThread = new Thread(() -&gt; {
            for (int j = 0; j &lt; 5000; j++)
            {

                append("aaaa");
            }
      });

      Thread decrThread = new Thread(() -&gt; {
            for (int j = 0; j &lt; 5000; j++)
            {
                append("aaaa");

            }
      });

      addThread.start();
      decrThread.start();
      addThread.join();
      decrThread.join();


      String str = stringBuilder.toString();
      System.out.println(str);
      System.out.println(str.length());
      System.out.println(str.contains("a"));
      System.out.println(str.length() == 5000 * 2 * 4);//true
    }

    private static void append(String val)
    {
      synchronized (SychronizedTest3.class)
      {
            stringBuilder.append(val);
      }
    }


}
</code></pre>
<h2 id="4-sychronized-代码块原理分析">4. sychronized 代码块原理分析</h2>
<h3 id="41-字节码实验">4.1. 字节码实验</h3>
<p>在 Idea 中运行下面的代码,并且使用 show byte code 插件查看字节码</p>
<pre><code class="language-java">public class MultiThread
{
    private static int val = 0;

    public static void main(String[] args) throws InterruptedException
    {
      Thread thread1 = new Thread(() -&gt; {

            for (int i = 0; i &lt; 100000; i++)
            {
                synchronized (MultiThread.class)
                {
                  val++;
                }

            }
      });

      Thread thread2 = new Thread(() -&gt; {

            for (int i = 0; i &lt; 100000; i++)
            {
                synchronized (MultiThread.class)
                {
                  val--;
                }
            }
      });


      thread1.start();
      thread2.start();

      thread1.join();
      thread2.join();
      System.out.println(val);
    }
}
</code></pre>
<p><img src="https://raw.githubusercontent.com/TDoct/images/master/img/20200114163219.png"></p>
<ul>
<li>字节码如下:<br>
<img src="https://raw.githubusercontent.com/TDoct/images/master/img/20191230155944.png"><br>
我们可以看到,18-21 的代码中对应的字节码有 MONITORENTER 和 MONITOREXIT 指令。<br>
即执行同步代码块之前首先要执行 monitorenter,执行同步代码块之后要执行 monitorexit。<br>
在 jvm 的指令手册中,MONITORENTER 表示进入并获取对象监视器,而 MONITOREXIT 表示释放并退出对象监视器,如下图:<br>
<img src="https://raw.githubusercontent.com/TDoct/images/master/img/20200118121619.png"></li>
</ul>
<h4 id="411-monitor-是个啥玩意">4.1.1. monitor 是个啥玩意</h4>
<p>每个对象都可以看作是一个 monitor。<br>
当这个对象作为 monitor 使用时,同一时间只能由一个线程持有。所谓持有其实就是做个标记,这个标记做在 java 对象头里面</p>
<h5 id="4111-jvm-对象组成">4.1.1.1. JVM 对象组成</h5>
<p>对象在内存中的布局.md</p>
<h3 id="42-汇编代码实验">4.2. 汇编代码实验</h3>
<h4 id="421-下载编译-hsdis-amd64dll">4.2.1. 下载编译 hsdis-amd64.dll</h4>
<p>参考How to build hsdis-amd64.dll and hsdis-i386.dll on Windows或者hsdis-amd64.7z</p>
<h3 id="43-放入-jre-bin-目录下">4.3. 放入 JRE bin 目录下</h3>
<p><img src="https://raw.githubusercontent.com/TDoct/images/master/img/20200114134345.png"></p>
<h4 id="431-对比实验">4.3.1. 对比实验</h4>
<ul>
<li>没有 sychronized</li>
</ul>
<pre><code class="language-java">public class TestSynchronized
{
    private static int i = 0;
    public static void main(String[] args)
    {
      test();
    }

    private static void test()
    {
      i++;
    }
}
</code></pre>
<ul>
<li>有 sychronized</li>
</ul>
<pre><code class="language-java">public class TestSynchronized
{
    private static int i = 0;
    public static void main(String[] args)
    {
      test();
    }

    private static void test()
    {
      synchronized (TestSynchronized.class)
      {
            i++;
      }
    }
}
</code></pre>
<h4 id="432-加上-jvm-参数运行">4.3.2. 加上 jvm 参数运行</h4>
<pre><code class="language-jvm">-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:-Inline -XX:CompileCommand=print,*TestSynchronized.test
</code></pre>
<p>使用 IDEA 的话如下图:<br>
<img src="https://raw.githubusercontent.com/TDoct/images/master/img/20200113223348.png"></p>
<h4 id="433-输出结果对比">4.3.3. 输出结果对比</h4>
<ul>
<li>加 synchronized.txt</li>
<li>没加 synchronized.txt<br>
使用 BeyondCompare 对比发现加 synchronized 的多了 lock 和 monitorenter 等指令,如下:<br>
<img src="https://raw.githubusercontent.com/TDoct/images/master/img/20200113225819.png"><br>
<img src="https://raw.githubusercontent.com/TDoct/images/master/img/20200114170939.png"></li>
</ul>
<h3 id="44-原子性">4.4. 原子性</h3>
<p>从汇编代码可以看出 monitorenter 与 monitorexit 包裹了如下代码:</p>
<pre><code class="language-bat">0x00000000033254d5: mov    0x68(%rax),%esi    ;*getstatic i //从内存中读取val的值到寄存器中
                                                ; - com.zsk.test.TestSynchronized::test@5 (line 15)

0x00000000033254d8: inc    %esi//执行val++
0x00000000033254da: mov    %esi,0x68(%rax)    ;*putstatic i//将val的值从寄存器写回内存
                                                ; - com.zsk.test.TestSynchronized::test@10 (line 15)
</code></pre>
<p>并且 monitorenter 前采用了原子操作<code>lock cmpxchg %rsi,(%rdi)</code>进行中间值的交换。<br>
如果交换成功,则执行 goto 直接退出当前函数。如果失败,执行 jne 跳转指令,继续循环执行,直到成功为止。</p>
<h3 id="45-可见性">4.5. 可见性</h3>
<p>在 monitor enter 后临界区开始前的地方插入一个获取屏障,在临界区结束后 moniter exit 前的地方插入释放屏障。<br>
获取屏障和释放屏障保证了临界区内的任何读写操作无法被重排序到临界区外<br>
<img src="https://raw.githubusercontent.com/TDoct/images/master/img/20200114195802.png"></p>
<h3 id="46-有序性">4.6. 有序性</h3>
<p>跟 volatile 一样<br>
在临界区结束后 moniter exit 前之前插入<strong>释放屏障</strong>使得该屏障之前的任何读写操作都先于这个 moniter exit(相当于写)被提交;<br>
在 monitor enter 后临界区开始前插入<strong>获取屏障</strong>使得这个 monitor enter(相当于读)先于该屏障之后的任何读写操作被提交。<br>
<img src="https://raw.githubusercontent.com/TDoct/images/master/img/20200114195802.png"></p>
<h2 id="5-sychronized-方法原理分析">5. sychronized 方法原理分析</h2>
<pre><code class="language-java">public class MultiThread2
{
    private static int val = 0;

    public static void main(String[] args) throws InterruptedException
    {
      Thread thread1 = new Thread(()-&gt;{
            for (int i = 0; i &lt; 100000; i++)
            {
                incr();
            }
      });

      Thread thread2 = new Thread(()-&gt;{
            for (int i = 0; i &lt; 100000; i++)
            {
                decr();
            }
      });


      thread1.start();
      thread2.start();

      thread1.join();
      thread2.join();
      System.out.println(val);
    }

    private synchronized static void decr()
    {
      val--;
    }

    private synchronized static void incr()
    {
      val++;
    }
}
</code></pre>
<p>字节码如下图:<br>
<img src="https://raw.githubusercontent.com/TDoct/images/master/img/20200130204731.png"><br>
<img src="https://raw.githubusercontent.com/TDoct/images/master/img/20200130204718.png"></p>
<blockquote>
<p>在 VM 字节码层面并没有任何特别的指令来实现被 synchronized 修饰的方法,而是在 Class 文件的方法表中将该方法的 access_flags 字段中的 synchronized 标志位置 1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的 Class 在 JVM 的内部对象表示 Klass 做为锁对象。</p>
</blockquote>
<h2 id="6-参考">6. 参考</h2>
<ul>
<li>精确解释 java 的 volatile 之可见性、原子性、有序性(通过汇编语言) - tantexian 的博客空间 - OSCHINA</li>
<li>java 架构师课程 一节课学透 Synchronized 的设计原理 图灵学院_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili</li>
<li>Guide to the Synchronized Keyword in Java | Baeldung</li>
<li>彻底理解 synchronized - 掘金</li>
<li>【死磕 Java 并发】- synchronized 的锁膨胀过程 - Java 技术驿站-Java 技术驿站</li>
<li>JVM 内部细节之一:synchronized 关键字及实现细节(轻量级锁 Lightweight Locking) - JAVA Miner - 博客园</li>
<li>Java synchronized 能防止指令重排序吗? - 知乎</li>
<li>Synchronized 之三:Synchronized 与线程中断、线程 wait - duanxz - 博客园</li>
</ul><br><br>
来源:https://www.cnblogs.com/ThinkerQAQ/p/18942213
頁: [1]
查看完整版本: 3. Java JUC源码分析系列笔记-Synchronized