游乐游手机版
首页/前端开发/文章详情

Java JUC Synchronized源码分析笔记

时间:2026-06-13 06:47
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
上一篇Angular7实现拖放DragDrop示例详解 下一篇响应式系统对比:链表提升computed更新效率
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
Layui弹出层监听子页面键盘快捷键实现方法
前端开发 · 2026-07-06

Layui弹出层监听子页面键盘快捷键实现方法

子页面键盘事件监听需在DOM加载完成后绑定,父页无法直接监听子页按键,必须由子页自身监听后通过parent或postMessage通知父页。典型写法为子页调用父页已定义的关闭函数。需注意焦点状态、输入法及layui版本兼容性等陷阱。

Layui表单提交时携带当前页面Meta信息的实现方法
前端开发 · 2026-07-06

Layui表单提交时携带当前页面Meta信息的实现方法

Layui表单提交不会自动携带页面Meta信息,需在form on( submit )回调中手动读取meta内容并拼接到表单数据,注意后端字段映射及特殊字符编码,多meta时按需选取。

HTML5拖拽事件流状态转移监控调试
前端开发 · 2026-07-06

HTML5拖拽事件流状态转移监控调试

HTML5拖拽事件流易因漏监听或未调用preventDefault而中断。需掌握dragstart设置数据、dragover接受放置、drop触发条件等关键点。通过统一日志捕获事件上下文、识别常见状态丢失场景并配合可视化面板,可清晰定位拖拽过程断点。

uni-app实现小红书商品详情图卡片切换
前端开发 · 2026-07-06

uni-app实现小红书商品详情图卡片切换

通过手写touch事件与transform控制五张卡片,动态计算translateX、scale、opacity及z-index模拟层叠滑动效果。滑动距离超过80rpx触发切换,否则复位。图片仅渲染当前及前后两张,有效优化加载性能与渲染效率。

图像旋转倾斜与扭曲的Canvas像素矩阵变换
前端开发 · 2026-07-06

图像旋转倾斜与扭曲的Canvas像素矩阵变换

Canvas图像变形本质是操作坐标系,图像被动跟随。旋转需先平移原点至目标中心再旋转后复位;倾斜通过仿射变换矩阵实现;扭曲无原生API,可用分块模拟或转用WebGL。每次变换前保存状态,完成后恢复,避免坐标系偏移。