我去,你竟然还不会用 synchronized
二哥,离你上一篇我去已经过去两周时间了,这个系列还不打算更新吗?着急着看呢。
以上是读者 Jason 发来的一条信息,不看不知道,一看真的是吓一跳,上次我去是 4 月 3 号更新的,离现在一个多月了,可不只是两周时间啊。可能我自己天天写,没觉得时间已经过去这么久了,是时候带来新的一篇“我去”了。

这次没有代码 review,是同事小王直接问我的,“青哥,能给我详细地说一说 synchronized 关键字怎么用吗?”他问的态度很谦逊,但我还是忍不住破口大骂:“我擦,小王,你丫的竟然不会用 synchronized,我当初是怎么面试你进来的!”
(我笔名是沉默王二,读者都叫二哥,但在公司不是的,同事叫我青哥,想知道我真名的,可以搜《Web全栈开发进阶之路》)
简单地说,当两个或者两个以上的线程同一时间要修改同一个可变的共享数据时,就需要一些保护措施,否则,共享数据修改后的结果大概率会超出你的预期。对于初学者来说,synchronized 关键字就是最好用的一种解决方案。
01、为什么需要保护
可能很多初学者不明白,为什么多线程环境下,可变共享变量修改后的结果会超出预期。为了解释清楚这一点,来看一个例子。
public class SynchronizedMethod {
private int sum;
public int getSum() {
return sum;
}
public void setSum(int sum) {
this.sum = sum;
}
public void calculate() {
setSum(getSum() + 1);
}
}
SynchronizedMethod 是一个非常简单的类,有一个私有的成员变量 sum,对应的 getter/setter,以及给 sum 加 1 的 calculate()
方法。
然后,我们来给 calculate()
方法写一个简单的测试用例。
可能一些初学者还不知道怎么快速创建测试用例,我这里就手摸手地现场教学下。
第一步,把鼠标移动到类名上,会弹出一个提示框。

第二步,点击「More actions」按钮,会弹出以下提示框。

第三步,选择「Create Test」,弹出创建测试用例的对话框。

选择最新的 JUnit5,如果项目之前没有引入 JUnit5 依赖的话,IDEA 会提醒你,点击 Fix,IDEA 会自动帮你添加,非常智能化。在对话框中勾选要创建测试用例的方法——calculate()
。
点击 OK 按钮后,IDEA 会在 src 的同级目录 test 下创建一个名为 SynchronizedMethodTest 的测试类:
class SynchronizedMethodTest {
@Test
void calculate() {
}
}
calculate()
方法上会有一个 @Test
的注解,表示这是一个测试方法。添加具体的代码,如下所示:
ExecutorService service = Executors.newFixedThreadPool(3);
SynchronizedMethod summation = new SynchronizedMethod();
IntStream.range(0, 1000)
.forEach(count -> service.submit(summation::calculate));
service.awaitTermination(1000, TimeUnit.MILLISECONDS);
assertEquals(1000, summation.getSum());
1)Executors.newFixedThreadPool()
方法可以创建一个指定大小的线程池服务 ExecutorService。
2)通过 IntStream.range(0, 1000).forEach()
来执行 calculate()
方法 1000 次。
3)通过 assertEquals()
方法进行判断。
运行该测试用例,结果会是什么呢?

