Skip to content

JUC面试题

Updated: at 04:12 PM

1. JUC

1.1. 线程基础

1.1.1. 创建线程的方式

a. 继承自Thread类,重写run,start()

b. new Thread(Runnable),重写Runnable接口的run,start()

c. 实现Callable接口,实现call方法,使用FutureTask来包装该线程(因为FutureTask是Runable和Future结合的,Future又是用来存储Callable结果的)。Thread的参数可以接收一个FutureTask,FutureTask futureTask = new FutureTask<>(callable);FutureTask可以作为Thread的参数,Thread thread = new Thread(futureTask);

1.1.2. Thread、Runnable、Callable三个接口的区别?

i. 使用继承Thread的方法的局限性在于无法再继承其他类,而且不能用线程池中(threadPool.submit(Runnable()))

ii. Thread的本质是实现了Runnable接口

iii. Callable常用于获取获取返回结果的场景,但是获取结果的get()是阻塞的。callable的优点在于可以抛出异常。

iv. Runnable和Callable的转化:Executors可以实现Runnable对象和Callable对象的转化

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;
}
public FutureTask(Runnable runnable, V result) {
    // 通过适配器RunnableAdapter来将Runnable对象runnable转换成Callable对象
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;
}

1.1.3. 为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()?

如果直接调用run(),则会把run函数作为普通方法来调用,而不会开启多线程

1.2. 线程有哪些状态?

  1. 六种
  2. NEW 创建,RUNNABLE 运行,BLOCKED阻塞,WAITING,TIME_WAITING等待,TERMINATED终止
  3. 当一个线程被创建后为NEW状态,当被调用start()后为RUNNABLE状态(包括RUNNING和READY),抢锁失败后为BLOCKED阻塞状态,调用wait、join、park后进入WAITING,带上时间进入TIME_WAITING状态,终止状态为TERMINATED状态,

Java 线程状态变迁图

1.2.1. java线程状态与Linux线程状态的区别?

合并

Linux中有Ready和Running状态,而java中将这两个状态合并成了RUNNABLE状态,也就是说,java向开发者屏蔽了OS线程调度的细节,不论一个线程有没有调度到CPU上,开发者看到的都是Runnable状态。

拆分

Java中将OS中的BLOCKED分为了BLOCKED和WAITING状态,这两个OS看来都不会占用CPU资源,但是Java的BLOCKED状态并不会释放锁资源。

1.2.2. wait()和notify()是什么?

wait()是让Object对象释放当前线程的对象锁,进入等待队列,线程进入WAITING状态。(必须和synchronized配合使用,释放的是当前线程获取到的synchronized锁)

wait和notify()的原理

wait():

(1)JVM将当前线程加入到Monitor的waitSet里面,等待被其他线程唤醒

(2)释放Monitor的Owner,让其他线程可以抢占锁

(3)线程状态变为WAITING

notify()/notifyAll():

(1)Monitor唤醒waitSet中第一条(全部)等待的线程

(2)被唤醒后的线程会从WaitSet移动到EntryList,线程具备了排队抢夺Monitor的Owner权利的资格,其状态从WAITING变成了BLOCKED。因为synchronized的非公平性,下次可以抢占锁资源

DEMO:

class A {
	private Object obj = new Object();
	public void f1() {
        	synchronized(obj1) {
                	...
                        obj1.wait();
                	...
                }
            }
    public void f2() {
        synchronized(obj1) {
            ...
            obj1.notify();
            ...
        }
    }
        }
    }
}
请区分一下线程的sleep,yield,wait,join方法
  1. sleep(time):(1)释放CPU资源,但不释放锁资源(2)需要指定时间,当时间结束后重新运行(3)不考虑线程的优先级(4)调用sleep后进入TIME_WAITING状态(5)InterruptException异常审查捕获
  2. yield:(1)释放CPU资源,不释放锁资源(2)释放后只给相同优先级的线程CPU调度的机会(3)调用后进入RUNNABLE状态(ready),还有可能重新抢占到CPU
  3. join:(1)不释放锁资源,阻塞线程直到目标线程执行结束,本质还是基于wait和notify实现的,如果仍然在运行就继续wait()(2)(4)InterruptException异常审查捕获
  4. wait:(1)释放锁资源,醒来后要重新抢锁(2)调用后进入WAITING状态(3)(4)InterruptException异常审查捕获
