当前位置: 首页
前端开发
Java JUC Synchronized源码分析笔记

Java JUC Synchronized源码分析笔记

热心网友 时间:2026-06-13
转载

synchronized是Java悲观锁的实现,能保证原子性、有序性和可见性,在多线程访问共享资源时解决并发问题。有三种用法:修饰静态方法锁类对象、修饰普通方法锁实例对象、修饰代码块锁指定对象。其原理基于字节码中的monitorenter和monitorexit指令,通过对象监视器实现线程互斥。

在深入探讨 Ja va 并发编程时,synchronized 是一个绕不开的核心关键字。很多人觉得它既笨重又过时,但事实上,在大多数实际场景下,它依然是解决并发问题最可靠手段之一。今天,我们就将synchronized彻底掰开揉碎,从它是什么、何时使用、如何运用,到背后的字节码与汇编原理,一次性讲清楚。

1. 什么是 synchronized

synchronized 是 Ja va 中悲观锁的一种典型实现。相较于 volatile,它属于重量级锁,但功能也更强大:能同时保证原子性、有序性和可见性。

  • 重量级
    会引起上下文切换(简单来说,就是会导致线程阻塞)。
  • 原子性
    synchronized 修饰的方法或代码块,在 JVM 视角中是一个不可分割的整体,不会被其他线程中断。
  • 有序性
    线程 A 释放锁之前的所有写操作,对之后成功获取锁的线程 B 都是可见的。更严谨的说法是:线程 A 对于锁 X 的释放,happens-before 线程 B 对于锁 X 的申请。这意味着 A 在释放锁之前修改的所有内容,B 获取锁后都能清晰看到。
  • 可见性
    当线程退出同步块时,所有在临界区内修改的共享变量都会被强制刷新回主内存。这样其他线程下次读取时就能拿到最新值。

2. 何时使用 synchronized

2.1. 多线程访问共享资源时的并发问题

在多线程开发中,线程之间经常需要读写共享变量来协作完成任务。但如果操作顺序出现混乱,结果就会变得离谱。来看一个简单的示例:

	public class MultiThread	{	    private static int val = 0;	 	    public static void main(String[] args) throws InterruptedException	    {	        Thread thread1 = new Thread(()->{	            for (int i = 0; i < 100000; i++)	            {	                val++;	            }	        });	 	        Thread thread2 = new Thread(()->{	            for (int i = 0; i < 100000; i++)	            {	                val--;	            }	        });	 	 	        thread1.start();	        thread2.start();	 	        thread1.join();	        thread2.join();	        System.out.println(val);	    }	}

thread1 对 val 进行 10 万次递增,thread2 对 val 进行 10 万次递减,按理结果应为 0,但实际运行结果每次都不相同。

2.1.1. 原因分析

假设两个线程的执行顺序被打乱,例如:

  • thread1
	第1步:thread1读取内存中的val到工作内存中,值为0	第2步:thread1对val+1,写回工作内存,此时工作内存中的值为1	第3步:thread1失去cpu	第8步:thread1把工作内存中的1写回主内存 //此时主内存中的值为1!!!
  • thread2
	第4步:thread2读取内存中的val到工作内存中,值为0	第5步:thread2对val-1,写回工作内存	第6步:thread2把工作内存中的值写回主内存 //此时主内存中的值为-1	第7步:thread2失去cpu

最终主内存中的 val 变为 -1,完全不符合预期。问题根源在于“读取→计算→写回”这个三步操作并非原子性,两个线程交错执行就会导致数据混乱。

2.1.2. 解决方案

加一把锁就能解决问题。下面使用 synchronized 代码块进行包裹:

	public class MultiThread	{	    private static int val = 0;	 	    public static void main(String[] args) throws InterruptedException	    {	        Thread thread1 = new Thread(() -> {	 	            for (int i = 0; i < 100000; i++)	            {	                synchronized (MultiThread.class)	                {	                    val++;	                }	 	            }	        });	 	        Thread thread2 = new Thread(() -> {	 	            for (int i = 0; i < 100000; i++)	            {	                synchronized (MultiThread.class)	                {	                    val--;	                }	            }	        });	 	 	        thread1.start();	        thread2.start();	 	        thread1.join();	        thread2.join();	        System.out.println(val);	    }	}

这样一来,每次只允许一个线程进入临界区,结果就稳定为 0 了。

3. 如何使用 synchronized

synchronized 有三种用法,区别在于所锁定的对象不同。

