三道java新手入门面试题,通往自由的道路--锁+Volatile

目录
  • 1. 你知道volatile是如何保证可见性吗?
    • 小结:
  • 2. 悲观锁和乐观锁可以讲下你的理解吗?
    • 3. 你还知道什么其他的锁吗?
  • 总结

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更有优势。

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再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

总结

这篇文章就到这里了,如果这篇文章对你也有所帮助,希望您能多多关注我们的更多内容!

(0)

相关推荐

  • 每日六道java新手入门面试题,通往自由的道路

    目录 1. 面试第一步,自我介绍. 2. 请你简述下对面向对象的理解,面向对象和面向过程的区别? 3. 那你再讲下面向对象的特征有哪些方面 4. 请问:我们是否可以继承 String类? 5. 我再问下final.finally.finalize这三者有什么区别吗? 6. 请讲下String和 StringBuilder.StringBuffer之间的区别? 总结 1. 面试第一步,自我介绍. 这个自我介绍,在整个面试当中可以说是第一步,如果你能把你想说的重点说出来,把面试官带到你准备好的技术点

  • 每日六道java新手入门面试题,通往自由的道路第二天

    目录 1. 你可以讲下你对String类有什么其他的了解吗? 2. == 和 equals 的区别 3. String s= new String("nz")创建了几个字符串对象? 4. 你可以讲下JVM的运行时数据区或者说内存结构吗? 5. 类加载过程 6. 而其中类加载器是什么,那有哪些呢? 总结 1. 你可以讲下你对String类有什么其他的了解吗? 在看String的源码过程中,可以发现String 内部实际存储结构为 char数组,在String中有几个比较重要的构造函数:

  • 三道java新手入门面试题,通往自由的道路--多线程

    目录 1. 你知道线程安全问题吗? 2. 那如何解决线程安全问题呢? 3. 那你讲下死锁是什么吧? 总结 1. 你知道线程安全问题吗? 线程安全问题:一般指在多线程模式下,多个线程对同一个共享数据进行操作时,第一个线程还没来得及更新共享数据,从而导致另外一个线程没得到最新的数据,并更新数据,从而产生线程安全问题.比较常见的场景有买票. 我举个例子吧: 需求:比如买周杰伦演唱会的门票,此时有三个窗口同时卖总共100张票.窗口就是线程对象,而100张票的资源,此时就相当于多个线程去抢占cpu的资源去

  • 每日几道java新手入门面试题,通往自由的道路

    目录 1.请你说一下什么是面向对象? 2.请你简述一下面向对象的三个基本特征? 3.为什么说 Java 是一种半解释半编译的程序设计语言呢? 4.请你说一下Java中的8大基本类型是那些? 5.请你讲讲抽象类和接口有什么区别? 6.请判断当一个对象被当作参数传递给一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递? 7.请你说一下JVM/JRE/JDK的区别? 8.请你说一下方法重载和方法重写的区别? 9.请你说一下List接口和Set接口的区别? 1

  • 每日六道java新手入门面试题,通往自由的道路--线程池

    目录 1. 你知道线程池吗?为什么需要使用线程池? 小结: 2. 你知道有多少种创建线程池的方式 3. 线程池的五种状态你有了解吗? 4. 你知道ThreadPoolExecutor的构造方法和参数吗 5. 你可以说下线程池的执行过程原理吗 6. 能否写一个简单线程池的demo? 总结 1. 你知道线程池吗?为什么需要使用线程池? 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源. 而在Java中, JVM 中每创建和销毁线程就需要资源和时间的损耗了,

  • 三道java新手入门面试题,通往自由的道路--JVM

    目录 1. 你知道JVM内存模型吗? 2. 你知道重排序是什么吗? 3. happens-before是什么,和as-if-serial有什么区别 总结 1. 你知道JVM内存模型吗? 在Java的并发中采用的就是JVM内存共享模型即JMM(Java Memory Model),它其实是是JVM规范中所定义的一种内存模型,跟计算机的CPU缓存内存模型类似,是基于CPU缓存内存模型来建立的,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别. 那我们先来讲下计算机的内存模型: 其实早期计算机

  • 每日六道java新手入门面试题,通往自由的道路--JVM

    目录 1. JVM是如何判断对象是否可回收 2. 你知道有什么垃圾回收的常见算法吗? 3. 你知道有什么垃圾收集器吗? 4. 那你知道什么时候才会触发Full GC 5. JVM中四种引用你有了解过吗? 6. 说说你知道的几种主要的JVM参数 1.堆设置 2.收集器设置 3.并行收集器设置 4.并发收集器设置 5.JVM 调优的参数 总结 1. JVM是如何判断对象是否可回收 垃圾收集器在做垃圾回收的时候,首先需要判断一个对象是存活状态还是死亡状态,死亡的对象将会被标识为垃圾数据并等待收集器进行

  • 每日六道java新手入门面试题,通往自由的道路--多线程

    目录 1. 你可以讲下进程与线程的区别?为什么要用多线程? 2. 什么是上下文切换? 3. 说说你知道的几种创建线程的方式 4. 昨天你讲到创建线程后使用start方法去调用线程,为什么run方法不行呢?有什么区别? 5. 你知道你开启一个线程后,它的状态有那些吗? 6. 既然讲到超时方法,那你讲下sleep和wait的区别和他们需要怎样唤醒 总结: 1. 你可以讲下进程与线程的区别?为什么要用多线程? 进程:进程是程序的一次执行过程,是系统运行程序的基本单位. 线程:单个进程中执行中每个任务就

  • 三道java新手入门面试题,通往自由的道路--锁+Volatile

    目录 1. 你知道volatile是如何保证可见性吗? 小结: 2. 悲观锁和乐观锁可以讲下你的理解吗? 3. 你还知道什么其他的锁吗? 总结 1. 你知道volatile是如何保证可见性吗? 我们先看一组代码: public class VolatileVisibleDemo { public static boolean initFlag = false; public static void main(String[] args) { new Thread(new Runnable() {

  • 三道MySQL新手入门面试题,通往自由的道路

    目录 1. 讲讲你认识MySQL锁吧 2. 你知道什么是事务.四大特性.隔离级别吗? 3. MyISAM 和 InnoDB 存储引擎的区别 总结 1. 讲讲你认识MySQL锁吧 对于数据库来讲,读写都是非常频繁的吧,在并发量来的时候,在进行读写操作时,可能会产生数据的不一致,这时候需要一些机制来保证访问的次序,所以锁就可以在一定限度保护它的一致性. 首先我们可以按锁的粒度分: 1.表级锁:它上锁是锁住的整张表,当下一个事务来访问的时候,必须等到当前事务把锁释放了,才能对表进行操作访问. 特点:表

随机推荐