Thread的Interrupt()的作用
  1. 线程处于阻塞状态:Object.wait()、Thread.sleep(time)、Thread.join(),如果interrupt了则抛出InterruptException异常,需要手动捕获并处理(也可以Thread.currentThread.interrupt())重设标志位。
  2. 线程处于运行状态,则只是将打断标记位置为true,通过isInterrupted()查看自己是否被中断,然后做相应的处理。
守护线程

JVM线程分为用户线程和守护线程,终止方向是用户线程->JVM进程->守护线程

Entrylist和WaitSet是如何实现的?

双向链表,基于JVM的Monitor实现的,每个节点是一个线程,差别在于

  1. EntryList中的节点是BLCOKED,WaitSet中的节点是WAITING和TIME_WAITING
什么时候用notifyAll什么时候notify?

基于synchronized的等待队列不能设置多个condition(ReentrantLock可以实现多个Condition)

所以等待多个condition的时候因为不知道是哪个condition所以用notifyAll(),而所有线程的等待条件都一样且单进单出的时候用notify()

什么是信号丢失?

线程还没有wait()的时候就调用signal(),signal()的信号丢失了。

解决:唤醒信号存储为类变量

public class SignalLossDemo {

    Object monitorObject = new Object();
    boolean wasSignalled = false;

