JUC并发编程常见面试题目

1. 进程和线程,并发和并行

  • 进程:进程是计算机程序在一个数据集上的一次运行过程,也是资源分配和调度的基本单位
  • 线程:线城是进程中的一个执行单元。一个进程中至少有一个线程
  • 并行:指的是两个或多个事件在同一时刻发生,
  • 并发:指的是两个或多个事件在同一时间间隔内发生

2. 守护线程和用户线程的区别

  • 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程
  • 守护 (Daemon) 线程:运行在后台,为用户线程服务。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,如垃圾回收线程

3. 什么是线程上下文切换

  当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

4. 死锁以及产生死锁的四个条件

  • 死锁是指多个进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
  • 产生死锁的必要条件:1. 互斥条件 2. 请求与保持条件 3. 不剥夺条件 4. 循环等待条件

5. 手写一个死锁

参考于:https://blog.csdn.net/qq_39033181/article/details/116034505

6. 什么是Callable和Future?Callable与Runnable接口的区别?

  • Callable接口类似于Runnable,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。可以认为是带有回调的Runnable。
  • Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。
  • 区别:
    1. Callable接口使用call方法,Runnable接口使用run方法;
    2. Callable接口有返回值和可以抛出异常;而Runnable接口没有返回值和不能抛出异常。

7. Java中线程的状态(操作系统中是五种)

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  6. 终止(TERMINATED):表示该线程已经执行完毕

8. sleep() 和wait(),sleep()和yield()有什么区别?

  • sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。

  • sleep() 不释放锁;wait() 释放锁。

  • sleep() 方法执行完成后,线程会自动苏醒;wait() 方法被调用后,线程不会自动苏醒,与notify()搭配使用。

  • 首先yield () 也是 Thread线程类的静态方法,也是不释放锁。

  • 线程执行 sleep()方法后转入阻塞(blocked)状态;而执行 yield()方法后转入就绪(ready)状态,因此接下来的获得执行权的线程可能是当前线程,也可能是其他线程。

  • sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;

9. notify()和notityAll()有什么区别?

  • notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
  • notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

10. volatile关键字理解

  • 保证内存可见性:当某个线程在自己的工作内存中将主内存中共享数据的副本修改并刷新到主内存后,其它线程能够立即感知到该共享数据发生变化
  • 不保证原子性:不保证原子性正是volatile轻量级的体现,多个线程对volatile修饰的变量进行操作时,会出现容易出现写覆盖的情况(i++)
  • 禁止指令重排序
  • 使用场景:高并发下的单例模式;原子类,比如AtomicInteger、AtomicReference中
  • java内存模型(JMM)的三大特性:可见性、原子性、有序性。

11. volatile可见性原理

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1、它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;
2、它会强制将对缓存的修改操作立即写入主存;
3、如果是写操作,它会导致其他CPU中对应的缓存行无效。
4、所以可见性如下:
volatile的功能就是被修饰的变量在被修改后可以立即同步到主内存,被修饰的变量在每次用之前都从主内存刷新。本质也是通过内存屏障来实现可见性。

写内存屏障(Store Memory Barrier)可以促使处理器将当前store buffer(存储缓存)的值写回主存。读内存屏障(Load Memory Barrier)可以促使处理器处理invalidate queue(失效队列)。进而避免由于StoreBuffer和Invalidate Queue的非实时性带来的问题。

12. 为什么使用CAS代替synchronized

  • CAS算法:当且仅当对象偏移量上的值和预期值相等时,才会用更新值,否则不执行更新。但是无论是否更新了内存上的值,最终都会返回内存上的旧值。CAS是一条CPU原语,执行是连续的,因此保证了原子性。
  • 原因:synchronized加锁,同一时间段只允许一个线程访问,能够保证一致性但是并发性下降。而是用CAS算法使用do-while不断判断而没有加锁(实际是一个自旋锁),保证一致性和并发性。

13. CAS的缺点以及怎样解决ABA问题

  • 缺点
    1) 如果CAS失败就会一直进行尝试,即一直在自旋,导致CPU开销。
    2) 只能一个共享变量的原子操作
    3) 会产生ABA问题。
  • ABA问题
      如果一个线程在初次读取时的值为A,并且在准备赋值的时候检查该值仍然是A,但是可能在这两次操作之间,有另外一个线程现将变量的值改成了B,然后又将该值改回为A,那么CAS会误认为该变量没有变化过。
  • 解决:使用时间戳的原子引用

