前言

集体收藏的80道Java多线程/并发经典面试题,当初给出11-20的答案解析哈,并且上传github哈~

https://github.com/whx123/Jav...

集体收藏的80道多线程并发面试题(1-10答案解析)

11、为什么要用线程池?Java的线程池外部机制,参数作用,几种工作阻塞队列,线程池类型以及应用场景

答复这些点:

  • 为什么要用线程池?
  • Java的线程池原理
  • 线程池外围参数
  • 几种工作阻塞队列
  • 线程池使用不当的问题
  • 线程池类型以及应用场景

为什么要用线程池?

线程池:一个治理线程的池子。

  • 治理线程,防止减少创立线程和销毁线程的资源损耗。
  • 进步响应速度。
  • 反复利用。

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(一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作始终处于阻塞状态)

线程池使用不当的问题

线程池实用不当可能导致内存飙升问题哦

有趣味能够看我这篇文章哈:源码角度剖析-newFixedThreadPool线程池导致的内存飙升问题

线程池类型以及应用场景

  • newFixedThreadPool
实用于解决CPU密集型的工作,确保CPU在长期被工作线程应用的状况下,尽可能的少的调配线程,即实用执行长期的工作。
  • newCachedThreadPool
用于并发执行大量短期的小工作。
  • newSingleThreadExecutor
实用于串行执行工作的场景,一个工作一个工作地执行。
  • newScheduledThreadPool
周期性执行工作的场景,须要限度线程数量的场景
  • newWorkStealingPool
建一个含有足够多线程的线程池,来维持相应的并行级别,它会通过工作窃取的形式,使得多核的 CPU 不会闲置,总会有活着的线程让 CPU 去运行,实质上就是一个 ForkJoinPool。)

有趣味能够看我这篇文章哈:面试必备:Java线程池解析

12、谈谈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;    //Adouble r   = 1.0;     //Bdouble 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.如果是写入动作,会导致其余处理器中对应的缓存有效。

2、3点保障可见性,第1点禁止指令重排~

有趣味的敌人能够看我这篇文章哈:Java程序员面试必备:Volatile全方位解析

13、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解析与实战

14、什么是多线程环境下的伪共享

  • 什么是伪共享
  • 如何解决伪共享问题

什么是伪共享

伪共享定义?

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

能够发现利用填充数据的形式,让读写的变量宰割到不同缓存行,能够很好挺高性能~

15、 说一下 Runnable和 Callable有什么区别?

  • Callable接口办法是call(),Runnable的办法是run();
  • Callable接口call办法有返回值,反对泛型,Runnable接口run办法无返回值。
  • Callable接口call()办法容许抛出异样;而Runnable接口run()办法不能持续上抛异样;
@FunctionalInterfacepublic interface Callable<V> {    /**     * 反对泛型V,有返回值,容许抛出异样     */    V call() throws Exception;}@FunctionalInterfacepublic 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你好呀,runnablenull

16、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()办法,将会唤醒在期待队列中等待时间最长的节点(首节点),在
唤醒节点之前,会将节点移到同步队列中。

18、线程池如何调优,最大数目如何确认?

在《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。

参考了网上这篇文章,写得很棒,有趣味的敌人能够去看一下哈:

  • 依据CPU外围数确定线程池并发线程数

19、 假如有T1、T2、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的时候看到具体的阻塞对象的信息。

公众号

参考与感激

  • 《java并发编程的艺术》
  • 杂谈 什么是伪共享(false sharing)?
  • 依据CPU外围数确定线程池并发线程数
  • LockSupport的用法及原理
  • 探讨缓存行与伪共享