JDK并发面试题大全

JDK并发面试题大全


目录

文档索引

面试题汇总

Q:为什么需要并发?

Q:线程的生命周期?

Q:什么是守护线程?

Q:线程间如何通信?

Q:wait-notify如何实现?

Q:为什么wait-notify要用synchronized包围?

Q:什么是线程池?线程池有哪些参数?

Q:线程池参数如何根据业务设置?

Q:线程并发安全问题如何解决?

Q:保证并发安全的三大特性是什么?

Q:synchronized如何实现锁?

Q:ReentrantLock 如何实现锁?

Q:ReentrantReadWriteLock 如何实现锁?

Q:synchronized和ReentrantLock 有什么区别?

Q:Semaphore、CountDownLatch、CyclicBarrier有什么用?如何实现的?

Q:Java内存模型(JMM)是什么?

Q:为什么会有可见性问题?如何解决?

 Q:线程间出现死锁如何解决?


文档索引

官网使用手册:Chapter 17. Threads and Locks


面试题汇总

Q:为什么需要并发

A:我们知道代码是串行执行的,当代码逻辑有IO操作,CPU需等待IO操作完成才可以继续往下执行剩余代码,如后续代码无需IO操作结果,那么我们可以创建线程,一个线程执行IO操作,一个线程执行剩余代码,通过多线程发挥CPU多核能力,从而提升代码性能

Q:线程的生命周期?

A:

1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。JDK提供了Thread(继承)、Runnable(无返回值实现)、Callable-Future(有返回值实现)来创建线程对象。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。(需注意需调用start方法,线程才会进入就绪态,等待CPU执行子线程的run方法,若直接调用run方法,则是由主线程执行run方法)
3. 阻塞(BLOCKED):表示线程阻塞于锁。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。

Q:什么是守护线程?

A:线程分为用户线程和守护线程。

JVM会在所有用户线程停止后退出,即使此时有守护线程在运行,JVM也会将守护线程停止并退出。守护线程经常被用来执行一些后台任务,如垃圾回收线程。

Q:线程间如何通信?

A:线程的通信可以被定义为:当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以避免无效的资源争夺。

线程间通信方式:

1、【共享内存】通常会设置一个共享变量,然后多个线程去操作同一个共享变量,从而达到线程通讯的目的,一般需要使用volitale关键字修饰,从而保证变量可见性。

2、【“等待-通知”通信方式】是Java中使用普遍的线程间通信方式,其经典的案例是“生产者-消费者”模式。

通过使用对象的wait()、notify()两类方法来实现,对象的wait()方法的主要作用是让当前线程阻塞并等待被唤醒,对象的notify()方法的主要作用是唤醒在等待的线程。

3、【同步工具类】Java提供了三个同步工具类:Semaphore、CountDownLatch、CyclicBarrier

Q:wait-notify如何实现?

A:每个对象都会有监视器,整体由入口队列EntrySet,获取监视器的线程Owner,等待队列WaitSet组成。

线程会先进入EntrySet,如Owner没有被其他线程持有,则线程与EntrySet其他线程竞争持有Owner,如未获取到Owner,则在EntrySet中BLOCKED阻塞,直到持有Owner的线程释放,则继续竞争获取Owner,获取到Owner则可以执行synchronized包围的代码。

当在代码里执行wait方法时,线程则会从Owner进入到WaitSet中WAITING等待,直到有线程调用对象的notify(唤醒一个WaitSet等待线程)或notifyAll(唤醒所有WaitSet等待线程)方法,此时在WaitSet的线程则会进入EntrySet中重新竞争锁

A java monitor

 

Q:为什么wait-notify要用synchronized包围

A:我们知道wait-notify都是监视器实现的,所以线程进入WaitSet的前提是已经获得Owner,即先通过synchronized进入监视器

Q:什么是线程池?线程池有哪些参数?

A:线程的创建会耗费系统的内存资源,大量的线程创建会引起内存异常,所以我们需要一个能对线程进行统一管理的方法

Java提供了ThreadPoolExcutor线程池类使用,相关参数及逻辑如下:

  1. 当前线程池中线程数少于corePoolSize,会新增线程来对等待的任务进行处理
  2. 当大于corePoolSize时,任务进入等待队列workQueue,等待队列有以下几种
    • LinkedBlockingQueue 无限大小,易导致内存过大
    • SynchronousQueue 生产消费队列
    • ArrayBlockingQueue 有限大小
  3. 等待队列满,但线程数少于maximumPoolSize时,会新增线程对等待的任务进行处理
  4. 当线程等待超过存活时间keepAliveTime无需处理任务,则线程注销
  5. 创建线程可通过ThreadFactory工厂对象进行创建
  6. 当超过最大线程,则执行拒绝策略RejectedExecutionHandler,拒绝策略有以下几种
    • 抛异常(默认策略)
    • 沉默处理
    • 当前线程阻塞处理
    • 丢弃最早的任务后重试
    • 自定义实现
  7. 通过shutdown或shutdownNow关闭线程池

Q:线程池参数如何根据业务设置?

A:业务功能通常分为IO密集和CPU密集。

如线程任务大部分为数据库操作、网络IO、文件IO等,则属于IO密集,如大部分为CPU计算,则属于CPU密集。

核心线程数:针对IO密集,通常为CPU数的2~3倍,针对CPU密集,通常为CPU数

最大线程数:通过为核心线程数+1

等待队列:如需进行排序,则使用PriorityBlockingQueue;

如队列要无限大,则使用LinkedBlockingQueue;

如不需要存储,则使用SynchronousQueue;

如定长无需扩容,通常使用ArrayBlockingQueue;

