一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
你好呀,我是歪歪。
事情是这样的,前几天有一个读者给我发消息,说他面试的时候遇到一个奇形怪状的面试题。
歪师傅纵横面试界多年,最喜欢的是奇形怪状的面试题。
可以说是见过大场面的人,所以让他描述一下具体啥问题。
据他的描述,这道面试题是这样的:
在多线程环境下使用 ConcurrentHashMap 时,是否需要将其声明为 volatile 以确保线程安全?
呃...
这个题...
有点意思...

简单盘一盘
这个题听起来确实有点奇奇怪怪的,多线程、ConcurrentHashMap(后续文中用 CHM 代替)、volatile、线程安全...
乍一听有一种全都是我熟悉的技术点,但是组合在一起,突然有点不认识了的陌生感。
但是如果你真的对上面这几个技术点达到了熟悉的程度,那么简单梳理一下之后,你又会觉得线索太多,甚至有点不知道从何说起。
先梳理清楚两个关键点:
CHM 是干啥的? volatile 又是干啥的?
首先,CHM 是八股中老大哥了,一般它和 HashMap 会在面试环节成对出现。
比如这样式儿的:HashMap 不是线程安全的,那我们应该怎么办呢?
然后 CHM 就噼里啪啦一大堆开始背诵起来了。
但是在这篇文章中,关于 CHM 我们需要注意的就一个点:
CHM 是线程安全的,但是它的线程安全仅限于方法内部的操作。
然后,volatile 是干啥的?
这种老八股应该是张口就来:
volatile 可以保证变量的可见性,即一个线程修改了被 volatile 修饰的变量,其他线程能立即看到新值。
这里画个重点:变量。
如果要把 CHM 和 volatile 牵扯到一起,那么他们就需要对齐一下颗粒度:CHM 需要是一个变量。
当 CHM 在程序的引用中会发生变化时,讨论 volatile 才有意义。
所以,这个面试题的答案也就呼之欲出了。
答案就是:得结合代码,看具体场景,分两种情况去讨论。
第一种情况是 CHM 的引用不会发生变化,就不需要加 volatile。
比如下面这种写法:
private static final ConcurrentHashMap<String, String> chm = new ConcurrentHashMap<>();
这里的 final 已经保证引用不可变,无论多少个线程在同时操作这个 CHM,都能确保看到的始终是同一个对象。
至于线程安全问题,CHM 内部的方法自会处理好并发问题。
第二种情况是 CHM 的引用会变,比如这样的代码逻辑:
private volatile ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
public void updateCache() {
ConcurrentHashMap<String, String> newCache = new ConcurrentHashMap<>();
// 填充新数据...
cache = newCache;
}
因为我们程序中有把 newCache 赋值给 cache 的操作,而这个 cache 可能又不只是一个线程在操作。
所以这个场景下,就需要使用 volatile,保证当前线程对 cache 操作之后,其他线程能立刻看到新引用。
从而保证了线程安全。
现在,我们再回过头去看这个问题,应该就清晰很多了:
在多线程环境下使用 ConcurrentHashMap 时,是否需要将其声明为 volatile 以确保线程安全?
这个问题确实是有点陷阱的,不能直接回答需要或者不需要。
需要面试者结合自己的理解,去分析出不同的场景,得到上面的回答。
想到另一个题
在分析上面这个问题的时候,我又联想到了另外一个经典的面试题。
问:Spring 的 Bean 是否是线程安全的?

我个人认为这两个题的相似程度算是非常高的。
为啥这样说呢?
我们一起分析一波。
首先,我们搞个示意代码:
@Controller
public class TestController {
private int num = 0;
@RequestMapping("/test")
public void test() {
System.out.println(++num);
}
@RequestMapping("/test1")
public void test1() {
System.out.println(++num);
}
}
TestController 就是在 Spring 中托管的一个 Bean。
这个程序跑起来,我们先访问 test,得到的答案是 1,然后再访问 test1,得到的答案是 2。
按照常规的理解,两个不同的请求,它们之间应该相互独立才对。
现在的现象是第二个线程的运行结果,受到了前一个线程的影响。
但是,你能说这是线程不安全的吗?

