茫茫人海千千万万,感谢这一秒你看到这里。希望我的面试题系列能对你的有所帮助!共勉!

愿你在未来的日子,保持热爱,奔赴山海!

每日三道面试题,成就更好自我

我们既然聊到了并发多线程的问题,怎么能少得了锁呢?

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)。内存屏障有两个作用:

  1. 阻止屏障两侧的指令重排序;
  2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,或者让缓存中相应的数据失效。

而编译器在生成字节码时,会在指令序列中插入内存屏障来禁止指令重排序。这样保证了任何程序中都能得到正确的volatile内存语义。这个策略是:

  • 在每个volatile写操作前插入一个StoreStore屏障;
  • 在每个volatile写操作后插入一个StoreLoad屏障;
  • 在每个volatile读操作后插入一个LoadLoad屏障;
  • 在每个volatile读操作后再插入一个LoadStore屏障。

看一下示意图:

总结:

volatile作用:

  1. volatile可以保证内存可见性且禁止重排序。
  2. volatile不具备保证原子性,而锁可以保证整个临界区代码的执行具有原子性。所以而锁可以保证整个临界区代码的执行具有原子性。所以在功能上,锁比volatile更强大;在性能上,volatile更有优势。

不错呀!volatile这么深的底层都有了解,看来你势要我这个offer呀,那咱们继续

2. 悲观锁和乐观锁可以讲下你的理解吗?

悲观锁和乐观锁都是比较老生常谈的了,所以还是得记住呀!

其实听名字,我们就应该有个概念:

悲观对应着我们生活中的人,悲观的人一般看待事物都会相对消极负能量点,会尽可能往坏处去想的。这也是对应着MyGirl,她其实是一个也不能说算是悲观的人,只能说看待事物可能会更往深入,更坏的一方面的去思考。

这其实跟我很互补,因为算是个乐天派吧,而乐观对应着我们生活中的人,乐观的人一般看待事物都会相对积极正能量,会尽可能往好处去想的。我其实对待生活的方方面面可能会更乐观点,但有时带来的一些坏处也是难以估计的。

所以说这两者不能说谁好谁坏,只能对应着场景选择对应的方法。

悲观锁:

MyGilr这个人呢,她总是会假设一种最坏的情况。比如,她每次要去拿数据的同时,认为别人也会来修改数据跟她作对,所以每次在拿数据的时候她都会上锁,堵上一个界限,这样别人想拿这个数据就只能等待她出去解锁成功后,直到它拿到锁。

在Java中,synchronizedReentrantLock等独占锁就是悲观锁思想的实现。而在数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁

我这个人呢,总是会假设一种最好的情况。比如, 我每次要去拿数据的同时,认为别人绝对不会来修改数据滴,所以每次拿数据的时候都不会上锁。但是人还是要点防备心里的,不是吗?所以在更新的时候会判断一下在此期间别人有没有去更新过这个数据。

而常见的有CAS算法+版本号实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。

在Java中,像原子类就是使用了乐观锁的一种实现方式CAS实现的。而在数据库提供的类似于write_condition机制,其实都是提供的乐观锁。

两者对应的场景的区别:

乐观锁多用于读多写少的环境,避免频繁加锁影响性能,加大了系统的整个吞吐量;而悲观锁多用于写多读少的环境,避免频繁失败和重试影响性能。

不错,这个常规的锁也懂嘛,最后问你一道:

3. 你还知道什么其他的锁吗?

可重入锁和非可重入锁

所谓重入锁又名递归锁,顾名思义。就是支持重新进入的锁,也就是说这个锁支持一个线程对资源重复加锁。指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。不会因为之前已经获取过还没释放而阻塞。