拒绝策略:如对被拒绝的线程进行补处理,可在抛异常策略中,异常处理里进行补处理;

如已有重试机制,则可选择沉默处理;

如有业务需要,也可选择自定义实现;

Q:线程并发安全问题如何解决?

A:通过满足三大特性实现并发安全,通常方案有synchronized、Lock、CAS、volatile、JDK并发工具(CyclicBarrier、CountDownLatch、Semaphore)、使用并发安全的集合、以及原子性的工具类

解决方案:线程并发安全问题解决方案

Q:保证并发安全的三大特性是什么

A:原子性、可见性、有序性

1、原子性

原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉

例如i=i+1不是原子操作,此时并发调用该指令时,i值不会如我们预想那样进行赋值,这便是原子性问题

2、可见性

可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改

可参考4.1 JMM抽象内存模型,主存与工作内存不一致,便会引起可见性问题

3、有序性

为了性能优化,编译器和处理器会进行指令重排序;也就是说java程序天然的有序性可以总结为:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的

Q:synchronized如何实现锁

A:synchronized通过对象的对象头中的MarkWord实现,其锁升级过程步骤如下

  1. 初期锁对象刚创建时,还没有任何线程来竞争,对象的Mark Word是下图的第一种情形,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)。
  2. 当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word会记录自己偏爱的线程的ID,把该线程当做自己的熟人。如下图第二种情形。
  3. 当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,通过CAS对对象头进行设置,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。如下图第三种情形。
  4. 如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程。如下图第四种情形。

Q:ReentrantLock 如何实现锁

A:ReentrantLock 基于 AQS(AbstractQueuedSynchronizer)来实现,

整体流程:一个线程通过CAS尝试获得锁,若此时已有线程占据了锁,则加入AQS的等待队列排队,当锁被释放后,排在等待队列队首的线程会被唤醒,再次通过CAS尝试获得锁,若在此时

  1. 非公平锁:同时有另一个线程进来尝试获取锁,则可能会让这个线程先抢到
  2. 公平锁:同时有另一个线程进来尝试获取锁,发现自己不是在队首时,排到队尾,由队首线程获取锁

Q:ReentrantReadWriteLock 如何实现锁

A:ReentrantReadWriteLock 是读写锁,对于读多写少的场景使用ReentrantReadWriteLock 性能会比ReentrantLock高。

读写锁内部维护了两个锁:

  1. 写锁用于写操作,其实现与ReentrantLock相似,为独占锁
  2. 读锁用于读操作,通过AQS的state实现重入,为共享锁

当线程持有写锁时,同个线程可继续持有读锁,其他线程无法持有写锁和读锁

当线程持有读锁时,其他线程可持有读锁,无法持有写锁

Q:synchronized和ReentrantLock 有什么区别

A:

  1. synchronized是非公平锁,ReentrantLock 可实现公平锁和非公平锁
  2. synchronized不是可中断锁,而Lock是可中断锁
  3. synchronized为独占锁,而Lock可实现独占锁和读写锁
  4. 发生异常时,synchronized会自动释放线程占有的锁,因此不会导致死锁现象发生,而Lock如果没有主动通过unLock()去释放锁,则很可能造成死锁现象
  5. 一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多余一个条件关联的时候,就不得不额外地添加一个锁

Q:Semaphore、CountDownLatch、CyclicBarrier有什么用?如何实现的?

A:

Semaphore:信号量,可以⽤来控制同时访问特定资源的线程数量。

初始化state为令牌数量,线程通过acquire获取令牌,此时state减去令牌数,当令牌数不足时,抢令牌的线程会进入等待队列,同时线程挂起。线程通过release释放令牌,此时state加上令牌数,同时会将队列先入列的线程进行唤醒。

CountDownLatch:允许一个或多个线程一直等待,直到其他线程执行完后再执行。

初始化state为令牌数量,主线程通过await,将线程挂起,并进入等待队列。子线程通过countDown将令牌释放一个,最后一个释放的,将唤醒等待队列中的线程。

CyclicBarrier:与CountDownLatch类似,但CountDownLatch是一批线程等待另一批线程,而CyclicBarrier是等待的线程达到一定数量后才继续执行,同时CyclicBarrier可以循环进行屏障。

初始化state为令牌数量,线程执行await方法,此时线程通过condition的await方法挂起,同时令牌数减一,待令牌数为0后,执行传入的任务,同时通过signalAll唤醒等待线程,并将令牌数重置,从而实现循环屏障。

Q:Java内存模型(JMM)是什么?

A:JMM抽象定义了主内存与本地内存

在Java内存模型(JMM)中规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory,可以与处理器的高速缓存类比),线程的工作内存中保存了被该线程使用的的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读取主内存中的数据。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化.

在组成原理中,我们知道CPU是对内存进行存取,但在现代计算机中,cpu的指令速度远超内存的存取速度,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。

但在多核CPU中,这引发了缓存一致性(Cache Coherence)的问题,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。

Q:为什么会有可见性问题?如何解决?

A:根据Java内存模型(JMM)规定,线程对变量的所有操作都在线程的工作内存中进行,如果线程A、B同时操作一个变量,从主内存读取到各自线程的工作内存,如果线程A对变量进行修改,此时线程B对修改并不知晓,引发B线程脏读,这便是可见性问题。

通过volatile修饰变量,可解决可见性问题。

Q:线程间出现死锁如何解决?

A:死锁即两个或多个线程无限期相互等待锁定资源的问题。

比如线程A锁定资源1,等待资源2、线程B锁定资源2,等待资源1,此时便形成死锁。

解决方案:死锁问题解决方案

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值