Java基础技术多线程与并发面试【笔记】

时间:2021-03-03 23:56:47

Java基础技术多线程与并发

什么是线程死锁?

​死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去,我们就可以称此时的系统处于死锁状态,即系统产生了死锁

死锁产生的条件是什么?

死锁产生的条件可以分成四个,且这四个都必须同时存在才能形成死锁

(1) 互斥条件:该资源任意一个时刻只由一个线程占用

(2) 请求与保持条件:一个线程以及进程因请求资源而阻塞时,对已获得的资源保持不放

(3) 不剥夺条件:线程以及进程已获得的资源在末使用完之前不能被其他线程以及进程强行剥夺,只有自己使用完毕后才释放资源

(4) 循环等待条件:若干线程以及进程之间形成一种头尾相接的循环等待资源关系

如何避免线程死锁?

​针对死锁产生的条件进行一一拆解:

​(1) 破坏互斥条件:一般无法破坏,因为使用锁的本意就是想让它们互斥的(临界资源需要互斥访问)

(2) 破坏请求与保持条件:一次性申请所有的资源

​(3) 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源

​(4) 破坏循环等待条件:按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件(最常用)

说说 synchronized 和 java.util.concurrent.locks.Lock 的异同?

​两者的相同之处就是 Lock 能完成 synchronized 所实现的所有功能

差异的话,简单地说,Lock 有比 synchronized 更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁,而且synchronized 会自动释放锁,像 Lock 就要求手工释放

更具体地来说,有以下差异:

含义不同

​Synchronized 是关键字,属于 JVM 层面,底层是通过 monitorenter 和 monitorexit 完成,依赖于 monitor 对象来完成

​Lock 是 java.util.concurrent.locks.lock 包下的,是 JDK1.5 以后引入的新 API 层面的锁

使用方法不同

Synchronized 不需要用户手动释放锁,代码完成之后系统自动让线程释放锁

ReentrantLock 需要用户手动释放锁,没有手动释放可能导致死锁

等待是否可以中断

​Synchronized 不可中断,除非抛出异常或者正常运行完成,而ReentrantLock 可以中断,这两个,一种是通过 tryLock (long timeout, TimeUnit unit),另一种是 lockInterruptibly () 放代码块中,调用 interrupt () 方法进行中断

加锁是否公平

​Synchronized 是非公平锁,而ReentrantLock 默认非公平锁,可以在构造方法传入 boolean 值来进行改变,true 代表公平锁,false 代表非公平锁

AtomicInteger 怎么实现原子操作的?AtomicInteger 有哪些缺点?

AtomicInteger 内部使用** CAS **原子语义来处理加减等操作,CAS 全称 Compare And Swap(比较与交换),通过判断内存某个位置的值是否与预期值相等,如果相等则进行值更新

CAS内部是通过 Unsafe 类实现,而 Unsafe 类的方法都是 native 的,在 JNI 里是借助于一个 CPU 指令完成的,属于原子操作

AtmoicInteger 缺点有

​(1) 循环开销大。如果 CAS 失败,会一直尝试。如果 CAS 长时间不成功,会给 CPU 带来很大的开销

(2) 只能保证单个共享变量的原子操作,对于多个共享变量,CAS 无法保证

​(3)存在 ABA 问题(如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A,当前线程的CAS操作无法分辨当前V值是否发生过变化)

ABA问题的解决?

解决方案:ABA 问题一般通过版本号的机制来解决。每次变量更新的时候,版本号加 1,这样只要变量被某一个线程修改过,该版本号就会发生递增操作。ABA 场景下的变化过程就从 “A-B-A” 变成了 “1A-2B-3A”

​JDK 从 1.5 开始提供了 AtomicStampedReference 类来解决 ABA 问题,具体操作封装在 compareAndSet () 中,compareAndSet () 首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值

java 中线程有哪些状态?

共 6 种状态:

初始状态 (NEW):尚未启动的线程处于此状态。通常是新创建了线程,但还没有调用 start () 方法

运行状态 (RUNNABLE):Java 线程中将就绪(ready)和运行中(running)两种状态笼统的称为 "运行中"。比如说线程可运行线程池中,等待被调度选中,获取 CPU 的使用权,此时处于就绪状态(ready),就绪状态的线程在获得 CPU 时间片后变为运行中状态(running)

阻塞状态 (BLOCKED):表示线程阻塞于锁

等待状态 (WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)