不能。
只能说因为 Bean 里面包含了可变的成员变量 num,有 ++num 这种代码,所以多个线程并发修改时会导致数据不一致,这里需要我们在开发的时候自行通过加锁或者使用原子类来保证同步。
而这个 Bean,我们一般叫它有状态的 Bean。
对应的,如果 Bean 里面没有成员变量,或者所有的变量都是只读的,那我们就能说它是线程安全的......吗?
先把这个问题按下不表,我们先看看 Bean 的作用域。
还是上面这个例子,如果我在 TestController 类上加一个注解 @Scope("prototype")。
其他都不变,程序跑起来,我们先访问 test,得到的答案是 1,然后再访问 test1,得到的答案还是 1。
这样看起来就是线程安全的了。
@Scope("prototype"),翻译过来是说这个 Bean 是原型作用域的 Bean。
其特性是每次请求 Bean 时,Spring 都会创建一个新实例。
由于每个线程操作独立的 Bean 实例,所以天然线程安全。
而我们在没加 @Scope("prototype") 之前,Bean 的默认作用域是 Singleton,即单例。
其特性是整个 Spring 容器中仅有这一个实例,所有的线程都共享此实例。
所以,由于共享,才出现了前面的线程不安全现象。
再看看我们刚刚按下不表的问题:如果 Bean 里面没有成员变量,或者所有的变量都是只读的,那我们就能说它是线程安全的......吗?
是的,它就是线程安全的。
不管作用域是 prototype 还是 Singleton。
我这样写,也只是为了模拟面试的时候,面试官故意通过反问的方式,来检验你对于知识点的掌握程度。
好,现在回到最开始的这个问题上:Spring 的 Bean 是否是线程安全的?
经过前面的分析,我们知道这个问题确实也是有点陷阱的,不能直接回答是或者不是。
和“CHM 需要将其声明为 volatile”一样,需要面试者结合自己的理解,去分析出不同的场景,得到下面的回答。
首先,Spring 本身并不自动保证 Bean 的线程安全。
在 Spring 框架中,Bean 的线程安全性取决于其作用域和具体实现方式。
当作用域是 Singleton 是,如果 Bean 是有状态的 Bean,即 Bean 中包含可变的成员变量,那就是线程不安全的,需要开发者执行保证。
如果是无状态的 Bean,则是线程安全的。
而当作用域是 Prototype 时,由于每次请求 Bean 时,Spring 都会创建一个新实例。所以每个线程操作的 Bean 实例都是独立的,天然线程安全。
那你可能在想,Spring 的 Bean 我天天都在用,也一直用的是默认的 Singleton 模式,为什么我用的时候没遇到过线程安全的问题呢?
那你就去仔细想想,翻一翻代码,我们用的绝大部分 Bean 是不是都是无状态的设计?
如果,你盘出来发现在项目中有几个有状态的 Bean,访问对应的变量时,你也没有做相应的加锁之类的处理,那恭喜你,找到一个潜伏工作做的不错的 BUG。
结合一下
好,如果前面写的你都理解到了,那现在我们把前面两个题结合一下。
比如我给你这样一份代码:
@RestController
public class TestController {
private ConcurrentHashMap chm = new ConcurrentHashMap();
@RequestMapping("/test")
public void test() {
chm.put("1", "1");
}
@RequestMapping("/test1")
public void test1() {
chm.put("2", "2");
}
}
你说一说它的问题是什么?
或者你说说它有没有出现线程安全问题的风险?
如果回答不出来说明你看的时候根本就没用心去理解,只是在用眼睛看,没有往脑子里面记。
说明你正在看文章的此刻,不是学习的时候。你就先放到收藏夹里面,退出去得了,等有时间的时候再慢慢看。

首先,由于我们使用的是 CHM,所以即使 1000 个线程同时调用 test 方法,最终结果也正确。
这个方法的线程安全性是由 CHM 来保证的。
其次,由于 TestController 是单例的,所有的请求共享同一个 TestController 实例,因此共享同一个 CHM。
但是这个 CHM 没有被 volatile 修饰。
如果以后新增代码,逻辑中修改了 chm 的引用,比如这样:

在 test2 方法中对 chm 进行了重新赋值的操作,因为没有使用 volatile 修饰 chm,所以可能导致其他线程看到的不是最新的 chm。
这就是风险点。
而这个风险点的化解方式之一是给 chm 加上 volatile。
化解方式之二是给 chm 加上 final,确保不能对 chm 进行重新赋值。
化解方式之三是把 Bean 的作用域修改为 prototype,让每个请求操作的 Bean 实例都是独立的。
具体采用哪个方案,就得结合你的应用场景来看了。
再延伸一下
关于 CHM,再做一个小小的延伸,是我多年前看到的一段源码了,印象深刻,和文章内容也比较匹配,分享一下。
Spring 的 SimpleAliasRegistry 类中有一个 CHM 类型的 aliasMap 变量。
但是在操作这个变量之前都是用 synchronized 把 aliasMap 锁住了:
、
请问,为什么我们操作 ConcurrentHashMap 的时候还要加锁呢?

看一下 registerAlias 方法中的这部分代码:

aliasMap 的 get 和 put 方法都是线程安全的,但是先 get,再检查是否存在,然后再 put,这几步操作组合在一起的时候,其他的线程能在 get 和 put 之间插入数据。
这个类是个别名管理器,具体来说就是可能导致重复别名。
即使你使用了 CHM,它也只能保证自身方法的原子性,无法保证外部复合操作的原子性。
因此,在这个场景下,用 synchronized 包裹住了整个复合操作。
如果你觉得不太好理解的话我再举一个 Redis 的例子。
Redis 的 get、set 方法都是线程安全的吧。
但是你如果先 get 再 set,那么在多线程的情况下还是会因为操作非原子性导致竞态条件,比如下面这种:
value = redis.get("counter") # 步骤1
value += 1
redis.set("counter", value)
两个线程可能同时执行步骤 1,读到相同的 value,导致最终结果少加 1。
因为这两个操作不是原子性的。所以 incr 就应运而生了。
我举这个例子的是想说线程安全与否不是绝对的,要看场景。给你一个线程安全的容器,你使用不当还是会有线程安全的问题。
再比如,HashMap 一定是线程不安全的吗?
朋友,说不能说的这么死吧?
它是一个线程不安全的容器。
但是如果我的使用场景是只读呢?
在这个只读的场景下,它就是线程安全的。
总之,看场景,不要脱离场景讨论问题。
道理,就是这么一个道理。
最后,记住歪师傅下面说的这句话,面试的时候可能用的上:
线程安全问题是一个全局问题,不能试图依赖单个组件的特性来解决这个全局的问题。
好,行文至此,暂驻笔锋。
诸君,可以鼓掌了。

