多线程面试题

前言

个人珍藏的80道Java多线程/并发经典面试题,现在给出11-20的答案解析哈,并且上传github哈~

https://github.com/whx123/JavaHome

个人珍藏的80道多线程并发面试题(1-10答案解析)

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

回答这些点:

为什么要用线程池?

Java的线程池原理

线程池核心参数

几种工作阻塞队列

线程池使用不当的问题

线程池类型以及使用场景

为什么要用线程池?

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

管理线程,避免增加创建线程和销毁线程的资源损耗。

提高响应速度。

重复利用。

Java的线程池执行原理

 为了形象描述线程池执行,打个比喻:

核心线程比作公司正式员工

非核心线程比作外包员工

阻塞队列比作需求池

提交任务比作提需求 

线程池核心参数

need-to-insert-img

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

如果在本线程内观察,所有的操作都是有序的;即不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不会被改变。

need-to-insert-img

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保证

need-to-insert-img

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


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

Callable接口方法是call(),Runnable的方法是run();

Callable接口call方法有返回值,支持泛型,Runnable接口run方法无返回值。

Callable接口call()方法允许抛出异常;而Runnable接口run()方法不能继续上抛异常;

need-to-insert-img

@FunctionalInterfacepublic interface Callable<V> {    /**    * 支持泛型V,有返回值,允许抛出异常    */    V call() throws Exception;}@FunctionalInterfacepublic interface Runnable {    /**    *  没有返回值,不能继续上抛异常    */    public abstract void run();}

看下demo代码吧,这样应该好理解一点哈~

need-to-insert-img

/* *  @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();    }}

运行结果:

need-to-insert-img

你好,callable你好呀,runnablenull

16、wait(),notify()和suspend(),resume()之间的区别

wait() 使得线程进入阻塞等待状态,并且释放锁

notify()唤醒一个处于等待状态的线程,它一般跟wait()方法配套使用。

suspend()使得线程进入阻塞状态,并且不会自动恢复,必须对应的resume() 被调用,才能使得线程重新进入可执行状态。suspend()方法很容易引起死锁问题。

resume()方法跟suspend()方法配套使用。

suspend()不建议使用,suspend()方法在调用后,线程不会释放已经占有的资 源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。


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

在《Java Concurrency in Practice》一书中,有一个评估线程池线程大小的公式

Nthreads=NcpuUcpu(1+w/c)

Ncpu = CPU总核数

Ucpu =cpu使用率,0~1

W/C=等待时间与计算时间的比率

假设cpu 100%运转,则公式为

need-to-insert-img

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执行权),在继续执行。**

代码如下:

need-to-insert-img

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();            }        }    }}

运行结果:

need-to-insert-img

春天来了: 1次春天来了: 2次春天来了: 3次夏天来了: 1次夏天来了: 2次夏天来了: 3次秋天来了: 1次秋天来了: 2次秋天来了: 3次

41. 公平锁非公平锁

这个是在ReentrankLock中实现的,synchronized没有,是用一个队列实现的,在公平锁好理解,就是先进这个队列的,也先出队列获得资源,而非公平锁的话,则是还没有进队列之前可以与队列中的线程竞争尝试获得锁,如果获取失败,则进队列,此时也是要乖乖等前面出队才行

44. 偏向锁轻量级锁重量级锁

在jdk1.6中做了第synchronized的优化,偏向锁指的是当前只有这个线程获得,没有发生争抢,此时将方法头的markword设置成0,然后每次过来都cas一下就好,不用重复的获取锁

轻量级锁:在偏向锁的基础上,有线程来争抢,此时膨胀为轻量级锁,多个线程获取锁时用cas自旋获取,而不是阻塞状态

重量级锁:轻量级锁自旋一定次数后,膨胀为重量级锁,其他线程阻塞,当获取锁线程释放锁后唤醒其他线程。(线程阻塞和唤醒比上下文切换的时间影响大的多,涉及到用户态和内核态的切换)

自旋锁:在没有获取锁的时候,不挂起而是不断轮询锁的状态

43.独享锁共享锁

共享锁可以由多个线程获取使用,而独享锁只能由一个线程获取。 对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。其中获得写锁的线程还能同时获得读锁,然后通过释放写锁来降级。读锁则不能升级

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容