很不幸,失败了。预期的值为 1000,但实际的值是 976。这是因为多线程环境下,可变的共享数据没有得到保护。
02、synchronized 的用法
这么说吧,初学者在遇到多线程问题时,只要 synchronized 关键字使用得当,问题就能够迎刃而解。记得我刚回洛阳的时候,面试官问我,项目中是怎么解决并发问题的呢?我就说用 synchronized 关键字,至于其他的一些锁机制,我那时候还不知道。
嗯,面试官好像也不知道,因为小公司嘛,并发的量级有限,性能也不用考量得太过深入(大公司的读者可以呵呵了)。接下来,就随我来,一起看看 synchronized 最常见的三种用法吧。
1)直接用在方法上,就像下面这样:
public synchronized void synchronizedCalculate() {
setSum(getSum() + 1);
}
修改一下测试用例:
@Test
void synchronizedCalculate() throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(3);
SynchronizedMethod summation = new SynchronizedMethod();
IntStream.range(0, 1000)
.forEach(count -> service.submit(summation::synchronizedCalculate));
service.awaitTermination(1000, TimeUnit.MILLISECONDS);
assertEquals(1000, summation.getSum());
}
这时候,再运行测试用例就通过了。因为 synchronized 关键字会对 SynchronizedMethod 对象进行加锁,同一时间内只允许一个线程对 sum 进行修改。这就好像有一间屋子,线程进入屋子里面才可以对 sum 加 1,而 synchronized 就相当于在门上加了一个锁,一个线程进去后就锁上门,修改完 sum 后,下一个线程再进去,其他线程就在门外候着。
2)用在 static 方法上,就像下面这样:
public class SynchronizedStaticMethod {
public static int sum;
public synchronized static void synchronizedCalculate() {
sum = sum + 1;
}
}
sum 是一个静态变量,要修改静态变量的时候,就需要把方法也变成 static 的。
来新建一个测试用例:
class SynchronizedStaticMethodTest {
@Test
void synchronizedCalculate() throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(3);
IntStream.range(0, 1000)
.forEach(count -> service.submit(SynchronizedStaticMethod::synchronizedCalculate));
service.awaitTermination(1000, TimeUnit.MILLISECONDS);
assertEquals(1000, SynchronizedStaticMethod.sum);
}
}
静态方法上添加 synchronized 的时候就不需要实例化对象了,直接使用类名就可以引用方法和使用变量了。测试用例也是可以通过的。
synchronized static 和 synchronized 不同的是,前者锁的是类,同一时间只能有一个线程访问这个类;后者锁的是对象,同一时间只能有一个线程访问方法。
3)用在方法块上,就像下面这样:
public void synchronisedThis() {
synchronized (this) {
setSum(getSum() + 1);
}
}
这时候,将 this 传递给了 synchronized 代码块,当在某个线程中执行这段代码块,该线程会获取 this 对象的锁,从而使得其他线程无法同时访问该代码块。如果方法是静态的,我们将传递类名代替对象引用,示例如下所示:
public static void synchronisedThis() {
synchronized (SynchronizedStaticMethod.class) {
sum = sum + 1;
}
}
新建一个测试用例:
@Test
void synchronisedThis() throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(3);
SynchronizedMethod summation = new SynchronizedMethod();
IntStream.range(0, 1000)
.forEach(count -> service.submit(summation::synchronisedThis));
service.awaitTermination(1000, TimeUnit.MILLISECONDS);
assertEquals(1000, summation.getSum());
}
测试用例和 synchronized 方法的大差不差,运行后也是可以通过的。两者之间有所不同,synchronized 代码块的锁粒度要比 synchronized 方法小一些,因为 synchronized 代码块所在的方法里还可以有其他代码。