14.ArrayList、HashSet、HashMap并发修改异常的解决

  • ArrayList的解决
  1. 利用vector解决,vector是安全的
  2. 使用Collections.synchronizedList(new ArrayList<>())将ArrayList变为安全的
  3. 使用写时复制,CopyOnWriteArrayList
    CopyOnWriteArrayList:写时复制是一种读写分离的思想,在并发读的时候不需要加锁,因为它能够保证并发读的情况下不会添加任何元素。而在并发写的情况下,需要先加锁,但是并不直接对当前容器进行写操作。而是先将当前容器进行复制获取一个新的容器,进行完并发写操作之后,当之前指向原容器的引用更改指向当前新容器。
  • HahSet的解决
  1. 使用Collections.synchronizedSet(new HashSet<>())将hashset变为安全的
  2. 使用写时复制,CopyOnWriteArraySet内部还是使用CopyOnWriteArrayList实现
  • HashMap的解决
  1. 使用HashTable解决
  2. 使用Collections.synchronizedMap(new HashMap<>())将HashMap变为安全的
  3. ConcurrentHashMap类解决,关于ConcurrentHashMap,可参考:

https://blog.csdn.net/qq_39033181/article/details/115859448 ,第九题

15. java多线程锁

  • 公平锁:多个线程按照申请锁的顺序来获取锁,先来后到在等待队列中FIFO
  • 非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的比先申请的有先获取,在高并发的情况下,有可能造成优先级翻转或饥饿现象
  • 乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。基本通过CAS实现
  • 悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁
  • 可重入锁(递归锁):同一线程在外层函数获取锁以后,进入内层函数时自动获取锁.其作用是避免死锁。
  • 自旋锁:获取不到锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,好处是减少上下文切换的消耗,坏处是消耗CPU
  • 独占锁:每次只允许一个线程获取锁,如 synchronized和ReentrantLock
  • 共享锁:允许多个线程同时获取锁,如读写锁(ReadWriteLock)
  • 读写锁:读读共享 读写-写读-写写是独占

16. CountDownLatch、CyclicBarrier、Semaphore的作用

  • CountDownLatch,当一个或多个线程调用await方法时,这些线程会阻塞。其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
  • CyclicBarrier,让一组线程到达一个屏障时,通过CyclicBarrier的await()方法阻塞,直到最后一个线程到达时一起往下运行。
  • Semaphore,用于多个并发线程对多个共享资源的互斥使用
    acquire()对信号量减1;release()对信号量加1;

具体可参考:https://blog.csdn.net/qq_20492405/article/details/103595186 五.5.5-5.7

17. 阻塞队列定义以及种类

  • 当队列是空的,从队列中获取元素的操作将会被阻塞当队列是满的,从队列中添加元素的操作将会被阻塞
  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
  • DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
  • SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
  • LinkedTransferQueue:由链表组成的无界阻塞队列。
  • LinkedBlockingDeque:由链表组成的双向阻塞队列。

18. 手写一个阻塞队列

参考于:https://blog.csdn.net/qq_39033181/article/details/116087046

19. Executor和Executors的区别

  • Executor是一个接口,只有一个execute()方法,用来执行线程需要做的事情。
  • Executors是一个工具类,可以按照不同需求创建不同的线程池。创建不同的线程池,底层是调用ThreadPoolExecutor类传入不同的参数,而ThreadPoolExecutor类的继承关系如下:
    JUC并发编程常见面试题目_第1张图片

20. 使用线程池的好处

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
第三:提高线程的可管理性。

21. 使用线程池的三种常见方式:

Executors.newFixedThreadPool(int):创建一个固定线程数量的线程池。
Executors.newSingleThreadExecutor():创建一个单线程的线程池。
Executors.newCachedThreadPool():创建一个可缓存的线程池,如果线程长度超过处理需要,可灵活回收空闲线程,若无可回收线程,则创建新线程

22. 线程池的7大参数

  • corePoolSize:线程池中常驻核心线程池
  • maximumPoolSize:线程池中能够容纳同时执行最大线程数,该值必须大于等于1
  • keepAliveTime:多余线程的最大存活时间
  • unit:keepAliveTime的单位
  • workQueue:阻塞队列,被提交但尚未被执行的任务
  • threadFactory:生成线程池中工作线程的线程工厂,一般使用默认即可
  • handler:拒绝策略,表示当任务队列满并且工作线程大于等于线程池的最大线程数时,对即将到来的线程的拒绝策略

23. 线程池具体工作流程:

添加一个请求任务时,线程池会做出以下判断:

  • 如果正在运行的线程数量小于corePoolSize,会立刻创建线程运行该任务
  • 如果正在运行的线程数量大于等于corePoolSize,会将该任务放入阻塞队列中
  • 如果队列也满但是正在运行的线程数量小于maximumPoolSize,线程池会进行拓展,将线程池中的线程数拓展到最大线程数
  • 如果队列满并且运行的线程数量大于等于maximumPoolSize,那么线程池会启动相应的拒绝策略来拒绝相应的任务请求
  • 当一个线程完成任务时,它会从队列中取下一个任务来执行
  • 当一个线程空闲时间超过给定的keepAliveTime时,线程会做出判断:如果当前运行线程大于corePoolSize,那么该线程将会被停止。最终的线程数目会收缩到corePoolSize的大小

24. 线程池的拒绝策略:

  • AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
  • CallerRunsPolicy:调用者运行的一种机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者
  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中尝试再次提交当前任务
  • DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果任务允许丢失,那么该策略是最好的方案

25. 简述synchronized的底层实现原理

代码块同步原理:Synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成

  • monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
    • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
    • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
    • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
  • monitorexit:指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

    monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异常时,锁也可以得到释放,避免死锁;

方法的同步原理:方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成,不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。

  • 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志,该标记表明线程进入该方法时,需要monitorenter,退出该方法时需要monitorexit

26. 理解Java对象头与Monitor

  对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针),Mark Word的最后两位存储了锁的标志位,01是初始状态,未加锁,其对象头里存储的是对象本身的哈希码,随着锁级别的不同,对象头里会存储不同的内容。通常说Synchronized的对象锁,Mark Word锁标识位为10,其中指针指向的是Monitor对象的起始地址。

27. synchronized锁的优化

  锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

  • 无锁;
  • 偏向锁:在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。偏向锁是在单线程执行代码块时使用的机制
  • 轻量级锁:轻量级锁所适应的场景是线程交替执行同步块的场合,是没有多线程竞争的。底层是CAS。
  • 重量级锁:如果存在多线程竞争情况下,轻量级锁会膨胀为重量级锁。

  Synchronized是通过对象的管程(Monitor)来实现的。但是管程又是依赖于底层的操作系统的信号量lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间。因此,这种依赖于操作系统信号量lock所实现的锁我们称之为 “重量级锁”

28. synchronized和ReentrantLock的区别

  • synchronized是关键字,属于jvm层面的;ReentrantLock是类,是api层面。
  • synchronized不需要用户手动释放锁,执行完会系统会自动释放对锁的占用
    ReentrantLock需要用户手动释放锁。否则容易出现死锁现象
  • synchronized不可以中断,除非抛出异常或者正常完成;ReentrantLock可以被中断
  • synchronized是非公平锁
    ReentrantLock默认是非公平锁,但是构造方法可以传入boolean值,true为公平,false为非公平
  • synchronized不可以绑定Condition
    ReentrantLock可以绑定多个Condition,实现精确唤醒

29. LockSupport的理解

  • LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程
  • LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0.调用一次unpark就加1变成1,调用一次park会消费permit,也就是将1变成0。如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证。
  • LockSupport的好处:传统的synchronized和lock的线程等待唤醒机制有两点约束,第一是线程必须先要获得锁(wait/await/notify/signal必须在synchronized或lock锁块中);第二是必须先等待(wait/await)后唤醒(notify/signal),线程才能被唤醒。而LockSpport不需要以上两点要求。

30. AQS与ReentrantLock底层实现原理

具体可参考:https://blog.csdn.net/qq_39033181/article/details/115962550

31. ThreadLocal

  ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,线程之间互不干涉,保证了数据安全,是一种以空间换时间的做法。在每个线程中都创建了一个 ThreadLocalMap 对象,ThreadLocalMap 中使用的 key 为 ThreadLocal,而 value 则是变量的值。在不同线程中,访问同一个ThreadLocal的set和get方法,它们对ThreadLocal的读、写操作仅限于各自线程的ThreadLocalMap,从而使ThreadLocal可以在多个线程中互不干扰地存储和修改数据。

32. ThreadLocal造成内存泄漏的原因与解决方案

  ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法

33. 手写一个自旋锁

参考于:https://blog.csdn.net/qq_39033181/article/details/116034770

34.并发编程三个必要因素

  • 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
  • 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到,如synchronized,volatile
  • 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

35. 其他

java基础常见面试题
java集合常见面试题目
JVM常见面试题汇总
MySQL数据库常见面试题目
Spring常见面试题总结

你可能感兴趣的:(java面试,java,并发编程,面试)