每日三道面试题,通往自由的道路13——锁+Volatile
茫茫人海千千万万,感谢这一秒你看到这里。希望我的面试题系列能对你的有所帮助!共勉!
愿你在未来的日子,保持热爱,奔赴山海!
每日三道面试题,成就更好自我
我们既然聊到了并发多线程的问题,怎么能少得了锁呢?
1. 你知道volatile是如何保证可见性吗?
我们先看一组代码:
public class VolatileVisibleDemo {
public static boolean initFlag = false;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("等待initFlag改变!!!");
// 如果initFlag发生改变了,这是为true的话,才会结束循环
while(!initFlag) {
}
System.out.println("今天的世界打烊了,晚安!");
}
}).start();
// 这里是为了能保证运行完上面的代码
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 这里是Lambda表达式,就是上面的缩写
new Thread(() -> {
System.out.println("准备填充数据,修改initFlag的值");
initFlag = true;
System.out.println("准备数据完了!");
}).start();
}
}
运行得到的结果:

我们可以发现,其实在准备数据完后,我们的initFlag的变量其实已经改变,但是为什么还是没有结束循环输出今天的世界打烊了,晚安!这一句呢?
从之间的JMM模型,我们可以知道,不同线程之间是不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成,并且线程在修改完数值后,也不是马上同步到主内存中,并且另一个线程也是无法感知到数据发生改变的,所以就会有可见性问题。
那我们可以加个volatile关键字修饰变量试下?
public static volatile boolean initFlag = false;
我们可以发现:

在我们的变量修饰了volatile关键字后,就能输出今天的世界打烊了,晚安!这一句了。
我们来看看图解吧:

先解释下这其中连接的几个单词:
- read(读取):从主内存中读取数据
- load (载入):将主内存中读取到的数据写入到本地(工作)内存中
- user(使用):从本地内存中读取数据给线程使用来计算
- assign(赋值):线程将计算好的值重新赋值到工作内存中
- store(存储):将本地内存的数据存储到主内存中
- write(写入):将stroe过来的变量值赋值给主内存中的变量,重新赋值。
大概讲一下流程:
在线程B读取initFlag变量后,重新赋值true给变量,此时,因为加了volatile修饰,所以会马上将值写入到主内存中修改变量中的值,此时因为有一个cpu总线嗅探机制会监听到主内存的变量值发生改变了,会把本地内存的中initFlag变量设置了失效,重新读取一边主内存的新值,就可以达到解决变量可见性问题。这是它第一个保证可见性的关键。
之前我们也有提到他如果发生指令重排序了,那是不是也不能读取到最新的值呢。答案是不会的呢。
因为被volatile修饰的话,它会禁止指令重排序。那它主要是依靠什么指令重排序呢?它是通过内存屏障来实现的。什么是内存屏障?硬件层面,内存屏障分两种:读屏障(Load Barrier)和写屏障(Store Barrier)。内存屏障有两个作用:
- 阻止屏障两侧的指令重排序;
- 强制把写缓冲区/高速缓存中的脏数据等写回主内存,或者让缓存中相应的数据失效。
而编译器在生成字节码时,会在指令序列中插入内存屏障来禁止指令重排序。这样保证了任何程序中都能得到正确的volatile内存语义。这个策略是:
- 在每个volatile写操作前插入一个StoreStore屏障;
- 在每个volatile写操作后插入一个StoreLoad屏障;
- 在每个volatile读操作后插入一个LoadLoad屏障;
- 在每个volatile读操作后再插入一个LoadStore屏障。
看一下示意图:

总结:
volatile作用:
- volatile可以保证内存可见性且禁止重排序。
- volatile不具备保证原子性,而锁可以保证整个临界区代码的执行具有原子性。所以而锁可以保证整个临界区代码的执行具有原子性。所以在功能上,锁比volatile更强大;在性能上,volatile更有优势。
不错呀!volatile这么深的底层都有了解,看来你势要我这个offer呀,那咱们继续
2. 悲观锁和乐观锁可以讲下你的理解吗?
悲观锁和乐观锁都是比较老生常谈的了,所以还是得记住呀!
其实听名字,我们就应该有个概念:
悲观对应着我们生活中的人,悲观的人一般看待事物都会相对消极负能量点,会尽可能往坏处去想的。这也是对应着MyGirl,她其实是一个也不能说算是悲观的人,只能说看待事物可能会更往深入,更坏的一方面的去思考。
这其实跟我很互补,因为算是个乐天派吧,而乐观对应着我们生活中的人,乐观的人一般看待事物都会相对积极正能量,会尽可能往好处去想的。我其实对待生活的方方面面可能会更乐观点,但有时带来的一些坏处也是难以估计的。
所以说这两者不能说谁好谁坏,只能对应着场景选择对应的方法。
悲观锁:
MyGilr这个人呢,她总是会假设一种最坏的情况。比如,她每次要去拿数据的同时,认为别人也会来修改数据跟她作对,所以每次在拿数据的时候她都会上锁,堵上一个界限,这样别人想拿这个数据就只能等待她出去解锁成功后,直到它拿到锁。
在Java中,synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。而在数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁:
我这个人呢,总是会假设一种最好的情况。比如, 我每次要去拿数据的同时,认为别人绝对不会来修改数据滴,所以每次拿数据的时候都不会上锁。但是人还是要点防备心里的,不是吗?所以在更新的时候会判断一下在此期间别人有没有去更新过这个数据。
而常见的有CAS算法+版本号实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
在Java中,像原子类就是使用了乐观锁的一种实现方式CAS实现的。而在数据库提供的类似于write_condition机制,其实都是提供的乐观锁。
两者对应的场景的区别:
乐观锁多用于读多写少的环境,避免频繁加锁影响性能,加大了系统的整个吞吐量;而悲观锁多用于写多读少的环境,避免频繁失败和重试影响性能。
不错,这个常规的锁也懂嘛,最后问你一道:
3. 你还知道什么其他的锁吗?
可重入锁和非可重入锁:
所谓重入锁又名递归锁,顾名思义。就是支持重新进入的锁,也就是说这个锁支持一个线程对资源重复加锁。指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。不会因为之前已经获取过还没释放而阻塞。
在Java中,ReentrantLock和synchronized都是可重入锁,可重入锁的还有一个优点是可一定程度避免死锁。
public static void main(String[] args) {
doOne();
}
public static synchronized void doOne(){
System.out.println("执行第一个任务");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 执行第二个任务
doTwo();
}
public static synchronized void doTwo(){
System.out.println("执行第二个任务");
}
简单的测试下结果:
执行第一个任务
执行第二个任务
可以验证得到,类中的两个方法都是被内置锁synchronized修饰的,而在doOne方法去调用doTwo方法时,因为是可重入锁,所以同个线程下可以直接获得当前对象锁,所以synchronized是可重入锁。
而如果我们自己在继承AQS实现同步器的时候,没有考虑到占有锁的线程再次获取锁的场景,可能就会导致线程阻塞,那这个就是一个非可重入锁。
公平锁和非公平锁 :
这里的公平,可以按生活上来讲,如果你跟你女朋友吵架,你觉得你是正确的,最后的结果却你必须得哄你女朋友还得道歉,你信吗?所以这是公平的吗?
如果对一个锁来说,先对锁获取请求的线程一定会先被满足,后对锁获取请求的线程后被满足,那这个锁就是公平的。反之,那就是不公平的。
公平锁:
多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。
缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁:
多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
在Java中,对于ReentrantLock而言,可以通过构造函数指定该锁是否是公平锁,默认是非公平锁。
独享锁和共享锁:
对于独享和共享,这两个概念应该可以见名知意,对于MyGirl喜欢的东西,是碰都碰不得,而对于不喜欢,或者还可以的东西,可以和她共享。
独享锁:
也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程B对变量A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得独享锁的线程即能读数据又能修改数据。
在Java中,synchronized就是一种独享锁。
共享锁:
代表该锁可被多个线程所持有。如果线程B对变量A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
小伙子不错嘛!今天就到这里,期待你明天的到来,希望能让我继续保持惊喜!
注: 如果文章有任何错误和建议,请各位大佬尽情留言!如果这篇文章对你也有所帮助,希望可爱亲切的您给个三连关注下,非常感谢啦!也可以微信搜索太子爷哪吒公众号私聊我,感谢各位大佬!