3.1. 修饰 static 方法,使用的锁是当前类对象

	public class SychronizedTest1	{	    private static StringBuilder stringBuilder = new StringBuilder();	 	    public static void main(String[] args) throws InterruptedException	    {	 	        Thread addThread = new Thread(() -> {	            for (int j = 0; j < 5000; j++)	            {	 	                append("aaaa");	            }	        });	 	        Thread decrThread = new Thread(() -> {	            for (int j = 0; j < 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);	    }	 	 	}

3.2. 修饰普通方法,使用的锁是当前实例对象

	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(() -> {	            for (int j = 0; j < 5000; j++)	            {	 	                sychronizedTest2.append("aaaa");	            }	        });	 	        Thread decrThread = new Thread(() -> {	            for (int j = 0; j < 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);	    }	 	 }

这里需要特别留意:由于锁的是当前实例对象,如果创建了两个不同的实例对象,锁就会失效。请看下面的反例:

	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(() -> {	            for (int j = 0; j < 5000; j++)	            {	 	                sychronizedTest2.append("aaaa");	            }	        });	 	        Thread decrThread = new Thread(() -> {	            for (int j = 0; j < 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);	    }	 	 }

结果为 false,因为两个线程使用的锁对象不同,彼此不干扰,自然线程不安全。

3.3. 修饰代码块,使用的锁是括号里指定的对象

	public class SychronizedTest3	{	    private static StringBuilder stringBuilder = new StringBuilder();	 	    public static void main(String[] args) throws InterruptedException	    {	 	        Thread addThread = new Thread(() -> {	            for (int j = 0; j < 5000; j++)	            {	 	                append("aaaa");	            }	        });	 	        Thread decrThread = new Thread(() -> {	            for (int j = 0; j < 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);	        }	    }	 	 }

使用 SychronizedTest3.class 作为锁,所有线程共用同一把锁,自然就保证了安全性。

4. synchronized 代码块原理分析

4.1. 字节码实验

在 IDEA 中运行以下代码,然后通过 Show Bytecode 插件查看字节码:

	public class MultiThread	{	    private static int val = 0;	 	    public static void main(String[] args) throws InterruptedException	    {	        Thread thread1 = new Thread(() -> {	 	            for (int i = 0; i < 100000; i++)	            {	                synchronized (MultiThread.class)	                {	                    val++;	                }	 	            }	        });	 	        Thread thread2 = new Thread(() -> {	 	            for (int i = 0; i < 100000; i++)	            {	                synchronized (MultiThread.class)	                {	                    val--;	                }	            }	        });	 	 	        thread1.start();	        thread2.start();	 	        thread1.join();	        thread2.join();	        System.out.println(val);	    }	}

  • 字节码如下:

    可以看到第 18-21 行对应的字节码中包含 MONITORENTERMONITOREXIT 指令。也就是说,执行同步代码块前先执行 monitorenter,之后执行 monitorexit。JVM 指令手册中明确说明:MONITORENTER 表示进入并获取对象监视器,MONITOREXIT 表示释放并退出对象监视器。下图是手册截图:

4.1.1. monitor 究竟是什么

简单来说,每个对象都可以看作是一个 monitor。当这个对象被用作 monitor 时,同一时刻只能由一个线程持有。所谓“持有”,其实就是做一个标记——这个标记就打在 Ja va 对象头中。

4.1.1.1. JVM 对象组成

这部分可以参考《对象在内存中的布局》那篇文章,这里不再展开。

4.2. 汇编代码实验

4.2.1. 下载编译 hsdis-amd64.dll

参考“How to build hsdis-amd64.dll and hsdis-i386.dll on Windows”或直接使用 hsdis-amd64.7z

4.3. 放入 JRE bin 目录下

4.3.1. 对比实验

  • 未加 synchronized
	public class TestSynchronized	{	    private static int i = 0;	    public static void main(String[] args)	    {	        test();	    }	 	    private static void test()	    {	        i++;	    }	}
  • 加了 synchronized
	public class TestSynchronized	{	    private static int i = 0;	    public static void main(String[] args)	    {	        test();	    }	 	    private static void test()	    {	        synchronized (TestSynchronized.class)	        {	            i++;	        }	    }	}

4.3.2. 添加 JVM 参数运行

	-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:-Inline -XX:CompileCommand=print,*TestSynchronized.test

在 IDEA 中配置参数的位置如下图所示:

4.3.3. 输出结果对比

  • 加 synchronized.txt
  • 没加 synchronized.txt
    使用 BeyondCompare 进行对比,可以发现加了 synchronized 的版本多出了 lockmonitorenter 等指令。具体截图如下:

4.4. 原子性

从汇编代码可以看出,monitorentermonitorexit 包裹了下面这几条指令:

	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)

此外,monitorenter 之前还使用了原子操作 lock cmpxchg %rsi,(%rdi) 来完成中间值交换。如果交换成功则直接 goto 退出;如果失败,则跳转到 jne 继续循环,直至成功。这就保证了临界区内的操作不会被中断。

4.5. 可见性

monitor enter 之后、临界区开始之前,会插入一个 获取屏障;在临界区结束之后、monitor exit 之前,会插入一个 释放屏障。这两个屏障保证了临界区内的任何读写操作都不会被重排序到临界区之外。示意图如下:

4.6. 有序性

这一部分与 volatile 的机制类似。在临界区结束后、monitor exit 之前插入 释放屏障,确保屏障之前的任何读写操作都先于这个 monitor exit(相当于写操作)被提交;在 monitor enter 之后、临界区开始前插入 获取屏障,确保这个 monitor enter(相当于读操作)先于屏障之后的任何读写操作被提交。图示如下:

5. synchronized 方法原理分析

	public class MultiThread2	{	    private static int val = 0;	 	    public static void main(String[] args) throws InterruptedException	    {	        Thread thread1 = new Thread(()->{	            for (int i = 0; i < 100000; i++)	            {	                incr();	            }	        });	 	        Thread thread2 = new Thread(()->{	            for (int i = 0; i < 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++;	    }	}

再来查看字节码,这次使用的是方法级别的 synchronized。字节码中没有 MONITORENTER/MONITOREXIT 指令,而是多了一个 ACC_SYNCHRONIZED 标志。当方法被调用时,JVM 检查到这个标志,就会自动获取和释放监视器。具体字节码如下图所示:

来源:https://juejin.cn/post/7647800502465642559

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
DCloud平台选型指南 适用场景与核心差异详解

DCloud平台选型指南 适用场景与核心差异详解

DCloud是一个围绕跨平台开发与流应用构建的技术生态,旨在帮助前端开发者使用Web技术开发高性能移动应用。其核心产品包括集成开发环境HBuilderX和基于Vue js的uni-app框架,支持一套代码发布至iOS、Android、Web及多端小程序。相比ReactNative和Flutter,uni-app在多端覆盖和Vue技术栈学习成本上具有优势,适合

时间:2026-07-10 06:47
DCloud使用教程与常见问题解决方案详解

DCloud使用教程与常见问题解决方案详解

DCloud提供跨平台应用开发方案,核心为HBuilderX与uni-app框架。开发者使用Vue js语法编写代码,可编译发布至iOS、Android、Web及小程序等多端。流程涵盖项目创建、开发调试与真机预览。常见问题如设备识别、编译失败等可通过检查设置、查看日志与条件编译解决。应用完成后支持云打包生成安装包。

时间:2026-07-10 06:47
DCloud新手入门教程 从零开始快速掌握开发技巧

DCloud新手入门教程 从零开始快速掌握开发技巧

DCloud是一个基于Web技术的跨平台应用开发平台,允许开发者使用HTML5等语言编写代码,并编译为iOS、Android及小程序应用,实现“一次编写,多端发布”。开发需使用HBuilderX,基于Vue js进行页面开发,平台提供丰富组件与API,并支持插件市场扩展功能。

时间:2026-07-10 06:47
DCloud新手入门指南从零开始了解这个开发平台

DCloud新手入门指南从零开始了解这个开发平台

DCloud是一个移动应用开发平台,旨在帮助前端开发者使用Web技术高效开发性能接近原生的跨平台应用。其核心产品包括HBuilderX开发环境和基于Vue js的uni-app框架,可实现一次开发、多端发布。平台通过完整工具链和插件生态降低开发门槛,适合需要快速迭代、覆盖多端的应用场景。

时间:2026-07-10 06:47
Zepto实战项目优化技巧与应用场景详解

Zepto实战项目优化技巧与应用场景详解

Zepto js作为轻量级JavaScript库,专为移动端优化,体积仅约10KB。它支持模块化定制,可按需构建以减小体积。使用时需减少DOM操作、善用事件委托,并可混合原生API提升性能。Zepto能融入现代工程流,但需注意其与jQuery的API差异及兼容性限制,适合在移动端性能优先的场景中替代jQuery。

时间:2026-07-10 06:47
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 热门数据榜