前言
我的珍藏的80道Java多线程/并发经典面试题,如今给出11-20的答案解析哈,而且上传github哈~html
十一、为何要用线程池?Java的线程池内部机制,参数做用,几种工做阻塞队列,线程池类型以及使用场景
回答这些点:程序员
- 为何要用线程池?
- Java的线程池原理
- 线程池核心参数
- 几种工做阻塞队列
- 线程池使用不当的问题
- 线程池类型以及使用场景
为何要用线程池?
线程池:一个管理线程的池子。github
- 管理线程,避免增长建立线程和销毁线程的资源损耗。
- 提升响应速度。
- 重复利用。
Java的线程池执行原理
为了形象描述线程池执行,打个比喻:
面试
- 核心线程比做公司正式员工
- 非核心线程比做外包员工
- 阻塞队列比做需求池
- 提交任务比做提需求
线程池核心参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
- corePoolSize: 线程池核心线程数最大值
- maximumPoolSize: 线程池最大线程数大小
- keepAliveTime: 线程池中非核心线程空闲的存活时间大小
- unit: 线程空闲存活时间单位
- workQueue: 存听任务的阻塞队列
- threadFactory: 用于设置建立线程的工厂,能够给建立的线程设置有意义的名字,可方便排查问题。
- handler:线城池的饱和策略事件,主要有四种类型拒绝策略。
四种拒绝策略算法
- AbortPolicy(抛出一个异常,默认的)
- DiscardPolicy(直接丢弃任务)
- DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
- CallerRunsPolicy(交给线程池调用所在的线程进行处理)
几种工做阻塞队列
- ArrayBlockingQueue(用数组实现的有界阻塞队列,按FIFO排序量)
- LinkedBlockingQueue(基于链表结构的阻塞队列,按FIFO排序任务,容量能够选择进行设置,不设置的话,将是一个无边界的阻塞队列)
- DelayQueue(一个任务定时周期的延迟执行的队列)
- PriorityBlockingQueue(具备优先级的无界阻塞队列)
- SynchronousQueue(一个不存储元素的阻塞队列,每一个插入操做必须等到另外一个线程调用移除操做,不然插入操做一直处于阻塞状态)
线程池使用不当的问题
线程池适用不当可能致使内存飙升问题哦spring
有兴趣能够看我这篇文章哈:源码角度分析-newFixedThreadPool线程池致使的内存飙升问题数据库
线程池类型以及使用场景
- newFixedThreadPool
适用于处理CPU密集型的任务,确保CPU在长期被工做线程使用的状况下,尽量的少的分配线程,即适用执行长期的任务。编程
- newCachedThreadPool
用于并发执行大量短时间的小任务。
- newSingleThreadExecutor
适用于串行执行任务的场景,一个任务一个任务地执行。
- newScheduledThreadPool
周期性执行任务的场景,须要限制线程数量的场景
- newWorkStealingPool
建一个含有足够多线程的线程池,来维持相应的并行级别,它会经过工做窃取的方式,使得多核的 CPU 不会闲置,总会有活着的线程让 CPU 去运行,本质上就是一个 ForkJoinPool。)
有兴趣能够看我这篇文章哈:面试必备:Java线程池解析
十二、谈谈volatile关键字的理解
volatile是面试官很是喜欢问的一个问题,能够回答如下这几点:
- vlatile变量的做用
- 现代计算机的内存模型(嗅探技术,MESI协议,总线)
- Java内存模型(JMM)
- 什么是可见性?
- 指令重排序
- volatile的内存语义
- as-if-serial
- Happens-before
- volatile能够解决原子性嘛?为何?
- volatile底层原理,如何保证可见性和禁止指令重排(内存屏障)
vlatile变量的做用?
- 保证变量对全部线程可见性
- 禁止指令重排
现代计算机的内存模型
- 其中高速缓存包括L1,L2,L3缓存~
- 缓存一致性协议,能够了解MESI协议
- 总线(Bus)是计算机各类功能部件之间传送信息的公共通讯干线,CPU和其余功能部件是经过总线通讯的。
- 处理器使用嗅探技术保证它的内部缓存、系统内存和其余处理器的缓存数据在总线上保持一致。
Java内存模型(JMM)
什么是可见性?
可见性就是当一个线程 修改一个共享变量时,另一个线程能读到这个修改的值。
指令重排序
指令重排是指在程序执行过程当中,为了提升性能, 编译器和CPU可能会对指令进行从新排序。
volatile的内存语义
- 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。
- 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
as-if-serial
若是在本线程内观察,全部的操做都是有序的;即无论怎么重排序(编译器和处理器为了提升并行度),(单线程)程序的执行结果不会被改变。
double pi = 3.14; //A double r = 1.0; //B double area = pi * r * r; //C
步骤C依赖于步骤A和B,由于指令重排的存在,程序执行顺讯多是A->B->C,也多是B->A->C,可是C不能在A或者B前面执行,这将违反as-if-serial语义。
Happens-before
Java语言中,有一个先行发生原则(happens-before):
- 程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操做先行发生于书写在后面的操做。
- 管程锁定规则:一个unLock操做先行发生于后面对同一个锁额lock操做
- volatile变量规则:对一个变量的写操做先行发生于后面对这个变量的读操做
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个一个动做
- 线程终止规则:线程中全部的操做都先行发生于线程的终止检测,咱们能够经过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
- 传递性:若是操做A先行发生于操做B,而操做B又先行发生于操做C,则能够得出操做A先行发生于操做C
volatile能够解决原子性嘛?为何?
不能够,能够直接举i++那个例子,原子性须要synchronzied或者lock保证
public class Test { public volatile int race = 0; public void increase() { race++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<100;j++) test.increase(); }; }.start(); } //等待全部累加线程结束 while(Thread.activeCount()>1) Thread.yield(); System.out.println(test.race); } }
volatile底层原理,如何保证可见性和禁止指令重排(内存屏障)
volatile 修饰的变量,转成汇编代码,会发现多出一个lock前缀指令。lock指令至关于一个内存屏障,它保证如下这几点:
- 1.重排序时不能把后面的指令重排序到内存屏障以前的位置
- 2.将本处理器的缓存写入内存
- 3.若是是写入动做,会致使其余处理器中对应的缓存无效。
二、3点保证可见性,第1点禁止指令重排~
有兴趣的朋友能够看我这篇文章哈:Java程序员面试必备:Volatile全方位解析
1三、AQS组件,实现原理
AQS,即AbstractQueuedSynchronizer,是构建锁或者其余同步组件的基础框架,它使用了一个int成员变量表示同步状态,经过内置的FIFO队列来完成资源获取线程的排队工做。能够回答如下这几个关键点哈:
- state 状态的维护。
- CLH队列
- ConditionObject通知
- 模板方法设计模式
- 独占与共享模式。
- 自定义同步器。
- AQS全家桶的一些延伸,如:ReentrantLock等。
state 状态的维护
- state,int变量,锁的状态,用volatile修饰,保证多线程中的可见性。
- getState()和setState()方法采用final修饰,限制AQS的子类重写它们两。
- compareAndSetState()方法采用乐观锁思想的CAS算法操做确保线程安全,保证状态
设置的原子性。
对CAS有兴趣的朋友,能够看下我这篇文章哈~
CAS乐观锁解决并发问题的一次实践
CLH队列
CLH(Craig, Landin, and Hagersten locks) 同步队列 是一个FIFO双向队列,其内部经过节点head和tail记录队首和队尾元素,队列元素的类型为Node。AQS依赖它来完成同步状态state的管理,当前线程若是获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构形成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
ConditionObject通知
咱们都知道,synchronized控制同步的时候,能够配合Object的wait()、notify(),notifyAll() 系列方法能够实现等待/通知模式。而Lock呢?它提供了条件Condition接口,配合await(),signal(),signalAll() 等方法也能够实现等待/通知机制。ConditionObject实现了Condition接口,给AQS提供条件变量的支持 。
ConditionObject队列与CLH队列的爱恨情仇:
- 调用了await()方法的线程,会被加入到conditionObject等待队列中,而且唤醒CLH队列中head节点的下一个节点。
- 线程在某个ConditionObject对象上调用了singnal()方法后,等待队列中的firstWaiter会被加入到AQS的CLH队列中,等待被唤醒。
- 当线程调用unLock()方法释放锁时,CLH队列中的head节点的下一个节点(在本例中是firtWaiter),会被唤醒。
模板方法设计模式
什么是模板设计模式?
在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类能够在不改变算法结构的状况下,从新定义算法中的某些步骤。
AQS的典型设计模式就是模板方法设计模式啦。AQS全家桶(ReentrantLock,Semaphore)的衍生实现,就体现出这个设计模式。如AQS提供tryAcquire,tryAcquireShared等模板方法,给子类实现自定义的同步器。
独占与共享模式
- 独占式: 同一时刻仅有一个线程持有同步状态,如ReentrantLock。又可分为公平锁和非公平锁。
- 共享模式:多个线程可同时执行,如Semaphore/CountDownLatch等都是共享式的产物。
自定义同步器
你要实现自定义锁的话,首先须要肯定你要实现的是独占锁仍是共享锁,定义原子变量state的含义,再定义一个内部类去继承AQS,重写对应的模板方法便可啦
AQS全家桶的一些延伸。
Semaphore,CountDownLatch,ReentrantLock
能够看下以前我这篇文章哈,AQS解析与实战
1四、什么是多线程环境下的伪共享
- 什么是伪共享
- 如何解决伪共享问题
什么是伪共享
伪共享定义?
CPU的缓存是以缓存行(cache line)为单位进行缓存的,当多个线程修改相互独立的变量,而这些变量又处于同一个缓存行时就会影响彼此的性能。这就是伪共享
现代计算机计算模型,你们都有印象吧?我以前这篇文章也有讲过,有兴趣能够看一下哈,Java程序员面试必备:Volatile全方位解析
- CPU执行速度比内存速度快好几个数量级,为了提升执行效率,现代计算机模型演变出CPU、缓存(L1,L2,L3),内存的模型。
- CPU执行运算时,如先从L1缓存查询数据,找不到再去L2缓存找,依次类推,直到在内存获取到数据。
- 为了不频繁从内存获取数据,聪明的科学家设计出缓存行,缓存行大小为64字节。
也正是由于缓存行,就致使伪共享问题的存在,如图所示:
假设数据a、b被加载到同一个缓存行。
- 当线程1修改了a的值,这时候CPU1就会通知其余CPU核,当前缓存行(Cache line)已经失效。
- 这时候,若是线程2发起修改b,由于缓存行已经失效了,因此core2 这时会从新从主内存中读取该 Cache line 数据。读完后,由于它要修改b的值,那么CPU2就通知其余CPU核,当前缓存行(Cache line)又已经失效。
- 酱紫,若是同一个Cache line的内容被多个线程读写,就很容易产生相互竞争,频繁回写主内存,会大大下降性能。
如何解决伪共享问题
既然伪共享是由于相互独立的变量存储到相同的Cache line致使的,一个缓存行大小是64字节。那么,咱们就能够使用空间换时间,即数据填充的方式,把独立的变量分散到不一样的Cache line~
共享内存demo例子:
public class FalseShareTest { public static void main(String[] args) throws InterruptedException { Rectangle rectangle = new Rectangle(); long beginTime = System.currentTimeMillis(); Thread thread1 = new Thread(() -> { for (int i = 0; i < 100000000; i++) { rectangle.a = rectangle.a + 1; } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 100000000; i++) { rectangle.b = rectangle.b + 1; } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("执行时间" + (System.currentTimeMillis() - beginTime)); } } class Rectangle { volatile long a; volatile long b; }
运行结果:
执行时间2815
一个long类型是8字节,咱们在变量a和b之间不上7个long类型变量呢,输出结果是啥呢?以下:
class Rectangle { volatile long a; long a1,a2,a3,a4,a5,a6,a7; volatile long b; }
运行结果:
执行时间1113
能够发现利用填充数据的方式,让读写的变量分割到不一样缓存行,能够很好挺高性能~
1五、 说一下 Runnable和 Callable有什么区别?
- Callable接口方法是call(),Runnable的方法是run();
- Callable接口call方法有返回值,支持泛型,Runnable接口run方法无返回值。
- Callable接口call()方法容许抛出异常;而Runnable接口run()方法不能继续上抛异常;
@FunctionalInterface public interface Callable<V> { /** * 支持泛型V,有返回值,容许抛出异常 */ V call() throws Exception; } @FunctionalInterface public interface Runnable { /** * 没有返回值,不能继续上抛异常 */ public abstract void run(); }
看下demo代码吧,这样应该好理解一点哈~
/* * @Author 捡田螺的小男孩 * @date 2020-08-18 */ public class CallableRunnableTest { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(5); Callable <String> callable =new Callable<String>() { @Override public String call() throws Exception { return "你好,callable"; } }; //支持泛型 Future<String> futureCallable = executorService.submit(callable); try { System.out.println(futureCallable.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } Runnable runnable = new Runnable() { @Override public void run() { System.out.println("你好呀,runnable"); } }; Future<?> futureRunnable = executorService.submit(runnable); try { System.out.println(futureRunnable.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } executorService.shutdown(); } }
运行结果:
你好,callable 你好呀,runnable null
1六、wait(),notify()和suspend(),resume()之间的区别
- wait() 使得线程进入阻塞等待状态,而且释放锁
- notify()唤醒一个处于等待状态的线程,它通常跟wait()方法配套使用。
- suspend()使得线程进入阻塞状态,而且不会自动恢复,必须对应的resume() 被调用,才能使得线程从新进入可执行状态。suspend()方法很容易引发死锁问题。
- resume()方法跟suspend()方法配套使用。
suspend()不建议使用,suspend()方法在调用后,线程不会释放已经占有的资 源(好比锁),而是占有着资源进入睡眠状态,这样容易引起死锁问题。
17.Condition接口及其实现原理
- Condition接口与Object监视器方法对比
- Condition接口使用demo
- Condition实现原理
Condition接口与Object监视器方法对比
Java对象(Object),提供wait()、notify(),notifyAll() 系列方法,配合synchronized,能够实现等待/通知模式。而Condition接口配合Lock,经过await(),signal(),signalAll() 等方法,也能够实现相似的等待/通知机制。
对比项 | 对象监视方法 | Condition |
---|---|---|
前置条件 | 得到对象的锁 | 调用Lock.lock()获取锁,调用Lock.newCondition()得到Condition对象 |
调用方式 | 直接调用,object.wait() | 直接调用,condition.await() |
等待队列数 | 1个 | 多个 |
当前线程释放锁并进入等待状态 | 支持 | 支持 |
在等待状态中不响应中断 | 不支持 | 支持 |
当前线程释放锁并进入超时等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态到未来的某个时间 | 不支持 | 支持 |
唤醒等待队列中的一个线程 | 支持 | 支持 |
唤醒等待队列中的所有线程 | 支持 | 支持 |
Condition接口使用demo
public class ConditionTest { Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); public void conditionWait() throws InterruptedException { lock.lock(); try { condition.await(); } finally { lock.unlock(); } } public void conditionSignal() throws InterruptedException { lock.lock(); try { condition.signal(); } finally { lock.unlock(); } } }
Condition实现原理
其实,同步队列和等待队列中节点类型都是同步器的静态内部类 AbstractQueuedSynchronizer.Node,接下来咱们图解一下Condition的实现原理~
等待队列的基本结构图
一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点 (lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队
AQS 结构图
ConditionI是跟Lock一块儿结合使用的,底层跟同步器(AQS)相关。同步器拥有一个同步队列和多个等待队列~
等待
当调用await()方法时,至关于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。
通知
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在
唤醒节点以前,会将节点移到同步队列中。
1八、线程池如何调优,最大数目如何确认?
在《Java Concurrency in Practice》一书中,有一个评估线程池线程大小的公式
Nthreads=NcpuUcpu(1+w/c)
- Ncpu = CPU总核数
- Ucpu =cpu使用率,0~1
- W/C=等待时间与计算时间的比率
假设cpu 100%运转,则公式为
Nthreads=Ncpu*(1+w/c)
估算的话,酱紫:
- 若是是IO密集型应用(如数据库数据交互、文件上传下载、网络数据传输等等),IO操做通常比较耗时,等待时间与计算时间的比率(w/c)会大于1,因此最佳线程数估计就是 Nthreads=Ncpu*(1+1)= 2Ncpu 。
- 若是是CPU密集型应用(如算法比较复杂的程序),最理想的状况,没有等待,w=0,Nthreads=Ncpu。又对于计算密集型的任务,在拥有N个处理器的系统上,当线程池的大小为N+1时,一般能实现最优的效率。因此 Nthreads = Ncpu+1
有具体指参考呢?举个例子
好比平均每一个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,好比IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算获得:线程池大小=(1+1.5/05)*8 =32。
参考了网上这篇文章,写得很棒,有兴趣的朋友能够去看一下哈:
1九、 假设有T一、T二、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
可使用join方法解决这个问题。好比在线程A中,调用线程B的join方法表示的意思就是:A等待B线程执行完毕后(释放CPU执行权),在继续执行。
代码以下:
public class ThreadTest { public static void main(String[] args) { Thread spring = new Thread(new SeasonThreadTask("春天")); Thread summer = new Thread(new SeasonThreadTask("夏天")); Thread autumn = new Thread(new SeasonThreadTask("秋天")); try { //春天线程先启动 spring.start(); //主线程等待线程spring执行完,再往下执行 spring.join(); //夏天线程再启动 summer.start(); //主线程等待线程summer执行完,再往下执行 summer.join(); //秋天线程最后启动 autumn.start(); //主线程等待线程autumn执行完,再往下执行 autumn.join(); } catch (InterruptedException e) { e.printStackTrace(); } } } class SeasonThreadTask implements Runnable{ private String name; public SeasonThreadTask(String name){ this.name = name; } @Override public void run() { for (int i = 1; i <4; i++) { System.out.println(this.name + "来了: " + i + "次"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
运行结果:
春天来了: 1次 春天来了: 2次 春天来了: 3次 夏天来了: 1次 夏天来了: 2次 夏天来了: 3次 秋天来了: 1次 秋天来了: 2次 秋天来了: 3次
20. LockSupport做用是?
- LockSupport做用
- park和unpark,与wait,notify的区别
- Object blocker做用?
LockSupport是个工具类,它的主要做用是挂起和唤醒线程, 该工具类是建立锁和其余同步类的基础。
public static void park(); //挂起当前线程,调用unpark(Thread thread)或者当前线程被中断,才能从park方法返回 public static void parkNanos(Object blocker, long nanos); // 挂起当前线程,有超时时间的限制 public static void parkUntil(Object blocker, long deadline); // 挂起当前线程,直到某个时间 public static void park(Object blocker); //挂起当前线程 public static void unpark(Thread thread); // 唤醒当前thread线程
看个例子吧:
public class LockSupportTest { public static void main(String[] args) { CarThread carThread = new CarThread(); carThread.setName("劳斯劳斯"); carThread.start(); try { Thread.currentThread().sleep(2000); carThread.park(); Thread.currentThread().sleep(2000); carThread.unPark(); } catch (InterruptedException e) { e.printStackTrace(); } } static class CarThread extends Thread{ private boolean isStop = false; @Override public void run() { System.out.println(this.getName() + "正在行驶中"); while (true) { if (isStop) { System.out.println(this.getName() + "车停下来了"); LockSupport.park(); //挂起当前线程 } System.out.println(this.getName() + "车还在正常跑"); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } } public void park() { isStop = true; System.out.println("停车啦,检查酒驾"); } public void unPark(){ isStop = false; LockSupport.unpark(this); //唤醒当前线程 System.out.println("老哥你没酒驾,继续开吧"); } } }
运行结果:
劳斯劳斯正在行驶中 劳斯劳斯车还在正常跑 劳斯劳斯车还在正常跑 停车啦,检查酒驾 劳斯劳斯车停下来了 老哥你没酒驾,继续开吧 劳斯劳斯车还在正常跑 劳斯劳斯车还在正常跑 劳斯劳斯车还在正常跑 劳斯劳斯车还在正常跑 劳斯劳斯车还在正常跑 劳斯劳斯车还在正常跑
LockSupport的park和unpark的实现,有点相似wait和notify的功能。可是
- park不须要获取对象锁
- 中断的时候park不会抛出InterruptedException异常,须要在park以后自行判断中断状态
- 使用park和unpark的时候,能够不用担忧park的时序问题形成死锁
- LockSupport不须要在同步代码块里
- unpark却能够唤醒一个指定的线程,notify只能随机选择一个线程唤醒
Object blocker做用?
方便在线程dump的时候看到具体的阻塞对象的信息。