超时等待状态 (TIMED_WAITING):进入该状态的线程需要等待其他线程在指定时间内做出一些特定动作(通知或中断),可以在指定的时间自行返回

终止状态 (TERMINATED):表示该线程已经执行完毕,已退出的线程处理此状态

线程状态 RUNNABLE 如何变为 BLOCKED?

线程状态变为阻塞有多种原因

可能调用 wait () 方法进入等待池

可能执行同步方法或是同步代码块进入等锁池

可能调用了 sleep ()/join () 等待休眠或其他线程结束

可能发生了 I/O 中断等

Java 线程状态转化?

线程创建之后它将处于 NEW 状态,调用 start () 方法后开始运行,线程这时候处于 READY 状态。可运行状态的线程获得了 CPU 时间片后就处于 RUNNING 状态

线程执行 wait () 方法后,进入 WAITING 状态,此时需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep()或 wait()可以将线程置于 TIME_WAITING 状态,当超时时间到达又会返回到 RUNNABLE 状态

当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED 状态。线程在执行 Runnable 的 run () 方法之后将会进入到 TERMINATED 状态

Java基础技术多线程与并发面试【笔记】

为什么要使用线程池?

(1) 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗

(2) 提高响应速度。 当任务到达时,任务可以不需要等到线程创建就能立即执行

(3) 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

线程池可以认为是更普遍的 “池化资源” 技术的一个示例,其产生的原因是:

由于创建以及销毁对象需要对内存资源或者其他相关资源进行相对操作,所以某些消耗资源的对象的创建和销毁是比较费事的,所以资源优化的一个方向就是减少创建和销毁对象的次数,尽可能地复用

如何使用线程池?

工具类 Executors 提供了静态工厂方法以生成常用的线程池:

(1) newSingleThreadExecutor:创建一个单线程的线程池。如果该线程因为异常而结束,那么会有一个新的线程来替代它

(2) newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大值,一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程

(3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(默认 60 秒不执行任务)的线程

当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小

(4) newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求

java 线程池的核心参数有哪些?

Executors 也提供自定义的线程池构造方法,里面包括七个参数:

public ThreadPoolExecutor(  int corePoolSize,   //常驻线程数,即使空闲时仍保留在池中的线程数
int maximumPoolSize, //线程池中允许的最大线程数
long keepAliveTime, // 存活时间。线程数比corePoolSize多且处于闲置状态的情况下,这些闲置的线程能存活的最大时间,为0表示会立即回收;
TimeUnit unit, //keepAliveTime的单位
BlockingQueue<Runnable> workQueue, //被提交尚未被执行的任务阻塞队
ThreadFactory threadFactory, // 创建线程的工厂
RejectedExecutionHandler handler // 饱和拒绝策略,当队列满了并且线程个数达到maximunPoolSize后采取的策略。目前支付四种:AbortPolicy(抛出异常),CallerRunsPolicy(调用者线程处理),DiscardOldestPolicy(直接丢弃任务,不予处理也不抛出异常),DiscardPolicy(默默丢弃,不抛出异常)
)

如何设置初始化线程池的大小?

可根据线程池中的线程处理任务的不同进行分别估计:

(1) CPU 密集型任务,这类任务需要大量的运算,通常 CPU 利用率很高,无阻塞,因此应配置尽可能少的线程数量,可设置为 CPU 核数 + 1

(2) IO 密集型任务,这类任务有大量 IO 操作,伴随着大量线程被阻塞,可配置更多的线程数,通常可设置 CPU 核心数 * 2

shutdown 和 shutdownNow 有什么区别?

shutdown,有序停止,将线程池状态设置为 SHUTDOWN

(1) 停止接收外部提交的任务

(2) 先前提交的任务务会执行(但不保证完成执行)

shutdownNow,尝试立即停止,将线程池状态设置为 STOP

(1) 停止接收外部提交的任务

(2) 不再处理队列里等待的任务

(3) 忽略队列里等待的任务

(4) 返回正在等待执行的任务列表

shutdownNow 试图取消线程的方法是通过调用 Thread.interrupt () 方法来实现的,非强制的,如果线程中没有 sleep 以及 wait 等应用,interrupt () 方法是无法中断当前的线程的

所以,ShutdownNow 并不代表线程池就一定立即就能退出,它也可能必须要等待所有正在执行的任务都执行完成了才能退出,但是大多数时候是能立即退出的

ThreadLocal 的作用是什么?

作用是提供每个线程存储自身专属的局部变量

其实现原理

每个 Thread 维护着一个 ThreadLocalMap 的引用,ThreadLocalMap 是 ThreadLocal 的内部类,用 Entry 来进行存储

调用 ThreadLocal 的 set () 方法时,实际上就是往 ThreadLocalMap 设置值,key 是 ThreadLocal 对象,值是传递进来的对象

调用 ThreadLocal 的 get () 方法时,实际上就是往 ThreadLocalMap 获取值,key 是 ThreadLocal 对象 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value,正是因为这个原理,所以 ThreadLocal 能够实现 “数据隔离”,获取当前线程的局部变量值,不受其他线程影响

ThreadLocal有什么风险?

会有内存泄漏的风险

ThreadLocal 被 ThreadLocalMap 中的 entry 的 key 弱引用,如果 ThreadLocal 没有被强引用, 那么 GC 时 Entry 的 key 就会被回收,但是对应的 value 却不会回收就会造成内存泄漏

​解决方案:每次使用完 ThreadLocal,都调用它的 remove () 方法,清除数据

AQS 的原理?

​AQS(AbstractQueuedSynchronizer)核心思想是,如果被请求的资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态

如果被请求的资源被占用,则需要一套线程阻塞等待以及唤醒分配的机制,该机制基于一个 FIFO(先进先出)的等待队列实现

AQS 的应用?

​作为一个用来构建锁和同步器的框架,AQS 能简单且高效地构造出大量同步器,事实上 java.util.concurrent.concurrent 包内许多并发类都是基于 AQS 构建,这些同步器从资源共享方式的方式来看,可以分为两类

(1)Exclusive(独占):只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁:

​ A、公平锁:按照线程在队列中的排队顺序,先到者先拿到锁;

​ B、非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的。

(2) Share(共享)多个线程可同时执行,如 Semaphore以及CountDownLatch以及CyclicBarrier 等

​此外,也可以通过 AQS 来自定义同步器,自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队 / 唤醒出队等),AQS 已经在顶层实现好了

