大厂面试题-Java并发编程基础篇(四)

一、讲下线程池的线程回收

这个问题从3个方面来回答

,线程池里面分为核心线程和非核心线程。

核心线程是常驻在线程池里面的工作线程,它有两种方式初始化:

    1、向线程池里面添加任务的时候,被动初始化

    2、主动调用prestartAllCoreThreads方法

当线程池里面的队列满了的情况下,为了增加线程池的任务处理能力。

线程池会增加非核心线程。

核心线程和非核心线程的数量,是在构造线程池的时候设置的,也可以动态进行更改。

由于非核心线程是为了解决任务过多的时候临时增加的,所以当任务处理完成后,工作线程处于空闲状态的时候,就需要回收。

因为所有工作线程都是从阻塞队列中去获取要执行的任务,所以只要在一定时间内,阻塞队列没有任何可以处理的任务,那这个线程就可以结束了。

这个功能是通过阻塞队列里面的poll方法来完成的。这个方法提供了超时时间和超时时间单位这两个参数当超过指定时间没有获取到任务的时候,poll方法返回null,从而终止当前线程完成线程回收。

默认况下,线程池只会回收非核心线程,如果希望核心线程也要回收,可以设置allowCoreThreadTimeOut这个属性为true,一般情况下我们不会去回收核心线

为线程池本身就是实现线程的复用,而且这些核心线程在没有任务要处理的时候是处于阻塞状态并没有占用CPU资源。

二、如果一个线程两次调用start(),会出现什么问题?

在Java里面,一个线程只能调用一次start()方法,第二次调用会抛出IllegalThreadStateException

一个线程本身是具备一个生命周期的。

在Java里面,线程的生命周期包括6种状态

    1、NEW,线程被创建还没有调用start启动

    2、RUNNABLE,在这个状态下的线程有可能是正在运行,也可能是在就绪队列里面等待操作系统进行调度分配CPU资源。

    3、BLOCKED,线程处于锁等待状态

    4、WAITING示线程处于条件等待状态,当触发条件后唤醒,比如wait/notify。

    5、TIMED_WAIT,和WAITING状态相同,只是它多了一个超时条件触发

    6、TERMINATED,表示线程执行结束

当我们第一次调用start()方法的时候,线程的状态可能处于终止或者非NEW状态下的其他状态。

再调用一start(),相当于让这个正在运行的线程重新运行,不管从线程的安全性角度,从线程本身的执行逻辑,都是不合理的。

因此为了避免这个问题,在线程运行的时候会先判断当前线程的运行状态。

三、Java官方提供了哪几种线程池,分别有什么特点?

JDK中幕刃提供了5中不同线程池的创建方式,下面分别说一下每一种线程池以及它的特点:

   1、newCachedThreadPool,是一种可以缓存的线程池,它可以用来处理大量短期的突发流量

它的特点有三个,最大线程数是Integer.MaxValue,线程存活时间是60秒,塞队列用的是SynchronousQueue,这是一种不存才任何元素的阻塞队列,也就是每提交一个任务给到线程池,都会分配一个工作线程来处理,由于最大线程数没有限制。

所以它可以处理大量的任务,另外每个工作线程又可以存活60s,使得这些工作线程可以缓存起来对更多任务的处理。

    2、newFixedThreadPool,是一种固定线程数量的线程池。它的特点是核心线程和最大线程数量都是一个固定的值如果任务比较多工作线程处理不过来 ,就会加入到阻塞队列里面等待。

    3、newSingleThreadExecutor,只有一个工作线程的线程池。

并且线程数量无法动态更改,因此可以保证所有的任务都按照FIFO的方式顺序执行。

    4、newScheduledThreadPool,具有延迟执行功能的线程池可以用它来实现定时调度

    5、newWorkStealingPool,Java8里面新加入的一个线程池它内部会构建一个ForkJoinPool,利用工作窃取的算法并行处理请求。

这些线程都是通过工具类Executors来构建的,线程池的最终实现类是ThreadPoolExecutor

四、请你说一下你对Happens-Before 的理解

这个问题需要从几个方面来回答

首先,Happens-Before是一种可见性模型,也就是说,在多线程环境下。

原本因为指令重排序的存在会导致数据的可见性问题,也就是A线程修改某个共享变量B线程不可见。

因此JMM通过Happens-Before关系向开发人员提供跨越线程的内存可见性保证。如果一个操作的执行结果对另外一个操作可见,那么这两个操作之间必然Happens-Before管理。

其次Happens-Before关系只是描述结果的可见性,并不表示指令执行的先后顺序,也就是说只要不对结果产生影响,仍然允许指令的重排序