最后,欢迎关注公众号"why技术",全网首发平台,还有技术之外的东西哦。
一个奇形怪状的面试题:Bean中的CHM要不要加volatile?的更多相关文章
- 面试题--JAVA中静态块、静态变量加载顺序
最后给大家一道面试题练练手,要求写出其结果(笔试) public class StaticTest { public static int k = 0; public static StaticTes ...
- (转)面试题--JAVA中静态块、静态变量加载顺序详解
public class Test { //1.第一步,准备加载类 public static void main(String[] args) { new Test(); //4.第四步,new一个 ...
- spring中如何向一个单例bean中注入非单例bean
看到这个题目相信很多小伙伴都是懵懵的,平时我们的做法大都是下面的操作 @Component public class People{ @Autowired private Man man; } 这里如 ...
- Spring boot 将配置文件属性注入到一个bean中
现在要做的就是将如下配置文件中的内容注入到一个bean 名为Properties中. Redis.properties配置文件中的内容如下: Properties java bean中代码如下,注意注 ...
- 记录Spring Boot大坑一个,在bean中如果有@Test单元测试,不会注入成功
记录Spring Boot大坑一个,在bean中如果有@Test单元测试,不会注入成功 记录Spring Boot大坑一个,在bean中如果有@Test单元测试,不会注入成功 记录Spring Boo ...
- bean中集合属性的配置
在实际的开发中,有的bean中会有集合属性,如下: package com.sevenhu.domain; import java.util.List; /** * Created by hu on ...
- 记录使用Hibernate查询bean中字段和数据库列类型不匹配问题
今天在工程中遇到Hibernate查询的时候,bean中的字段和数据库中的字段不符合(bean中有pageTime字段,但是数据库中没有此列)报错问题. 具体问题环境: 在auto_off表中,off ...
- 【转】 一个fork的面试题
转自:一个fork的面试题 前两天有人问了个关于Unix的fork()系统调用的面试题,这个题正好是我大约十年前找工作时某公司问我的一个题,我觉得比较有趣,写篇文章与大家分享一下.这个题是这样的: 题 ...
- Spring bean中的properties元素内的name 和 ref都代表什么意思啊?
<bean id="userAction" class="com.neusoft.gmsbs.gms.user.action.UserAction" sc ...
- 面试题解:输入一个数A,找到大于A的一个最小数B,且B中不存在连续相等的两个数字
玄魂工作室秘书 [玄魂工作室] 昨天发的算法有一处情况没考虑到,比如加一后有进位,导致又出现重复数字的情况,修正后今天重新发一次. 比如输入99,那B应该是101 因为100有两个连 ...
随机推荐
- Java基础 —— 泛型
泛型 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型. 理解 为了可以进一步理解泛型,我们先来看一个问题 需求: 编写一个程序,在ArrayList中添加三个对象,类 ...
- st包无效
本机正常安装了 oracle11g 和 ArcSDE10, 想要查询某个空间图层表的shape字段值,所以写了如下sql语句在PL/SQL里执行,select sde.st_astext(shape ...
- Electron 通信
1.web向主进程发送消息 (单项) 使用ipcMain.on 监听事件 const hanle = (event, data) => { console.log(event) console. ...
- Linux 网络设置及管理
Linux 网络管理 网络管理 1.使用NetworkManager管理网络 NetworkManager(网络管理器)是一个动态网络的控制器与配置系统,它用于当网络设备可用时保持设备连接和开启并激活 ...
- Qt/C++地址转坐标/坐标转地址/逆地址解析/支持百度高德腾讯和天地图
一.前言说明 地址和经纬度坐标转换的功能必须在线使用,一般用在导航需求上,比如用户输入起点地址和终点地址,查询路线后,显示对应的路线,而实际上各大地图厂家默认支持的是给定经纬度坐标来查询(百度地图支持 ...
- Qt/C++音视频开发81-采集本地麦克风/本地摄像头带麦克风/桌面采集和麦克风/本地设备和桌面推流
一.前言 随着直播的兴起,采集本地摄像头和麦克风进行直播推流,也是一个刚需,最简单的做法是直接用ffmpeg命令行采集并推流,这种方式简单粗暴,但是不能实时预览画面,而且不方便加上一些特殊要求.之前就 ...
- Qt开发经验小技巧171-175
在Qt编程中经常会遇到编码的问题,由于跨平台的考虑兼容各种系统,而windows系统默认是gbk或者gb2312编码,当然后期可能msvc编译器都支持utf8编码,所以在部分程序中传入中文目录文件名称 ...
- Qt音视频开发38-USB摄像头解码linux方案
一.前言 做嵌入式linux上的开发很多年了,扳手指头算算,也起码9年了,陆陆续续做过很过诸如需要读取外接的USB摄像头或者CMOS摄像机的程序,实时采集视频,将图像传到前端,或者对图像进行人脸分析处 ...
- 11.14javaweb学习
- IM通讯协议专题学习(八):金蝶随手记团队的Protobuf应用实践(原理篇)
本文由金蝶随手记技术团队丁同舟分享. 1.引言 跟移动端IM中追求数据传输效率.网络流量消耗等需求一样,随手记客户端与服务端交互的过程中,对部分数据的传输大小和效率也有较高的要求,普通的数据格式如 J ...