爱上JUC: 面试常考题大总结(线程安全篇)

一起备战面试吧,也是巩固,不再害怕面试

文章目录

    • 进程和线程区别
    • 并行和并发的区别
    • 创建线程的方式有哪些
    • runnable和callable有什么区别
    • run和start区别
    • 线程包含哪些状态,是如何转换的?
    • 新建t1,t2,t3三个线程,如何保证它们顺序执行
    • sleep和wait方法不同
    • 如何停止一个正在运行的线程
    • 说一说sychronized的原理
    • 重量级锁的执行流程
    • Monitor锁属于重量级锁,你了解过锁升级吗
    • 你谈谈JMM
    • CAS知道吗
    • 悲观锁和乐观锁的区别
    • 什么是AQS
    • AQS与Synchronized的区别
    • AQS工作机制
    • **如果多个线程共同去抢这个资源是如何保证原子性的呢?**
    • **AQS是公平锁吗,还是非公平锁?**
    • ReentrantLock实现原理
    • Sychronized和Lock有什么区别
    • 死锁产生的条件是什么
    • 如何进行死锁诊断
    • 聊一聊ConcurrentHashMap
        • DK1.7中concurrentHashMap
        • JDK1.8中concurrentHashMap
    • 引起并发的原因是什么

进程和线程区别

  1. 进程就是正在运行程序的实例,进程也分为单实例进程比如一款游戏,和多实例进程,比如txt文档,浏览器这些。进程中包含多个线程。
  2. 每个进程都有自己独立的内存空间,在当前进程下,所有线程共享内存空间
  3. 线程更轻量,线程上下文切换成本要比进程高。

并行和并发的区别

  • 并行同一时间动手做多件事的能力,就是多个CPU核心执行各自的线程
  • 并发是同一时间应对多件事的能力,一个cpu核心在多个线程间交替执行。

创建线程的方式有哪些

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
  • 使用线程池创建

runnable和callable有什么区别

  1. Runnable 接口run方法没有返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
  2. Callalbe接口支持返回执行结果,需要调用**FutureTask.get()**得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
  3. Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛

run和start区别

  • start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
  • run(): 封装了要被线程执行的代码,可以被调用多次。

线程包含哪些状态,是如何转换的?

爱上JUC: 面试常考题大总结(线程安全篇)_第1张图片
分别是

  • 新建
    • 当一个线程对象被创建,但还未调用 start 方法时处于新建状态
    • 此时未与操作系统底层线程关联
  • 可运行
    • 调用了 start 方法,就会由新建进入可运行
    • 此时与底层线程关联,由操作系统调度执行
  • 终结
    • 线程内代码已经执行完毕,由可运行进入终结
    • 此时会取消与底层线程关联
  • 阻塞
    • 当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间
    • 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态
  • 等待
    • 当获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间
    • 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态
  • 有时限等待
    • 当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法,此时从可运行状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu 时间
    • 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁
    • 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁
    • 还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自然恢复为可运行状态

新建t1,t2,t3三个线程,如何保证它们顺序执行

  • 可以通过join方法
  • 可以使用wait/notify方法

notifyAll:唤醒所有wait的线程
notify:只随机唤醒一个 wait 线程

sleep和wait方法不同

共同点

  • wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态

不同点

  • 方法归属不同
    • sleep(long) 是 Thread 的静态方法
    • 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
  • 醒来时机不同
    • 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
    • wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
    • 它们都可以被打断唤醒
  • 锁特性不同(重点)
    • wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
    • wait 方法执行后会释放对象锁,允许其它线程获得 该对象锁(我放弃 cpu,但你们还可以用)
    • 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)

如何停止一个正在运行的线程

有三种方式可以停止线程

  • 使用退出标志(while循环,flag为条件),使线程正常退出,也就是当run方法完成后线程终止
  • 使用stop方法强行终止(不推荐,方法已作废)
  • 使用interrupt方法中断线程

说一说sychronized的原理

爱上JUC: 面试常考题大总结(线程安全篇)_第2张图片

  • Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
  • 它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor
  • 在monitor内部有三个属性,分别是owner、entrylist、waitset
  • 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程

重量级锁的执行流程

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
具体的流程:

  • 代码进入synchorized代码块,先让lock(对象锁)关联monitor,然后判断Owner是否有线程持有
  • 如果没有线程持有,则让当前线程持有,表示该线程获取锁成功
  • 如果有线程持有,则让当前线程进入entryList进行阻塞,如果Owner持有的线程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平)
  • 如果代码块中调用了wait()方法,则会进去WaitSet中进行等待

Monitor锁属于重量级锁,你了解过锁升级吗

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有(偏向锁)、不同线程交替持有锁(轻量级锁)、多线程竞争锁(重量级锁)三种情况。

描述
重量级锁 底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
轻量级锁 解决重量级锁的弊端线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
偏向锁 解决轻量级锁在同一个线程需要多次CAS操作的问题,一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令