每日三道面试题,通往自由的道路13——锁+Volatile的更多相关文章
- 每日三道面试题,通往自由的道路10——JMM篇
茫茫人海千千万万,感谢这一秒你看到这里.希望我的面试题系列能对你的有所帮助!共勉! 愿你在未来的日子,保持热爱,奔赴山海! 每日三道面试题,成就更好自我 今天我们还是继续聊聊多线程的一些其他话题吧! ...
- 每日三道面试题,通往自由的道路14——MySQL
茫茫人海千千万万,感谢这一秒你看到这里.希望我的面试题系列能对你的有所帮助!共勉! 愿你在未来的日子,保持热爱,奔赴山海! 每日三道面试题,成就更好自我 昨天我们是不是聊到了锁,而你提到了MySQL? ...
- 每日三道面试题,通往自由的道路6——JVM
茫茫人海千千万万,感谢这一秒你看到这里.希望我的面试题系列能对你的有所帮助!共勉! 愿你在未来的日子,保持热爱,奔赴山海! 每日三道面试题,成就更好自我 今天我们继续聊聊JVM的话题吧! 1. 那你知 ...
- 每日三道面试题,通往自由的道路5——JVM
茫茫人海千千万万,感谢这一秒你看到这里.希望我的面试题系列能对你的有所帮助!共勉! 愿你在未来的日子,保持热爱,奔赴山海! 每日三道面试题,成就更好自我 昨天既然我们聊到了JVM,那我们继续这一个话题 ...
- 每日三道面试题,通往自由的道路4——JVM篇
茫茫人海千千万万,感谢这一秒你看到这里.希望我的面试题系列能对你的有所帮助!共勉! 愿你在未来的日子,保持热爱,奔赴山海! 每日三道面试题,成就更好自我 昨天既然你有讲到字符串常量池是吧,那这样吧 1 ...
- 每日微软面试题——day 6(打印所有对称子串)
每日微软面试题——day 6(打印所有对称子串) 分类: 2.数据结构与算法2011-08-14 14:27 9595人阅读 评论(15) 收藏 举报 面试微软string测试systemdistan ...
- 三道JS试题(遍历、创建对象、URL解析)
最近在网上看到了三道不错的JS试题,还是很基础(一直认为学好前端基本功很重要...),现在记录如下: 原帖地址:http://www.w3cfuns.com/forum.php?mod=viewthr ...
- Java消息队列三道面试题详解!
面试题 为什么使用消息队列? 消息队列有什么优点和缺点? Kafka.ActiveMQ.RabbitMQ.RocketMQ 都有什么区别,以及适合哪些场景? 面试官心理分析 其实面试官主要是想看看: ...
- Java面试题-Java中的锁
1. 如何实现乐观锁(CAS)?如何避免ABA问题? 答:1)读取内存值的方式实现了乐观锁(比如:SVN系统),方法:第一,比较内存值和期望值:第二,替换内存值为要替换值. 2)带参数版 ...
随机推荐
- python类属性和实例属性的访问
- 【Azure 事件中心】azure-spring-cloud-stream-binder-eventhubs客户端组件问题, 实践消息非顺序可达
问题描述 查阅了Azure的官方文档( 将事件发送到特定分区: https://docs.azure.cn/zh-cn/event-hubs/event-hubs-availability-and-c ...
- Swagger的学习
Swagger 号称世界上最流行的Api框架; RestFul Api文档在线自动生成工具=>Api文档与API定义同步更新 直接运行,可以在线测试API接口 支持多种语言:(Java,Php. ...
- Guava Cache,Java本地内存缓存使用实践
Guava Cache,网上介绍很多,我就不赘述了. 分享一篇好的文章: Guava Cache内存缓存使用实践-定时异步刷新及简单抽象封装 Google Guava 3-缓存 在原作者基础上,我做了 ...
- 如何不做登录请求而获取cookie到Jmeter里
如何不做登录请求而获取cookie到Jmeter里? 登录被测系统后,按F12,找到如下位置,将这个表格所有信息都复制到Jmeter的HTTP Cookie管理器元件,这样就可以不需要登录,能继续发送 ...
- Jmeter- 笔记4 - 参数化 、函数
参数化 调用变量的用法: ${变量名} 参数化第一 二种. 定义变量的两种方法: 配置元件(Config Element) -> 用户定义的变量(User Defined Variables) ...
- SQL SERVER 实现相同记录为空显示(多列去除重复值,相同的只显示一条数据)
sql server语句查询中碰到结果集有重复数据,需要把这个重复数据汇总成一条显示.其余则正常显示. 使用SQL内置函数 ROW_NUMBER() 加 PARTITION 完成 ROW_NUMBER ...
- 稀疏自编码器及TensorFlow实现
自动编码机更像是一个识别网络,只是简单重构了输入.而重点应是在像素级重构图像,施加的唯一约束是隐藏层单元的数量. 有趣的是,像素级重构并不能保证网络将从数据集中学习抽象特征,但是可以通过添加更多的约束 ...
- 如何写新的C++ OP
如何写新的C++ OP 概念简介 简单介绍需要用到基类,详细介绍请参考设计文档. framework::OperatorBase: Operator(简写,Op)基类. framework::OpKe ...
- TensorFlow解析常量、变量和占位符
TensorFlow解析常量.变量和占位符 最基本的 TensorFlow 提供了一个库来定义和执行对张量的各种数学运算.张量,可理解为一个 n 维矩阵,所有类型的数据,包括标量.矢量和矩阵等都是特殊 ...