、
本文是给**「建议收藏」200mb大厂面试文档,整理总结2020年最强面试题库「corejava篇」**写的答案,所有相关文章已经收录在码云仓库:https://gitee.com/bingqilinpeishenme/java-interview
千上万水总是情,先赞后看行不行,奥力给
本文为多线程面试题答案的上篇:线程基本概念+线程池,锁+其他面试题会在下篇写出。
上篇情况:
进程(process)和线程(thread)是操作系统的基本概念,但是它们比较抽象,不容易掌握。
最近在阮一峰的博客上看到了一个解释,感觉非常的好,分享给小伙伴们。
**计算机的核心是cpu,它承担了所有的计算任务。**它就像一座工厂,时刻在运行。
假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个cpu一次只能运行一个任务。
进程就好比工厂的车间,它代表cpu所能处理的单个任务。任一时刻,cpu总是运行一个进程,其他进程处于非运行状态。
一个车间里,可以有很多工人。他们协同完成一个任务
线程就好比车间里的工人。一个进程可以包括多个线程。
进程
所谓进程就是运行在操作系统的一个任务,进程是计算机任务调度的一个单位,操作系统在启动一个程序的时候,会为其创建一个进程,jvm就是一个进程。进程与进程之间是相互隔离的,每个进程都有独立的内存空间。
计算机实现并发的原理是:cpu分时间片,交替执行,宏观并行,微观串行。同理,在进程的基础上分出更小的任务调度单元就是线程,我们所谓的多线程就是一个进程并发多个线程。
线程
在上面我们提到,一个进程可以并发出多个线程,而线程就是最小的任务执行单元,具体来说,一个程序顺序执行的流程就是一个线程,我们常见的main就是一个线程(主线程)。
线程的组成
想要拥有一个线程,有这样的一些不可或缺的部分,主要有:cpu时间片,数据存储空间,代码。
cpu时间片都是有操作系统进行分配的,数据存储空间就是我们常说的堆空间和栈空间,在线程之间,堆空间是多线程共享的,栈空间是互相独立的,这样做的好处不仅在于方便,也减少了很多资源的浪费。代码就不做过多解释了,没有代码搞个毛的多线程。
关于什么是线程安全,为什么会有线程安全的出现,以及为什么需要锁,我在三四年前写过一个小故事。
几个小概念
临界资源:当多线程访问同一个对象时, 这个对象叫做临界资源
原子操作:在临界资源中不可分割的操作叫原子操作
线程不安全:多线程同时访问同一个对象, 破坏了不可分割的操作, 就可能发生数据不一致
“弱肉强食”的线程世界
大家好,我叫王大锤,我的目标是当上ceo...额 不好意思拿错剧本了。大家好,我叫0x7575,是一个线程,我的线生理想是永远最快拿到cpu。
先给大家介绍一下线程世界,线程世界是一个弱肉强食的世界,资源永远稀缺,什么东西都要抢,这几个纳秒我有幸拿到cpu,对int a = 20进行一次加1操作,当我从内存中取出a,进行加1后就失去了cpu,休息结束之后准备写入内存的时候,我惊奇的发现:内存中的a这时候已经变成了22。
一定有线程趁我不在修改了数据,我左右为难,很多线程也都劝我不要写入,但是迫于指令,我只能把21写入内存覆盖掉不符合我的运算逻辑的22。
以上只是一个微小的事故,类似的事情在线程世界层出不穷,所以虽然我们每一个线程都尽职尽责,但是在人类看来我们是引起数据不安全的祸首。
这是何等的冤枉啊,线程世界一直都是竞争激烈的世界,尤其是对于一些共享变量,共享资源(临界资源),同时有多个线程进行争夺使用时再正常不过的事情了。除非消除共享的资源,但是这又是不可能的,于是事情就开始僵持了。
线程世界出现了一把锁
幸好还是又聪明人的,有人想到了一个解决问题的好方法。虽然不知道谁想到的注意,但是这个注意确实解决了一部分问题,解决的方案是加锁。
你想要进行对一组加锁的代码进行操作吗?想的话就先去抢到锁,拿到锁之后就可以对被加锁的代码为所欲为了,倘若拿不到锁的话就只能在代码块门口等着,因为等的线程太多了,这还成为了一种社会现象(状态),该社会现象被命名为线程的阻塞。
听上去很简单,但是实际上加锁有很多详细的规定的,详情政府发布了《关于synchronzied使用的若干规定》以及后来发布的《关于lock使用的若干规定》。
线程和线程之间是共享内存的,当多线程对共享内存进行操作的时候有几个问题是难以避免的,竞态条件(race condition)和内存可见性。
**竞态条件:**当多线程访问和操作同一对象的时候,最终结果和执行时序有关,正确性是不能够人为控制的,可能正确也可能不正确。(如上文例子)
上文中说到的加锁就是为了解决这个问题,常见的解决方案有:
**内存可见性:**关于内存可见性问题要先从内存和cpu的配合谈起,内存是一个硬件,执行速度比cpu慢几百倍,所以在计算机中,cpu在执行运算的时候,不会每次运算都和内存进行数据交互,而是先把一些数据写入cpu中的缓存区(寄存器和各级缓存),在结束之后写入内存。这个过程是及其快的,单线程下并没有任何问题。
但是在多线程下就出现了问题,一个线程对内存中的一个数据做出了修改,但是并没有及时写入内存(暂时存放在缓存中);这时候另一个线程对同样的数据进行修改的时候拿到的就是内存中还没有被修改的数据,也就是说一个线程对一个共享变量的修改,另一个线程不能马上看到,甚至永远看不到。
这就是内存的可见性问题。
解决这个问题的常见方法是:
一个线程在启动之后不会立马执行,而是处于就绪状态(ready),就绪状态就是线程的状态的一种,处于这种状态的线程意味着一切准备就绪, 需要等待系统分配到时间片。为什么没有立马运行呢,因为同一时间只有一个线程能够拿到时间片运行,新线程启动的时候让它启动的线程(主线程)正在运行,只有等主线程结束,它才有机会拿到时间片运行。
**线程的状态:**初始状态(new),就绪状态(ready),运行状态(running)(特别说明:在语法的定义中,就绪状态和运行状态是一个状态runable),等待状态(waitering),终止状态(terminated)
import java.util.concurrent.callable; import java.util.concurrent.futuretask; import java.util.concurrent.timeunit; public class newthreaddemo { public static void main(string[] args) throws exception { //第一种方式 thread t1 = new thread(){ @override public void run() { system.out.println("第1种方式:new thread 1"); } }; t1.start(); timeunit.seconds.sleep(1); //第二种方式 thread t2 = new thread(new runnable() { @override public void run() { system.out.println("第2种方式:new thread 2"); } }); t2.start(); timeunit.seconds.sleep(1); //第三种方式 futuretask<string> ft = new futuretask<>(new callable<string>() { @override public string call() throws exception { string result = "第3种方式:new thread 3"; return result; } }); thread t3 = new thread(ft); t3.start(); // 线程执行完,才会执行get(),所以futuretask也可以用于闭锁 string result = ft.get(); system.out.println(result); timeunit.seconds.sleep(1); //第四种方式 executorservice pool = executors.newfixedthreadpool(5); future<string> future = pool.submit(new callable<string>(){ @override public string call() throws exception { string result = "第4种方式:new thread 4"; return result; } }); pool.shutdown(); system.out.println(future.get()); } }
class c implements callable<string>{ @override public string call() throws exception { return null; } } class r implements runnable{ @override public void run() { } }
相同点:
不同点:
谈到线程池就会想到池化技术,其中最核心的思想就是把宝贵的资源放到一个池子中;每次使用都从里面获取,用完之后又放回池子供其他人使用,有点吃大锅饭的意思。
java线程池有以下优点:
- 通过executors类
- 通过threadpoolexecutor类
在java中,我们可以通过executors类创建线程池,常见的api有:
以上的这些创建线程池的方法,实际上jdk已经给我们写好的,可以拿来即用的。但是只要我们查看上述方法的源码就会发现:
public static executorservice newcachedthreadpool() { return new threadpoolexecutor(0, integer.max_value, 60l, timeunit.seconds, new synchronousqueue<runnable>()); }
以上方法实际上都是利用 threadpoolexecutor 类实现的。
所以第二种创建线程方式是自己通过 new threadpoolexecutor来进行创建。
答案:一个都不用。
从《阿里巴巴java开发手册》中可以看到
关于参数的详细解释见下一个问题。
在上一个问题中,我们提到了创建线程池要通过 new threadpoolexecutor 的方式,那么,如何创建呢?在创建的时候,又需要哪些参数呢?
我们直接看一下 threadpoolexecutor 的构造方法源码,如下:
public threadpoolexecutor(int corepoolsize, int maximumpoolsize, long keepalivetime, timeunit unit, blockingqueue<runnable> workqueue, threadfactory threadfactory, rejectedexecutionhandler handler) { if (corepoolsize < 0 || maximumpoolsize <= 0 || maximumpoolsize < corepoolsize || keepalivetime < 0) throw new illegalargumentexception(); if (workqueue == null || threadfactory == null || handler == null) throw new nullpointerexception(); this.acc = system.getsecuritymanager() == null ? null : accesscontroller.getcontext(); this.corepoolsize = corepoolsize; this.maximumpoolsize = maximumpoolsize; this.workqueue = workqueue; this.keepalivetime = unit.tonanos(keepalivetime); this.threadfactory = threadfactory; this.handler = handler; }
密密麻麻都是参数,那么这些参数都什么呢?
大致的流程就是
- 创建线程池之后,有任务提交给线程池,会先由 核心线程执行
- 如果任务持续增加,corepoolsize用完并且任务队列满了,这个时候线程池会增加线程的数量,增大到最大线程数
- 这个时候如果任务继续增加,那么由于线程数量已经达到最大线程数,等待队列也已经满了,这个时候线程池实际上是没有能力执行新的任务的,就会采用拒绝策略
- 如果任务量下降,就会有很多线程是不需要的,无所事事,而只要这些线程空闲的时间超过空闲线程时间,就会被销毁,直到剩余线程数为corepoolsize。
通过以上参数可以就可以灵活的设置一个线程池了,示例代码如下:
/** * 获取cpu核心数 */ private static int corepoolsize = runtime.getruntime().availableprocessors(); /** * corepoolsize用于指定核心线程数量 * maximumpoolsize指定最大线程数 * keepalivetime和timeunit指定线程空闲后的最大存活时间 */ public static threadpoolexecutor executor = new threadpoolexecutor(corepoolsize, corepoolsize+1, 10l, timeunit.seconds, new linkedblockingqueue<runnable>(1000));
关于线程池的工作原理和执行流程,通过两张图来进行展示
所谓饱和策略就是:当等待队列已经排满,再也发不下新的任务的时候,这时,线程池的最大线程数也到了最大值,意味着线程池没有能力继续执行新任务了,这个时候再有新任务提交到线程池,如何进行处理,就是饱和(拒绝)策略
通常我们是需要根据这批任务执行的性质来确定的。
当然这些都是经验值,最好的方式还是根据实际情况测试得出最佳配置。
关闭线程池的方法有两个:shutdown()/shutdownnow()
。
shutdown()
执行后停止接受新任务,会把队列的任务执行完毕。shutdownnow()
也是停止接受新任务,但会中断所有的任务,将线程池状态变为 stop。关闭线程池的代码:
long start = system.currenttimemillis(); for (int i = 0; i <= 5; i++) { pool.execute(new job()); } pool.shutdown(); while (!pool.awaittermination(1, timeunit.seconds)) { logger.info("线程还在执行。。。"); } long end = system.currenttimemillis(); logger.info("一共处理了【{}】", (end - start));
pool.awaittermination(1, timeunit.seconds)
会每隔一秒钟检查一次是否执行完毕(状态为 terminated
),当从 while 循环退出时就表明线程池已经完全终止了。
参考资料:
- java—多线程基础
- java—线程同步
- callable和runnable的区别
- 如何优雅的使用和理解线程池
- 《阿里巴巴java开发手册》
欢迎关注本人公众号:鹿老师的java笔记,将在长期更新java技术图文教程和视频教程,java学习经验,java面试经验以及java实战开发经验。
如对本文有疑问, 点击进行留言回复!!
第三次学JAVA再学不好就吃翔(part88)--ArrayList嵌套ArrayList
使用ffmpeg视频切片并加密和视频AES-128加密后播放
JAVA程序设计:最长重复子串(LeetCode:1044)
LiveGBS国标GB/T28181云端录像分布式录像存储自动清理时移回看录像下载播放
网友评论