在Java中,ReentrantLocksynchronized都是可重入锁,可重入锁的还有一个优点是可一定程度避免死锁。

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的更多相关文章

  1. 每日三道面试题,通往自由的道路10——JMM篇

    茫茫人海千千万万,感谢这一秒你看到这里.希望我的面试题系列能对你的有所帮助!共勉! 愿你在未来的日子,保持热爱,奔赴山海! 每日三道面试题,成就更好自我 今天我们还是继续聊聊多线程的一些其他话题吧! ...

  2. 每日三道面试题,通往自由的道路14——MySQL

    茫茫人海千千万万,感谢这一秒你看到这里.希望我的面试题系列能对你的有所帮助!共勉! 愿你在未来的日子,保持热爱,奔赴山海! 每日三道面试题,成就更好自我 昨天我们是不是聊到了锁,而你提到了MySQL? ...

  3. 每日三道面试题,通往自由的道路6——JVM

    茫茫人海千千万万,感谢这一秒你看到这里.希望我的面试题系列能对你的有所帮助!共勉! 愿你在未来的日子,保持热爱,奔赴山海! 每日三道面试题,成就更好自我 今天我们继续聊聊JVM的话题吧! 1. 那你知 ...

  4. 每日三道面试题,通往自由的道路5——JVM

    茫茫人海千千万万,感谢这一秒你看到这里.希望我的面试题系列能对你的有所帮助!共勉! 愿你在未来的日子,保持热爱,奔赴山海! 每日三道面试题,成就更好自我 昨天既然我们聊到了JVM,那我们继续这一个话题 ...

  5. 每日三道面试题,通往自由的道路4——JVM篇

    茫茫人海千千万万,感谢这一秒你看到这里.希望我的面试题系列能对你的有所帮助!共勉! 愿你在未来的日子,保持热爱,奔赴山海! 每日三道面试题,成就更好自我 昨天既然你有讲到字符串常量池是吧,那这样吧 1 ...

  6. 每日微软面试题——day 6(打印所有对称子串)

    每日微软面试题——day 6(打印所有对称子串) 分类: 2.数据结构与算法2011-08-14 14:27 9595人阅读 评论(15) 收藏 举报 面试微软string测试systemdistan ...

  7. 三道JS试题(遍历、创建对象、URL解析)

    最近在网上看到了三道不错的JS试题,还是很基础(一直认为学好前端基本功很重要...),现在记录如下: 原帖地址:http://www.w3cfuns.com/forum.php?mod=viewthr ...

  8. Java消息队列三道面试题详解!

    面试题 为什么使用消息队列? 消息队列有什么优点和缺点? Kafka.ActiveMQ.RabbitMQ.RocketMQ 都有什么区别,以及适合哪些场景? 面试官心理分析 其实面试官主要是想看看: ...

  9. Java面试题-Java中的锁

    1. 如何实现乐观锁(CAS)?如何避免ABA问题? 答:1)读取内存值的方式实现了乐观锁(比如:SVN系统),方法:第一,比较内存值和期望值:第二,替换内存值为要替换值.        2)带参数版 ...

随机推荐

  1. [Django高级之批量插入数据、分页器组件]

    [Django高级之批量插入数据.分页器组件] 批量插入数据 模板层models.py from django.db import models class Books(models.Model): ...

  2. EdgeX Foundry试运行

    EdgeX Foundry试运行 简介 EdgeX Foundry是一个由Linux基金会发起的,且厂商中立的开源IoT边缘计算项目.它可以采集来自多个源的数据,并将这些数据转发到一个中央系统.Edg ...

  3. 限流&熔断的考量

    限流的原则,是尽量在流量源头限,并且是需要依据现有团队所掌握的技能来. 如上最左侧便是主要流量的来源入口,首先就要限制的地方就是slb节点的income流量 slb节点的流量特点是啥?加限流怎么加?限 ...

  4. GO语言的JSON03---JSON文件的序列化与反序列化

    package main import ( "encoding/json" "fmt" "os" ) type Human2 struct ...

  5. C# HTTP请求对外接口、第三方接口公用类

    /// <summary> /// 网络数据请求公共函数 /// </summary> public class HttpWebRequestCommon { #region ...

  6. 目标检测coco数据集点滴介绍

    目标检测coco数据集点滴介绍 1.  COCO数据集介绍 MS COCO 是google 开源的大型数据集, 分为目标检测.分割.关键点检测三大任务, 数据集主要由图片和json 标签文件组成. c ...

  7. java并发编程工具类JUC第七篇:BlockingDeque双端阻塞队列

    在之前的文章中已经为大家介绍了java并发编程的工具:BlockingQueue接口.ArrayBlockingQueue.DelayQueue.LinkedBlockingQueue.Priorit ...

  8. day05对象和类

    day06作业: 第一题:分析以下需求,并用代码实现 手机类Phone 属性: 品牌brand 价格price 行为: 打电话call() 发短信sendMessage() 玩游戏playGame() ...

  9. halcon——缺陷检测常用方法总结(测量拟合)

    引言 机器视觉中缺陷检测分为一下几种: blob分析+特征 模板匹配(定位)+差分:halcon--缺陷检测常用方法总结(模板匹配(定位)+差分) - 唯有自己强大 - 博客园 (cnblogs.co ...

  10. P2782 友好城市(最长不下降子序列)

    题目描述 有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的$N$个城市.北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同.每对友好城市都向政府申请在河上开辟一条直 ...