基础
1.什么是进程和线程,二者之间的关系?
- 进程:是系统运行程序的基本单位。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
- 线程:线程是一个比进程更小的执行单位,一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。
- 在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
2.进程和线程的区别和优缺点?
- 一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
- 区别:进程之间是独立的,各个线程不一定,同一进程中的线程极有可能会相互影响
- 优缺点:二者相反,线程执行开销小,但不利于资源的管理和保护(比较安全点)
3.为什么要使用多线程?
- 从计算机角度:因为线程是开销比较小的,是程序执行的最小单位,所以就是为了充分利用多核cpu的能力。
- 从项目角度:现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础。
4.说说线程的生命周期和状态?
new
:初始创建状态runnable
:运行状态blocked
:阻塞状态,等待锁释放waiting
:等待状态,表示当前线程需要等待其他线程做出一些动作(通知或中断)time_waiting
:超时等待状态,可以在指定的时间后返回terminated
:终止状态,表示线程已经运行完毕了
5.什么是线程死锁?
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。比如,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
6.如何预防和避免线程死锁?
- 预防死锁:
– 破坏死锁的产生的必要条件
– 1. 请求与保持:一个线程因请求资源而阻塞时,对已获得的资源保持不放。就可以让该线程释放掉已经用过的资源
– 2.不剥夺:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。当其他线程申请不到资源的时候,主动释放自己占有的资源。
– 3.循环等待:多个线程之间形成循环等待(你等我的,我等你的)资源关系。可以按照某一顺序来进行申请资源,破坏掉循环等待的条件。 - 预防完还是有死锁的必要条件呢?
- 避免死锁:
- 在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态(能够满足每个线程对资源的最大需求)。
乐观锁和悲观锁
7.乐观锁和悲观锁的区别?
- 悲观锁:总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题,所以每次在获取资源操作的时候都会上锁,其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
- 乐观锁:总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题。线程可以不停地执行,无需加锁也无需等待。
8.如何实现乐观锁?
9.CAS了解吗?原理?
- 版本号机制:在数据表上加一个版本号字段,每次修改后,字段会加1。然后在更新数据提交时,需要检测读取到的版本号和当前版本号相同时才能更新。
- CAS(Compare And Swap)算法:(多用)
- 用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
ConcurrentHashMap
采用CAS
和synchronized
来保证并发安全。CAS的实际应用
:举例说明(如何实现乐观锁,被应用于各大框架中)
10.乐观锁存在哪些问题?如何解决?
- ABA问题:一个值最开始读取是A,中间变成了B,但是最后准备赋值的时候又变成了A。这个时候CAS是检查不到变化的,但是实际上被更新了两次。
- ABA解决:在变量前面追加上版本号或者时间戳,联合使用
- 循环时间开销大:CAS一般不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
- 只能保证一个共享变量有效:当操作涉及跨多个共享变量时 CAS 无效
JMM(Java内存模型)
11. 并发编程的三个重要特性?
- 原子性(同步):一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。
- 可见性:当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。
- 有序性:程序执行的顺序要符合预期,一般在多线程中编译器和处理器会对指令进行重排序提高性能
12.什么是JMM?为什么需要JMM?
- JMM:Java定义并发编程相关的一组规范
- 目的是为了简化多线程编程,增强程序的可移植性
13.JMM是如何抽象线程和主内存之间的关系?
- 如何抽象:将程之间的共享变量必须存储在主内存中。
- 然后不同线程之间要进行通信的话,就必须先将线程1本地内存中的共享变量副本同步到主内存中,然后线程2才能到主内存中读取对应共享变量的值。
14.Java内存区域和JMM有何区别?
- 二者完全不同
- 前者:是JVM的一部分,主要用来存储数据和代码
- 后者:是并发编程的一种规范,主要是用来确保多线程环境下程序的正确性。
synchronized(重要)和volatile(处理多线程并发)
15.synchronized关键字的作用?怎么使用的?
- 作用:用于实现线程之间的同步,保证同一时间只有一个线程可以被访问
- 使用:用来修饰代码块或者方法
16.synchronized的底层原理???( 重要 )
- synchronized 是 JVM 内部实现的一种内置锁,当一个线程尝试获取一个已经被其他线程获取的 synchronized 锁时,该线程会进入阻塞状态等待其他线程释放该锁。在这个等待过程中,该线程会不断的尝试获取锁,这个过程被称为“自旋”,即不断重试直到获取到锁为止。
- JDK1.6之前:synchronized 采用的是偏向锁和重量级锁两种机制。对于偏向锁,如果一个线程获取到了一个对象的锁,那么该对象的头信息中会存储这个线程的标识,后续该线程再次获取这个锁的时候就不需要竞争了。对于重量级锁,当多个线程需要争用同一个锁的时候,会进入到阻塞状态,此时需要等待其他线程释放锁才能继续执行。
- JDK1.6之后:synchronized 引入了自适应锁,当线程竞争锁的次数较少时,锁会采用自旋的方式,而不是阻塞。如果线程竞争锁的次数达到一定阈值,就会升级为重量级锁,避免出现过多的自旋。
17.synchronized(内置锁)和ReentrantLock(可重入锁)的区别?
- 用法不同:前者可修饰方法、代码块;后者只能修饰代码块,而且需要初始化对象。
lock
上锁,unlock
释放锁 - 获取锁和释放锁方式不同:synchronized 会自动加锁和释放锁,后者手动
- 是否公平锁:前者非公平锁,后者可通过构造方法
boolean
值选择是否公平 - 是否可中断:前者不可中断,除非加锁代码异常或正常执行完成,若发生了死锁,就会一直等下去;后者可通过中断指令释放锁,解决死锁问题。
- 锁的对象不同:前者锁是保存在对象里面的,根据对象头数据来标识是否有线程获得锁或争抢锁;后者锁的是线程,根据进入线程的int类型和状态标识锁的获得或争抢。
- 底层实现不同:前者通过JVM层面的监视器实现,后者基于AQS实现
18.synchronized和volatile的区别?
- volatile不需要加锁,比synchronized更轻量级,不会阻塞线程
- 从内存可见性角度,volatile读相当于加锁,volatile写相当于解锁
- synchronized既能够保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性
19.volatile为什么不能保证原子性?
因为 volatile 的主要作用是确保对共享资源的操作在多线程环境下是可见的,即当一个线程修改了 volatile 变量的值后,其他线程可以立即看到这个修改。但它并不能保证对变量的复合操作是原子的。
例如:
例如,假设初始时 count 的值为 0,线程 A 和线程 B 同时执行 increment() 方法。由于复合操作不是原子的,可能出现以下情况:
线程 A 读取了 count 的值为 0。
线程 B 也读取了 count 的值为 0。
线程 A 将 count 的值加一,得到 1,并写回 count。
线程 B 也将 count 的值加一,得到 1,并写回 count。
结果 count 的值只增加了 1,但是实际上应该是增加了 2。这种情况就违反了原子性的要求。
ThreadLocal(线程局部变量)
20.ThreadLocal底层原理?内存泄露问题?如何在项目中使用ThreadLocal关键字?
- 作用域是当前单个线程,在线程开始时分配,线程结束时回收。ThreadLocal保存当前线程的变量,当前线程内,可以任意获取,但每个线程往ThreadLocal中读写数据是线程隔离,互不影响。
- 底层原理:ThreadLocal并不存放任何的信息,它是线程(Thread)操作ThreadLocalMap中存放的变量的桥梁。主要提供了初始化、set()、get()、remove()几个方法.
- 内存泄漏问题:
- 内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
- ThreadLocal存在内存泄漏问题,可以理解成在虚拟机垃圾回收时,会出现ThreadLocal占用内存无法被GC回收。
- 如何使用:
- 保存线程上下文信息,在任意需要的地方可以获取(比如每个请求怎么把一串后续关联起来,就可以用ThreadLocal进行set,在后续的任意需要记录日志的方法里面进行get获取到请求id,从而把整个请求串起来。)
- 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失
线程池
21.什么是线程池?有几种?重要参数?执行流程?饱和(拒绝)策略?如何设置线程池的核心线程数?
- 顾名思义,线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。
- 有几种?
- 定长线程池(FixedThreadPool):只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
- 定时线程池(ScheduledThreadPool):核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。
- 可缓存线程池(CachedThreadPool):无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。
- 单线程化线程池(SingleThreadExecutor):只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列
- 主要参数:
- corePoolSize:核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
- maximumPoolSize:线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
- keepAliveTime:线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
- unit:指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
- workQueue:任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
- 执行流程?
- 举个栗子:假如线程池中核心线程数是5,最大线程数是10,阻塞队列也是10;有新任务来的时候,将先使用核心线程执行;当任务数达到5个的时候,第6个任务开始排队进入阻塞队列;当任务数达到15个(>5+10,阻塞队列也满了)的时候,第16个任务将开启新的线程执行,也就是第6个线程;当任务数达到20个的时候,线程池满了,如果有第21个任务,将执行拒绝策略。
- 饱和(拒绝)策略?
- AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
- CallerRunsPolicy:由调用线程处理该任务。
- DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
- DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。
- 如何设置线程池的核心线程数?
- 首先了解下上下文切换:多线程编程中一般线程的个数都大于 CPU 核心的个数,CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式,当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
- 如果我们设置的线程池数量太小的话,那么同一时间有大量任务/请求需要处理,任务会阻塞,而且CPU 根本没有得到充分利用;如果设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,也会影响整体效率。
- CPU 密集型任务(N+1):这种任务消耗的主要是 CPU 资源,多出来的一个线程是为了防止一些其他原因导致任务暂停而带来的影响,一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。
- 二者如何判断?CPU密集型就是利用 CPU 计算能力的任务,比如说你在内存中对大量数据进行排序;但凡涉及到网络读取,文件读取这类都是 IO 密集型。
AQS
22.AQS是什么?AQS的原理是什么?
- AQS(AbstractQueuedSynchronizer,抽象队列同步器):是一个抽象类,主要用来构建锁和同步器
- 核心思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁 实现的,即将暂时获取不到锁的线程加入到队列中。
23.Semaphore 有什么用?Semaphore 的原理是什么?
- synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量。
- Semaphore 是共享锁的一种实现,它默认构造 AQS 的 state 值为 permits,你可以将 permits 的值理解为许可证的数量,只有拿到许可证的线程才能执行。
24.CountDownLatch 有什么用?原理是什么?用过吗?什么场景下用的?
- CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
- 原理:CountDownLatch 是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用 countDown() 方法时, state会减少,直至 state 为 0。当调用 await() 方法的时候,如果 state 不为 0,那就证明任务还没有执行完毕,await() 方法就会一直阻塞,也就是说 await() 方法之后的语句不会被执行。直到count 个线程调用了countDown()使 state 值被减为 0,或者调用await()的线程被中断,该线程才会从阻塞中被唤醒,await() 方法之后的语句得到执行。
- 用过吗?CountDownLatch 的作用就是 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
- 之前在项目中,有一个使用多线程读取多个文件处理的场景,我用到了 CountDownLatch 。具体场景是下面这样的:我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。为此我们定义了一个线程池和 count 为 6 的CountDownLatch对象 。使用线程池处理读取任务,每一个线程处理完之后就将 count-1,调用CountDownLatch对象的 await()方法,直到所有文件读取完之后,才会接着执行后面的逻辑。
- 什么场景?(上面)
25.CyclicBarrier 有什么用?原理是什么?
CyclicBarrier和CountDownLatch非常类似,它也可以实现线程间的技术等待,但是它的功能比CyclicBarrier更加复杂和强大,主要应用场景和CountDownLatch类似。