你谈谈JMM

  • JMM定义了共享模型中多线程程序读写操作的一种规范,通过这些规则来规范对内存的读写操作,从而保证指令的正确性
  • JMM把内存分为2块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
  • 线程和线程直接是隔离的,线程和线程之间的交互需要通过主内存

CAS知道吗

  • CAS的全称是: Compare And Swap(比较再交换);它体现的一种乐观锁的思想在无锁状态下保证线程操作数据的原子性。
  • CAS使用到的地方很多:AQS框架、AtomicXXX类
  • 在操作共享变量的时候使用的自旋锁,效率上更高一些
  • CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现

悲观锁和乐观锁的区别

  • CAS 是基于乐观锁的思想: 最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
  • synchronized 是基于悲观锁的思想: 最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

什么是AQS

全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,它是构建锁或者其他同步组件的基础框架

AQS与Synchronized的区别

synchronized AQS
关键字,c++ 语言实现 java 语言实现
悲观锁,自动释放锁 悲观锁,手动开启和关闭(cancelAcquire()来设置状态为0)
锁竞争激烈都是重量级锁,性能差 锁竞争激烈的情况下,提供了多种解决方案

AQS常见的实现类

  • ReentrantLock 阻塞式锁
  • Semaphore 信号量
  • CountDownLatch 倒计时锁

AQS工作机制

  • 在AQS中维护了一个使用了volatile修饰的state属性来表示资源的状态,0表示无锁,1表示有锁
  • 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

如果多个线程共同去抢这个资源是如何保证原子性的呢?

在去修改state状态的时候**,使用的cas自旋锁来保证原子性**,确保只能有一个线程修改成功,修改失败的线程将会进入FIFO队列(addWaiter(Node.EXCLUSIVE))中等待

AQS是公平锁吗,还是非公平锁?

  • 新的线程与队列中的线程共同来抢资源,是非公平锁
  • 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁

比较典型的AQS实现类ReentrantLock,它默认就是非公平锁,新的线程与队列中的线程共同来抢资源

ReentrantLock实现原理

ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置公平锁
  • 支持多个条件变量
  • 与synchronized一样,都支持重入

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似
爱上JUC: 面试常考题大总结(线程安全篇)_第3张图片

  • 程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
  • 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
  • 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
  • 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁

Sychronized和Lock有什么区别

  • 语法层面
    • synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
    • Lock 是接口,源码由 jdk 提供,用 java 语言实现
    • 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
  • 功能层面
    • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
    • Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
    • Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock
  • 性能层面
    • 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
    • 在竞争激烈时,Lock 的实现通常会提供更好的性能

死锁产生的条件是什么

死锁:一个线程需要同时获取多把锁,这时就容易发生死锁

例如:
t1 线程获得A对象锁,接下来想获取B对象的锁
t2 线程获得B对象锁,接下来想获取A对象的锁

如何进行死锁诊断

当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和 jstack

  • 使用jps查看运行的线程
  • 使用jstack查看线程运行的情况

其他解决工具,可视化工具

  • jconsole

用于对jvm的内存,线程,类 的监控,是一个基于 jmx 的 GUI 性能监控工具
打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 就行

  • VisualVM:故障处理工具

能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈
打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行

聊一聊ConcurrentHashMap

ConcurrentHashMap 是一种线程安全的高效Map集合
底层数据结构:

  • JDK1.7底层采用分段的数组+链表实现
  • JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
DK1.7中concurrentHashMap

爱上JUC: 面试常考题大总结(线程安全篇)_第4张图片

  • 提供了一个segment数组,在初始化ConcurrentHashMap 的时候可以指定数组的长度,默认是16,一旦初始化之后中间不可扩容
  • 在每个segment中都可以挂一个HashEntry数组,数组里面可以存储具体的元素,HashEntry数组是可以扩容的
  • 在HashEntry存储的数组中存储的元素,如果发生冲突,则可以挂单向链表

存储流程
爱上JUC: 面试常考题大总结(线程安全篇)_第5张图片

  • 先去计算key的hash值,然后确定segment数组下标
  • 再通过hash值确定hashEntry数组中的下标存储数据
  • 在进行操作数据的之前,会先判断当前segment对应下标位置是否有线程进行操作,为了线程安全使用的是ReentrantLock进行加锁,如果获取锁是被会使用cas自旋锁进行尝试
JDK1.8中concurrentHashMap

在JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表
采用 CAS + Synchronized来保证并发安全进行实现

  • CAS控制数组节点的添加
  • synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题 , 效率得到提升

爱上JUC: 面试常考题大总结(线程安全篇)_第6张图片

引起并发的原因是什么

Java并发编程三大特性

  • 原子性:

解决方案:
1.synchronized:同步加锁
2.JUC里面的lock:加锁

  • 可见性

解决方案:

  • synchronized
  • volatile(推荐)
  • LOCK
  • 有序性

你可能感兴趣的:(JUC,面试,java)