最后,在JMM中存在很多的Happens-Before规则:

    1、程序顺序规则,一个线程中的每个操作,happens-before这个线程中的任意后续操作,可以简单认为是as-if-serial也就是不管怎么重排序,单线程的程序的执行结果不能改变

    2、传递规则(如图),也就是A Happens-Before B,  B Happens-Before C。 就可以推导出 A Happens-Before C

    3、volatile变量规则,对一个volatile修饰的变量的写一定happens-before于任意后续对这个volatile变量的读操作

    4、监视器锁规则(如图),一个线程对于一个锁的释放锁操作,一定happens-before与后续线程对这个锁的加锁操作

这个场景中,如果线程A获得了锁并且把x修改成了12,那么后续的线程获得锁之得到的x的值一定是12.

    5、线程启动规则(如图),如果线程A执行操作ThreadB.start(),那么线程A的ThreadB.start()之前的操作happens-before线程B中的任意操作。

在这样一个场景中,t1线程启动之前对于x=10的赋值操作,t1线程启动以后读取x的值一定是10.

   6、join 规则  (如图)  ,如果线程 A 执行操作 ThreadB.join()并成功返回,

那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作成功的返 

五、线程池是如何实现线程复用的

线程池里面采用了生产者消费者的模式 ,来实现线程复用。

生产者消费者模型 ,其实就是通过一个中间容器来解耦生产者和消费者的任务处理过 

生产者不断生产任务保存到容器 ,消费者不断从容器中消费任务。

在线程池里面 ,因需要保证工作线程的重复使用,

并且这些线程应该是有任务的时候执行 ,没任务的时候等待并释放 CPU 资源。

因此  (如图)  ,它使用了阻塞队列来实现这样一个需求。

提交任务到线程池里面的线程称为生产者线程 ,它不断往线程池里面传递任务。

这些任务会保存到线程池的阻塞队列里面。

然后线程池里面的工作线程不断从阻塞队列获取任务去执行。

基于阻塞队列的特性,使得阻塞队列中如果没有任务的时候,这些工作线程就会阻塞等 待。

直到又有新的任务进来 ,这些工作线程再次被唤醒从而达到线程用的目的

六、可以说下阻塞队列被异步消费怎么保持顺序吗?

这个问题从三个方面来回答

首先 ,阻塞队列本身是符合 FIFO 特性的队列 ,也就是存储进去的元素符合先进先出的规则。

其次 ,在阻塞队列里面 ,使用了 condition 条件等待来维护了两个等待队列  (如图)  , 一个是队列为空的时候存储被阻塞的消费者另一个是队列满了的时候存储被阻塞的生产者且存储在等待队列里面的线程 ,都符合 FIFO 的特性。

最后 对于阻塞队列的消费过程 ,有两种情况。

    第一种,就是阻塞队列里面已经包含了很多任务,这个时候启动多个消费者去消费的时候,它的有序性保证是通过加锁来实现的,也就是每个消费者线程去阻塞队列获取任务的时 候必须要先获得排他锁。

    第二种,如果有多个消费者线程因为阻塞队列中没有任务而阻塞,这个时候这些线程是按照 FIFO 的顺序存储到condition 条件等待队列中的。    当阻塞队列中开始有任务要处理的时候 ,这些被阻塞 的消费者线程会严格按照 FIFO 的顺序来唤醒 ,从而保证了消费的顺序型。

七、当任务数超过线程池的核心线程数时 ,如何让它不进入队列,而是直接启用最大线程数?

当我们提交一个任务线程池的时候 ,它的工作原理分为四步

    第一步 ,预热核心线程

    第二 ,把任务添加到阻塞队列

    第三步 ,如果添加到阻塞队列失败 ,则创建非核心线程增加处理效率

    第四步 ,如果非核心线程数达到了阈值 ,就触发拒绝策略

所以 ,如果希望这个任务不进入队列 ,那么只需要去影响第二步的执行逻辑就行了。

Java 线程池提供的构造方法里面 ,有一个参数可以修改阻塞队列的类型。

其中,就有一个阻塞队列叫SynchronousQueue(如图),这个队列不能存储任何元

它的特性是,生产一个任务,就必须要指派一个消费者来处理,否则就会阻塞生产者。

基于这个特性 ,只要把线程池的阻塞队列替换成 SynchronousQueue

就能够避免任务进入到阻塞队列 ,而是直接启动最大线程数去处理这个任务。

八、SimpleDateFormat 是线程安全的吗? 为什么?

SimpleDateFormat 不是线程安全的,SimpleDateFormat 类内部有一个 Calendar 对象引用,它用来储存和这个 SimpleDateFormat 相关的日期信息。

当我们把 SimpleDateFormat 作为多个线程的共享资源来使用的时候。