好了,我亲爱的读者朋友,以上就是本文的全部内容了,synchronized 的三种用法你一定掌握了吧?觉得文章有点用的话,请微信搜索「沉默王二」第一时间阅读。
本文 GitHub 已经收录,有大厂面试完整考点,欢迎 Star。
我是沉默王二,一枚有趣的程序员,关注即可提高学习效率。最后,请无情地点赞、收藏、留言吧,谢谢。
我去,你竟然还不会用 synchronized的更多相关文章
- 技术大佬:我去,你竟然还在用 try–catch-finally
二哥,你之前那篇 我去 switch的文章也特么太有趣了,读完后意犹未尽啊,要不要再写一篇啊?虽然用的是 Java 13 的语法,对旧版本不太友好.但谁能保证 Java 不会再来一次重大更新呢,就像 ...
- 技术大佬:我去,你竟然还不会用 this 关键字
上一篇文章写的是 Spring Boot 的入门,结果有读者留言说,Java 都还没搞完,搞什么 Spring Boot,唬得我一愣一愣的.那这篇就继续来搞 Java,推出广受好评的我去系列第四集:你 ...
- 我去,你竟然还不会用 Java final 关键字
写一篇文章容易吗?太不容易了,首先,需要一个安静的环境,这一点就非常不容易.很多小伙伴的办公室都是开放式的,非常吵,况且上班时间写的话,领导就不高兴了:只能抽时间写.其次,环境有了,还要有一颗安静的心 ...
- postman一些你不常用的实用技巧,竟然还能这么玩
序言 各位好啊,我是会编程的蜗牛,作为java开发者,平时调试接口的时候,肯定需要用到接口调试工具,或者Swagger之类的.Swagger的优势在于它可以将后台加的一些接口注释信息直接展示出来,但是 ...
- 什么?你竟然还没有用这几个chrome插件?
前言 其实18年之前写过一篇关于chrome插件的文章,里面安利了4个chrome插件.鉴于这已经是9102年了,之前觉得好用的chrome插件跟新了解到的比起来,还是差了那么点味道.所以决定再更新一 ...
- 使用过Redis,我竟然还不知道Rdb
目录 使用过Redis,那就先说说使用过那些场景吧 Rdb文件是什么,它是干什么的 分析工具 小结 联想 推荐阅读 使用过Redis,那就先说说使用过那些场景吧 字符串缓存 //举例 $redis-& ...
- 写了这么多年 JavaScript ,竟然还不知道这些技巧?
不少人有五年的 JavaScript 经验,但实际上可能只是一年的经验重复用了五次而已.完成同样的逻辑和功能,有人可以写出意大利面条一样的代码,也有人两三行简洁清晰的代码就搞定了.简洁的代码不但方便阅 ...
- netstat 竟然还能这么玩儿?
一次摸鱼的机会,看到群里小伙伴问了一嘴 netstat -tnpl 这个命令是干啥的,这个命令用过很多,但是我其实也没有认真研究过,但是这是一个问题,我不能放过它,而且 netstat 这个命令我日常 ...
- 合同主体列表添加两条合同主体,返回合并支付页面,支付总弹"请选择合同主体",删除后,竟然还能支付(改合并支付页面的字段状态)
bug描述: 操作步骤:1.进入"商标续展"产品详情页面,点击立即购买(数量设为2),进入合并订单界面,选择合同主体,点击全部,清空所有合同主体2.新建合同主体保存,设置该合同主体 ...
随机推荐
- The equation SGU - 106
题目链接:https://codeforces.com/problemsets/acmsguru/problem/99999/106 这个题是关于EXGCD特别好的一个题目.题目大意:有一个等式ax+ ...
- 包、mode模式、if-else语句、switch语句
目录 包 mode模式 if-else语句 循环 switch语句 包 //1 在同一个包下(文件夹下),包名必须一致 //2 以后,包名就是文件夹的名字 //3 同一个包下,同名函数只能有一个(in ...
- python之pymysql库连接mysql实现增、删、改、查
安装第三方库pymysql 命令行cmd下通过pip install pymysql进行安装,安装完成后自行pip list可查看对应的版本信息 建立连接 1 #导入pymysql库 2 import ...
- Servlet 和 Servlet容器
Servlet 很多同学可能跟我一样始终没有搞清楚到底什么是 Servlet,什么是 Servlet 容器.网上看了很多帖子,或许人家说的很清楚,但是自己的那个弯弯就是拐不过来. 想了很久说一下自己的 ...
- js中string转map的方法
例如: var r = "{'msg':'你好'}" ; var map = eval("("+r+")"); //r为String类型的数 ...
- Python大数据与机器学习之NumPy初体验
本文是Python大数据与机器学习系列文章中的第6篇,将介绍学习Python大数据与机器学习所必须的NumPy库. 通过本文系列文章您将能够学到的知识如下: 应用Python进行大数据与机器学习 应用 ...
- css: scroll-table
.scroll-table { table tbody { display: block; max-height: 120px; overflow-y: scroll; } table thead,t ...
- python-trade
https://tool.lu/pyc/在线反编译pyc import base64 correct = 'XlNkVmtUI1MgXWBZXCFeKY+AaXNt' flag = base64.b6 ...
- php token验证范例
<?php $module = $_GET['module']; $action = $_GET['action']; $token = md5sum($module.date('Y-m-d', ...
- 深入理解BIO、NIO、AIO
导读:本文你将获取到:同/异步 + 阻/非阻塞的性能区别:BIO.NIO.AIO 的区别:理解和实现 NIO 操作 Socket 时的多路复用:同时掌握 IO 最底层最核心的操作技巧. BIO.NIO ...