Semaphore,CountDownLatch,CyclicBarrier 这些同步器各自的原理和应用场景?

​ A、Semaphore (信号量),synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore (信号量)可以指定多个线程同时访问某个资源

​ B、CountDownLatch (闭锁): CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。比如有一个任务 A,它要等待其他 4 个任务执行完毕之后才能执行,此时就可以利用 CountDownLatch 来实现这种功能了

​ C、CyclicBarrier (循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,主要应用场景也差不多,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大,CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)

它要做的事情是,让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活

​简单地说,CountDownLatch 表示一个或者多个线程,等待其他多个线程完成某件事情之后才能执行,CyclicBarrier 表示多个线程互相等待,直到达到同一个同步点(屏障),再继续一起执行

代码题:写两个线程,交替打印数字和字母,一个线程打印 1~26,另一个线程打印字母 A-Z

其最终打印结果为:1A2B3C4D5E6F7G8H9I10J11K12L13M14N15O16P17Q18R19S20T21U22V23W24X25Y26Z

这是一个是很典型的多线路笔试题目,题目解题思路也比较清晰,看到通知以及等待,其实就可以想到 Object 中的 wait 和 notify:数字打印线程先打印一个数,然后通知字母打印线程打印字母,自身暂停,字母打印线程打印一个字母后,通知数字打印线程继续打印,自身暂停,不断循环这个过程

代码如下:

    public static void main(String[] args) {
//所有线程共用的线程锁
Object lock = new Object();
new Thread(new LockRunable(lock), "数字打印线程").start();
new Thread(new LockRunable(lock), "字母打印线程").start();
} /**
* 线程类
*/
static class LockRunable implements Runnable {
@Override
public void run() {
synchronized (LockRunable.class) { //类锁来代替lock
for (int i = 0; i < 26; i++) {
if (Thread.currentThread().getName() == "数字打印线程") {
System.out.print((i + 1));
LockRunable.class.notifyAll(); //类锁
try {
LockRunable.class.wait(); // //类锁
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (Thread.currentThread().getName() == "字母打印线程") {
System.out.print((char) ('A' + i));
LockRunable.class.notifyAll();//类锁
try {
LockRunable.class.wait();//类锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}// end for for
}// end for synchronized
}// end for run
}

Java基础技术多线程与并发面试【笔记】