意味着多个线程会共享 SimpleDateFormat 里面的 Calendar 引用,多个线程对于同一个 Calendar 的操作,会出现数据脏读现象导致一些不可预料的错误。 在实际应用中 ,有 4 种方法可以解决这个问题

    第一种 ,把 SimpleDateFormat 定义成局部变量 ,每个线程调用的时候都创建一个新的实例。

    第二种 ,使用 ThreadLocal 工具 ,把 SimpleDateFormat 变成线程私有的

    第三种 ,加同步锁 ,在同一时刻只允许一个线程操作 SimpleDateFormat

    第四种 ,在 Java8 里面引入了一些线程安全的日期 API ,比如 LocalDateTimer、DateTimeFormatter 等。

九、并行和并发有什么区别?

题解析

并行和并发最早其实描述的是Java并发编程里面的概念。

他们强调的是CPU处理任务的能力。

简单来说,并发,就是同一个时刻,CPU能够处理的任务数量,并且对于应用程序来说,不会出现卡顿现象。

并行就是同一个时刻,允许多个任务同时执行,在多核CPU架构中,同时执行的任务数量是由核心数决定的,比如在4核4线程的CPU中,只能同时执行4个线程。

这两个概念看起来类似,但其实描述的纬度是不同的,并发描述的是程序处理能力的视并行描述的是CPU处理任务方式的视角,一个是宏观层面,一个是微观层面。

他们两个又是相辅相成的,CPU并行执行任务的能力,又能提升程序的并发处理性能。所以多核CPU的性能要比单核CPU好。

当然,如果是单核CPU,也可以通过时间片切换的方式提升并发能力。

(如图)Erlang之父JoeArmstrong用了一张图片解释了并行和并发的区别。

并发就是两个队列交替使用一台咖啡机,并行是两个队列同时使用两台咖啡

所以,这个面试题可以很好的考察求职者Java并发编程的理解程度。

网上很多的文章都在尝试解释这个概念,但是这些解释反而让这个问题越来越复杂。只有对线程的底层原理有深度理解,才能很好的回答这个问题。

回答

并行和并发是Java并发编程里面的概念。

并行,是指在多核CPU架构下,同一时刻同时可以执行多个线程的能力。

在单核CPU架构中,同一时刻只能运行一个线程。

4核4线程的CPU架构中,同一时刻可以运行4个线程,那这4个线程就是并行执行的

并发,是指在同一时刻CPU能够处理的任务数量,也可以理解成CPU的并发能力。在单核CPU架构中,操作系统通过CPU时间片机制提升CPU的并发能力核CPU架构中,基于任务的并行执行能力以及CPU时间片切换的能力来提升CPU的并发能力

所以,总的来说,并发是一个宏观概念,它指的是CPU能够承载的压力大小,并行是一个微观概念,它描述CPU同时执行多个任务的能力。

十、如何解决死锁问题?

考察目

这个问题还是有一点难度,首先他考察的是并发编程相关领域的知识。

其次,对于死锁这个问题,平时我们遇到得比较少,即便是看过相关的问题,也不一定能够记住所以,在一定程度上,能够回答清楚这个问题,说明这个求职者的基本功还不错。

题解析

死锁,就是两个或者两个以上的线程在执行过程中,去争夺同一个共享资源导致互相等待的现象。

在没有外部干预的情况下,线程会一直处于阻塞状态,无法往下执行。

不过,要想真正产生死锁,必须同时满足四个条件

    1、互斥条件,共享资源x和y只能被一个线程占用

    2、请求和保持条件,线程t1已经获取共享资源x,在等待共享资源y的时候,不释放共享资源x

    3、不可抢占条件,其他线程不能强行抢占线程t1占有的资源

    4、循环等待条件,线程t1等到线程t2占有的资源,线程t2等待线程t1占有的资源,形成循环等待

线程在产生死锁以后,只能通过外部干预来解决,比如重启、或者kill线程等。

所以我们在写码的时候,就应该去刻意规避死锁的问题。

也就是避免同时满足这四个条件。

在这四个条件里面,互斥条件是锁本身的特性,无法被破坏,其他三个条件都可以被破坏。

对于请求和保持条件,我们可以在第一次执行的时候一次性申请所有的共资源

对于不可抢占条件,占用部分资源的线程在进一步申请其他资源的时候,如果申请不到,就主动释放它占有的资源。

对于循环等待条件,可以按照顺序来申请资源,相当于给资源编号,按照编号顺序申请就可以避免循环等待。

当然,死锁问题不仅仅局限在多线程领域,单反涉及到互斥锁的地方都有可能出现,比如Mysql数据库的行锁、表锁,以及分布式锁等。

底层原理上都是相同的。

回答

程序出现死锁,是因为在多线程环境里面两个或两个以上的线程同时满足互斥条件、请求保持条件、不可抢占条件、循环等待条件。

出现死锁以后,可以通过jstack命令去导出线程的dump日志,然后从dump日志里面定位到具体死锁的程序代码。

通过修改程序代码去破坏这四个条件里面的任意一个,就可以解决死锁问题。

当然,因为互斥条件因为是锁本身的特性,所以不能被破坏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值