    public void doWait() {
        synchronized (monitorObject) {
            if(!wasSignalled) {
                try {
                    monitorObject.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            wasSignalled = false;
        }
    }

    public void doNotify() {
        synchronized (monitorObject) {
            monitorObject.notify();
            wasSignalled = true;
        }
    }
}
什么是虚假唤醒?

线程从wait()中醒来,并不一定是条件满足了(只能一个队列,可能多个condition),或者条件没有满足直接调用了notify,因此会额外增加一些开销

解决:wait()放在循环体内

public class EarlyWakeUpDemo {
    Object monitorObject = new Object();
    boolean wasSignalled = false;

    public void doWait() {
        synchronized (monitorObject) {
            while(!wasSignalled) {
                try {
                    monitorObject.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            wasSignalled = false;
        }
    }

    public void doNotify() {
        synchronized (monitorObject) {
            monitorObject.notify();
            wasSignalled = true;
        }
    }
}

1.3. Volatile&Synchronized&ReentrantLock&Condition

1.3.1. 讲一下synchronized的用法与底层实现

用法
  1. 代码块加锁,要拿到指定对象,如果是this那就用当前实例的锁
  2. 为实例方法加锁,此时拿到的是实例对象this的锁
  3. 静态方法加锁,拿到的是类的锁
实现
  1. 指令层面:synchronized同步代码块与同步方法的底层实现 monitorenter/monitorexit(同步代码块), acc_sychronized(accessFlags,同步方法)
  2. 数据记录在哪里:对象头MarkWord
  3. 锁的底层实现:

​ i. 偏向锁:对象的MarkWord记录线程的ID,只能是一个存活的线程,原来线程挂了可以重偏向

​ ii. 轻量级锁:线程的Lock Record帧记录了对象的MarkWord,MarkWord记录了Lock Record

​ iii. 重量级锁:JVM C++实现的ObjectMonitor,关键数据结构Cxq(入队前CAS自旋是不公平锁的原因),还有个EntryList,WaitSet。其他还有ownerThread和线程重入次数(可重入锁)。ObjectMonitor的ownerThread是持有锁的线程,MarkWord是Monitor地址,Lock Record是MarkWord。

1.3.2. 锁升级过程

偏向锁

原理:锁之间不存在竞争,只有一个存活的线程占有该对象的锁。

加锁过程
  1. MarkWord是否为可偏向状态,即是否为1,且锁标识位为01
  2. 测试当前线程ID是否是当前线程ID,如果是则执行同步代码块
  3. 不是则执行CAS,如果线程ID是空的则MarkWord改为当前线程ID
  4. 如果线程ID是别的线程ID,看那个线程是否还存活,如果不存活则重偏向,如果存活则撤销那个线程偏向锁,升级为轻量级锁
偏向锁的撤销
  1. 在一个安全点停止拥有锁的线程
  2. 遍历线程的栈帧,检查是否有Lock Record。如果有,就要清空Lock Record并且将MarkWord重置以变为无锁状态。
  3. 将当前锁升级为轻量级锁
  4. 唤醒当前线程
延迟偏向

一开始5s时JVM加载java类,加偏向锁再撤销会浪费。

什么是批量重偏向,什么是批量撤销?

批量重偏向:线程1创建了N个对象,一开始开启了偏向锁的这N个对象,线程2又去访问这N个对象,访问过程中这N个对象因为发生线程竞争而变为轻量级锁,当第20次的时候JVM会认为一开始重偏向错了,所以以后创建的对象改为偏向线程2的偏向锁了。

批量撤销:线程1创建40个,全是偏向线程1。线程2加锁这40个,前20个轻量,后20个偏向t2(撤销了40次)。线程3再加锁,对象变为不可偏向状态。以后新创建的对象也变为不可偏向状态。

代码例子:并发编程:批量重偏向、批量撤销-CSDN博客

轻量级锁

原理:本质是自旋锁,一般用于锁竞争不是很激烈的时候

加锁流程:
  1. 栈帧创建一个Lock Record,记录MarkWord拷贝,MarkWord记录Object的地址
锁升级过程:
  1. CAS尝试加轻量级锁,不成功说明其他线程已经加上轻量级锁了,自旋一定次数,如果还是失败了则升级重量解锁
  2. 升级重量级锁的过程是申请Monitor,MarkWord指向Monitor地址,Monitor的OwnerThread指向当前线程。
普通自旋锁和适应性自旋锁

普通自旋锁自旋次数固定(默认10)

适应性自旋锁根据上次加锁自旋次数动态改变下次自旋次数,越激烈就自旋越少次

重量级锁

数据结构:Cxq(其实可以忽略,不重要),EntryList,WaitSet,OwnerThread

因为涉及信号量和互斥锁,涉及到到用户态到内核态的切换,开销大

为什么是非公平锁?

新线程加进Cxq队列之前会先进行一次CAS尝试,所以是非公平锁

entryList和waitSet

wait()进入waitSet,被唤醒后进入entrylist。线程抢锁失败进入entryList

1.3.3. ReentrantLock是什么?AQS是什么?与synchronized的区别?

AQS的实现?

AQS同步器,提供了FIFO双向链表和volatile修饰的state变量。

AQS用了模板方法:

  1. acquire()里面先调用钩子方法tryAcquire()实现对state变量的修改,成功抢到锁,如果失败则调用addWaiter()加入队列。addWaiter()通过CAS将已经被封装成Node的线程加入队尾,加入队尾后调用acquireQueued()不断在前驱节点自旋,如果上一个节点是头节点则尝试获得锁,如果上一个节点不是头节点则park()住,等待前一个节点将它唤醒。唤醒后重新进入acquireQueued()的自旋抢锁。

  2. release()里面首先调用了tryRelease()钩子方法,对state变量进行修改,然后unpark()唤醒其后继节点。后继节点唤醒后又进入acquireQueued()自旋,在这里面完成出队操作。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
ReentrantLock的实现?
用处:
  1. 支持公平锁和非公平锁
  2. 能够设置阻塞时间(lock.tryLock(5, TimeUnit.MICROSECOND))
原理:

ReentrantLock基于AQS实现,重写了tryAcquire()和tryRelease()。tryAcquire()是CAS让state + 1,tryRelease()是CAS让state - 1。

可重入的体现:

tryAcquire()中如果发现是持有锁的线程,会让state累加。tryRelease()释放锁的时候如果持有锁的线程,会让state减1。

公平锁和非公平锁的体现?

FairSync和NonFairSync有不同的tryAcquire()的实现。非公平锁每次lock()进入tryAcquire()都会先进行CAS操作加锁,公平锁则增加了hasQueuedPredecessors()函数判断队列中有没有前继节点,如果当前线程是队列头节点则可以抢,否则不能抢。

ReentrantLock可打断的实现?

支持可打断和不可打断的。可打断是指的一个线程在获取锁的过程中,另一个线程可以调用interrupt方法打断该线程。不可打断指的是,哪怕一个线程调用了interrupt,该线程也不会被中断。

  1. 不可打断模式:

​ park()之后检查如果被中断过,只是将局部变量interrupted置为true返回给上层acquire(),由acquire()处理,不会影响到acquireQueued()的循环执行。

  1. 可打断模式:
static final class NonfairSync extends Sync {
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg); // 该函数类似于acquireQueued(),如果park()之后检查到被park()了,则抛出中断异常。
    }
Reentrantlock非公平锁释放的过程

释放锁成功,将当前的exclusiveOwnerThread置为null,Sync的state置为0,当前队列不为null则进入unparkSuccessor(),将head的下一个Node的线程给unpark()。这时该线程又会进入acquireQueued()进行tryAcquire()获取锁。

源码总结
public void acquire(...) {
        if (!tryAcquire(...) &&
            acquireQueued(addWaiter(...))
    }
            
  public acquireQueued(...) {
      for(;;;) {
          if(p == head && tryAcquire(...)) {
              // 出队
              break;
          }
          
          park();
      }
  }
ReentrantLock与Synchronized的区别?
相同

可重入锁,同一线程多次加锁和释放锁

不同
  1. synchronized依赖于JVM,需要os实现,开销大。ReentrantLock是JDK层面实现。
  2. Reentranlock可以实现公平和非公平锁,synchronized是非公平锁
  3. 可以设置抢占阻塞时间
  4. 等待可中断:即正在等待的线程可以选择放弃等待,改为处理其他事情,通过lock.lockInterruptibly()来实现这个机制
  5. ReentrantLock + Condition实现不同条件通知,synchronized相当于只有一个condition

1.3.4. volatile的实现原理?

用处
  1. 可见性:JMM层面指每个线程的工作内存都是最新的,CPU层面是cache是最新的
  2. 有序性:JVM层面和CPU层面指令禁止重排序(并发情况下会出现指令重排序)
  3. 原子性:不能保证
底层实现
  1. CPU层面可见性:lock前缀指令要求修改变量后所在缓存立即写回系统主存。并且要求根据MESI协议,通过CPU嗅探总线上其他CPU是否由该缓存,将其置为无效,以后要重新从主存获取。
  2. CPU层面有序性:lock前缀指令还有一个作用是加内存屏障,禁止内存屏障前后的指令重排序
  3. JMM层面可见性:线程的工作内存修改后强制刷新到共享内存
  4. JMM层面有序性:Happens-before原则
happens-before原则

前一个操作的结果对下一个操作可见

  1. volatile变量规则:对一个变量的写操作先行发生于后一个变量的读操作
  2. 线程启动规则:Thread的start方法先于此线程的每一个操作
  3. 传递规则:A先于B,B先于C,则A先于C
  4. 锁定原则:unlock先于对它后续的lock
  5. as-if-serial规则:同一个线程中,有依赖关系的先发生于后。换句话说,单线程运行一定保证结果不变。
与synchronized的比较

volatile仅仅保证可见性和有序性,synchronized可以保证可见性、有序性和原子性

1.3.5. JAVA对象头包括了哪些信息?

  1. MarkWord:锁信息
  2. Class Metadata Address:指向方法区中类元数据
  3. 数组的长度(如果是数组)
Mark Word

img

1.3.6. Condition的用法?

Condition的等待队列是单向链表,但是还是记录了一个首尾节点

Lock.newCondition()创建一个新的等待队列

AQS同步队列和等待队列是聚合关系,只能一个同步队列,可以多个等待队列

await()流程
  1. 创建线程封装的Node加入等待队列尾
  2. 释放锁,唤醒AQS同步队列头节点后的那个节点
  3. while中park(),直到park唤醒并且检查到节点离开等待队列重新回到同步队列,退出while循环
  4. 调用accquireQueued()不断尝试获得锁

伪代码:

public void await() {
	addConditionWaiter(); // 加入等待队列
        release(...);  // 唤醒头节点下一个
	while(!isOnsyncQueue(...)) {
		park();  // 唤醒后看是否已经从等待队列到同步队列中了
	}
	acquireQueued(); // 里面还是park(),即自旋+阻塞
}
signal()流程
  1. enq()将等待队列头部放入AQS同步队列尾部

  2. 唤醒当前线程,从park()之后执行,即正常进入同步队列中的阻塞等待(acquireQueued)

总结

ReentrantLock的acquire()和release()是进入同步队列中的acquireQueue()进行自旋+阻塞。condition相当于进入等待队列中进行自旋+阻塞,如果唤醒并离开while循环(说明已经在同步队列了),则重新进入同步队列中的acquireQueue()进行自旋+阻塞。

1.3.7. ReentrantReadWriteLock是什么?

支持读写锁,读锁可重入,写锁不可重入

lock.readLock() lock.writeLock()

为什么只支持锁降级,不支持锁升级?
  1. 为什么支持锁降级:假如已经获得了写锁,写锁是只有一个线程才能持有,降级为读锁不受影响
  2. 为什么不能锁升级:锁升级是读锁升级为写锁。但是读锁可能有多个,升级为写锁需要等待所有读锁都释放,这就可能出现互相等待。

总结为:因为写锁只能一个线程持有,读锁可能多个线程持有。

1.3.8. StampedLock是什么?

是对ReentrantReadWriteLock的一种改进,主要改进为读锁如果没有竞争,不会加锁。底层原理是CLH队列,不具体讲了。

具体为三种情况:

  1. 悲观读锁:与ReadWriteLock的读锁类似,多个线程可以同时获取悲观读锁,悲观读锁是一个共享锁
  2. 乐观读锁:直接操作数据,不加任何锁
  3. 写锁:与ReadWriteLock的写锁类似,写锁和悲观读锁是互 斥的。

1.3.9. AQS的应用

ReentrantLock

tryAcquire()是CAS让state + 1,tryRelease()是CAS将state - 1,只有state = 0才有机会让其他线程获取到锁。

CountDownLatch

任务划分为N个子线程执行,对应于state的值。每个子线程执行完countDown()以后CAS使得state - 1,等待所有子线程执行完以后(state = 0),unpark()掉主线程,主线程从await()处唤醒继续执行。

Semaphore(信号量)

state是剩余的许可。如果 state >= 0 的话则获取成功。支持公平和非公平。

1.4. Atomic&ThreadLocal&CyclicBarrier

1.4.1. ThreadLocal是什么?用法和实现?

每个线程的变量副本

实现

每个Thread都有一个ThreadLocalMap变量,ThreadLocalMap的key是ThreadLocal,value是对应的值

ThreadLocalMap解决hash冲突的方式采用的是线性探测法,如果发生冲突会继续寻找下一个位置

ThreadLocal内存泄漏问题?怎么解决的?

原因是ThreadLocalMap中,entry是弱引用(防止不用的ThreadLocal占用内存),value是强引用。所以GC了就会出现key变为null的entry,导致内存泄漏。

解决是每次ThreadLocal用完后手动remove()

ThreadLocal的使用场景
  1. 线程间数据隔离
  2. 绑CPU,增加CPU的亲和性,避免了上下文切换刷新TLB的开销。(避免CPU切换的开销)
如果子进程要调用父进程的ThreadLocal怎么办?InheritableThreadLocal?怎么跨线程传递参数?

InheritableThreadLocal 的存储机制和 ThreadLocal 是一样的,区别在于复写了 createMap 和 getMap

他在 createMap 的时候会调用 ThreadLocalMap(parentMap) ,父线程的变量值赋值给子线程

1.4.2. Atomic是什么?用法?

基本类型:AtomicInteger、AtomicLong、AtomicBoolean、AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray、AtomicReference、AtomicMarkableReference、AtomicStampedReference

  1. AtomicXXXX:如AtomicInteger和AtomicLong,可以原子执行+1/-1操作,API如getAndIncrement(),getAndDecrement(),compareAndSet()
  2. AtomicXXXXFieldUpdater:可以无入侵的对类中的某个元素进行原子性的加减,但是要求被加减的元素必须是volatile的。API,new AtomicReference(),其他接口一样。
  3. AtomicXXXXArray:对各种数组的原子操作,针对某下标。API接口:getAndSet(int i, int newValue),getAndIncrement(int i),getAndDecrement(int i),compareAndSet(int i, int except, int update)
AtomicInteger原理
  1. 读数据的原理:使用getIntVolatile(Object obj, long offset)来获取对象的内存offset处的值。这是UnSafe包的原子操作。
  2. 修改数据原理:底层使用UnSafe包的本地方法(CPU原子操作)this.compareAndSwapInt来实现。
  3. 变量可见性:变量使用volatile修饰,保证可见性
Java里面哪里用到了CAS?
  1. Atomic/UnSafe类:AtomicInteger、AtomicLong、AtomicReference使用CPU的CAS原子操作
  2. ConcurrentHashMap接口:使用CAS实现线程安全的并发访问
  3. AQS:CAS操作实现对state的更新和线程等待唤醒
  4. 线程池:java的线程池通过CAS操作实现线程的启动和终止
CAS有什么问题?解决方案?
ABA问题

问题:初始50,改为100,又改为50

解决方案:

AtomicStampedReference以及AtomicMarkableReference支持两个变量上执行原子的条件更新,但大多数情况下没有必要解决这个问题。

循环时间开销大

问题:循环时间开销大,如果没有获取到目标值就会一直自旋重试

解决方案:设置threshold,到该值时就停止

LongAdder和AtomicLong有什么区别?LongAdder的区别?
实现
  1. AtomicLong底层依靠CAS来保证原子性的更新,但是在并发很高的时候会不断尝试,开销大
  2. LongAdder采用的是一个Cell数组,借鉴了ConcurrentHashMap的分段锁的思想,里面维护了一个volatile的变量base,在没有并发(指CAS成功)的时候只是base上加,当CAS失败了,就在cell数组中(没有则创建)计数,最后有个sum()函数会把所有的技术累加。(但是sum()的过程中不会阻塞计数,所以只是实现最终一致性

在这里插入图片描述

什么是CyclicBarrier?与CountDownLatch的区别?

CyclicBarrier也是一种线程同步工具,初始化时指定其容量大小,调用await()让计数-1并阻塞,当计数为0时释放线程

区别
  1. 是否可以复用:CyclicBarrier是可以复用的,如果减小为0,会reset。但是CountDownLatch不会复用,需要重新new一个
  2. 用途不同:CountDownLatch主要用于主线程等待其他线程完成任务,CyclicBarrier主要用于同步线程状态,当各个线程到达某个同步线程状态时再开始执行

1.4.3. 线程池

线程池创建?参数的含义?为什么不直接用executors?runnable的类型?

最重要的三个参数描述线程池的大小

  1. corePoolSize:核心线程的大小,创建线程池后不会立即启动。任务提交的时候才会启动线程(大于等于corePoolSize)
  2. workQueue:任务队列
  3. maximumPoolSize:最大线程数,核心线程数满且阻塞队列满且阻塞队列满的时候才会判断这个

两个用于空闲线程存活的时间相关的参数

  1. keepAliveTime:线程数大于核心数,多余的空闲线程最多的存活时间

  2. Unit:存活单位

剩下一个threadFactory,一个拒绝策略

  1. threadFactory:产生线程的factory,用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。

  2. rejectExecutionHandler:拒绝策略

    (1)AbortPolicy中止策略:抛出一个RejectedExecutionException异常

    (2)DiscardPolicy抛弃策略:放弃本次任务

    (3)DiscardOldestPolicy抛弃最旧的策略:抛弃最旧任务

    (4)CallerRunsPolicy调用者运行策略:让调用者运行任务

    (5)可以自定义拒绝策略,需要重写RejectedExecutionHandler的接口,重写其中唯一的方法rejectedExecution即可

executors是什么?为什么不用executors创建?
  1. FixedThreadPool:<1>核心线程数等于最大线程数<2>阻塞队列是无界的
  2. SingleThreadPool:<1>核心线程数和最大线程数固定为1<2>阻塞队列是无界的
  3. CachedThreadPool:<1>核心线程数为0,最大线程数是Integer.MAX_VALUE<2>阻塞队列为0
  4. ScheduledThreadPool:<1>支持定时和周期任务,使用的DelayedWorkQueue<2>最大线程数是Integer.MAX_VALUE
任务调度线程池是什么?与Timer的区别?

ScheduledThreadPoolExecutor本身是一个线程池,支持并发执行。其内部使用DelayedWorkQueue作为任务队列。

	1. Timer的优缺点?

​ Timer也是任务调度,Timer.schedule(TimerTask, delay),但是所有任务都由一个线程来调度,因此是串行执行的,前面的线程可能会影响后面的。

  1. ScheduledThreadPool与Timer的差别?

​ 单线程执行和多线程执行

  1. 任务调度线程池的常用方法

​ a. 延迟 schedule()

​ b. 延迟+固定频率 scheduleAtFixedRate()

// 初始延迟时间为1秒,固定频率为3秒
        executorService.scheduleAtFixedRate(() -> {
            // 执行任务逻辑
            System.out.println("任务执行时间:" + System.currentTimeMillis());
        }, 1, 3, TimeUnit.SECONDS);

​ b. 延迟+固定时延,时延是上一次任务终止到下一次任务开始 scheduleAtFixedDelay(),接口参数同scheduleAtFixedRate()

每种线程池可能OOM的原因?
  1. FixedThreadPool 和 SingleThreadExecutor : 允许阻塞队列的长度为为 Integer.MAX_VALUE,可能堆积⼤量的请求,从⽽导致OOM。

  2. CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE,可能会创建⼤量线程,从⽽导致OOM。

ForkJoin线程池是什么?

非常像MapReduce的一集,ForkJoinPool使用了分治的思想,将一个大任务拆分成多个小任务,并且在分治的基础上加入了多线程,可以把每个任务的分解Fork和合并Join交给不同的线程来完成。ForkJoinPool会创建和CPU核心线程数相同的线程池。

使用方法:

  1. 写一个任务类,继承自RecusiveTask类,泛型是返回值类型
  2. 重写RecusiveTask类的compute()方法,调用fork()进行计算,join()合并计算结果
  3. forkJoinPool调用invoke()方法传递任务对象,接受一个RecursiveTask类型任务对象

DEMO:CSDN自己查吧

线程数的设置

最大线程数的设置:

  1. CPU密集型:n+1。CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。+1是因为防止thread因为缺页等原因陷入中断。
  2. IO密集型:n*2,由于IO密集型任务的线程不会一直在执行任务,所以多分配一些线程数

核心线程数的设置:

核心线程数 = 最大线程数 * 20%

runnableQueue有几种类型?
  1. 无界:

​ LinkedBlockingQueue:<1>一个基于链表结构的阻塞队列<2>注意不要囤积大量的请求,否则会造成OOM<3>newFixedThreadPool()和newSingleThreadPool使用了这个队列

  1. 有界:

​ ArrayBlockingQueue:<1>是一个基于数组的有界阻塞队列。内部通过两个condition(notEmpty和notFull)+循环数组的方式实现。add()失败抛异常,put()失败阻塞,offer()失败返回false。poll()失败返回null,take()失败阻塞。

​ PriorityBlockingQueue:<1>一个具有优先级的无限阻塞队列,内部使用堆和自定义的比较器comparator来实现优先级比较<2>newSheduleThreadPool使用delayedWorkQueue

  1. 同步:

    SynchronousQueue:<1>队列大小为0。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态 <2> newCachedThreadPool使用了这个队列。

提交线程有哪些方法?
  1. execute()方法用于提交不需要返回值的任务
  2. submit()方法用于提交需要返回值的任务,线程池会返回一个Future类型对象(FutureTask),可以通过Future.get()阻塞获得返回值。
  3. invokeAll()方法接收一个Callable对象的集合,提交其中的所有任务
  4. invokeAny()方法也接收一个Callable对象的集合,哪个任务先执行完毕,返回此任务的结果,其他任务取消
execute()执行的流程
  1. 第一步判断是否小于corePoolSize,如果小于就创建线程(这一步要获取全局锁)
  2. 否则判断BlockedQueue是否满了,如果没满就加入BlockedQueue
public void execute(Runnable command) { 
    if (command == null) 
        throw new NullPointerException(); 
    // 如果线程数小于基本线程数,则创建线程并执行当前任务 
    if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) { 
    // 如线程数大于等于基本线程数或线程创建失败,则将当前任务放到工作队列中。 
    if (runState == RUNNING && workQueue.offer(command)) { 
        if (runState != RUNNING || poolSize == 0) 
            ensureQueuedTaskHandled(command); 
    }
    // 如果线程池不处于运行中或任务无法放入队列,并且当前线程数量小于最大允许的线程数量, 
    // 则创建一个线程执行任务。 
    else if (!addIfUnderMaximumPoolSize(command)) 
        // 抛出RejectedExecutionException异常 
        reject(command); // is shutdown or saturated } 
}
核心线程什么时候初始化?

默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才能创建线程。

如果想线程池创建后立即创建线程,可以通过以下两个方法:

prestartCoreThread():初始化一个核心线程

prestartAllCoreThread():初始化所有核心线程

如何获取Executor的报错?

如果是execute()方法,可以直接使用try-catch捕获异常

如果是submit(),可以使用future类的get方法,如果有异常则会返回报错信息

ThreadPoolExecutor是如何表示线程池状态的?

int使用高3位表示线程状态,低29位表示线程数,存在一个原子变量ctl,使用一个CAS操作赋值

线程池有五种状态:

  1. RUNNING状态:可以接收新任务,可以处理阻塞队列的任务
  2. SHUTDOWN状态:不可以接收新任务,但可以处理阻塞队列的任务
  3. STOP状态:中断正在执行的线程并抛弃阻塞队列的任务
  4. TIDYING状态:任务全部执行完毕,活动线程为0即将进入终结
  5. TERMINATED状态:终结状态

img

关闭线程池的两种方法/关闭线程池时如果有任务怎么办?
shutdown()

开始结束线程池,新的任务加入会被拒绝策略中止,但是还要等待任务队列中的所有任务执行完。

shutdownNow()

立即结束线程池,给每个正在执行的线程发送interrupt,并将所有任务队列中的任务通过一个list返回。