Java并发面试总结

http://www.importnew.com/12773.html#comment-580347

1.线程的生命周期、线程各个状态之间的切换

在线程的生命周期中,它要经过 新建(New)、就绪(Runnable)、运行(Running)、阻塞(Bolcked)、死亡(Dead)总共5种状态。

【新建和就绪状态】

新建状态:当程序使用new关键字创建了一个线程之后,这个线程就处于新建状态,此时,它和一般的java对象没有区别,仅仅由java虚拟机为其分配内存,并初始化其成员变量值。此时的线程对象没有表现出任何线程的动态特性,程序也不会执行线程的执行体。

就绪状态:当线程对象调用了start()方法之后,该线程就处于就绪状态,就绪状态相当于"等待执行"。java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示线程可以运行,但是进入运行状态取决于JVM里线程调度器的调度。如下情况会进入就绪状态;

1.调用sleep()方法到了指定的时间。

2.线程调用的阻塞式IO方法已经返回。

3.线程成功地获得了试图取得的同步监视器。

4.线程正在等待某个通知时,其它线程发出了一个通知。

5.处于挂起状态的线程被调用了resume()恢复方法。


[ 注意 ]

启动线程使用start()方法,不是run()方法!!永远不要调用线程对象的run()方法!!!

调用start()方法,系统会把run()方法当成线程执行体来处理。

直接调用run()方法,则run()方法会立即被执行,系统会把线程对象当成一个普通对象来处理,run()方法也变成了一个普通的方法

 另外,直接调用线程对象的run()方法,则run()方法内不能通过getName()来获得当前执行线程的名字,而是需要使用Thread.currentThread先获得当前线程,再调用线程的getName()方法来获得线程的名字。

另外,调用了线程的run()方法之后,该线程已经不再处于新建状态,不要再次调用线程对象的start()方法,只能对处于新建状态的线程调用start()方法,否则会引发IllegalThreadStateException异常。


[ 小技巧 ]

        如果希望某一线程对象在调用了start()方法后立即执行,可以使用Thread.sleep(1)让当前执行的其它线程(如主线程)睡眠1毫秒,在这1毫秒内,CPU不会空闲,它会立即去执行处于就绪状态的线程,这样就可以让某一线程立即开始执行。


【运行和阻塞状态】

运行状态:处于就绪状态的线程获得了CPU的执行权,开始执行run()方法的线程执行体,则该线程处于运行状态。

阻塞状态:发生以下几种情况,线程会进入阻塞状态:

1.线程调用了sleep()方法,主动放弃所占用的系统资源。

2.线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。

3.线程试图获得一个同步同步监视器,但该同步监视器被其它线程所持有。

4.线程正在等待某个通知(notify或notifyAll)。

5.程序调用了线程的suspend()方法将该线程挂起,但该方法会导致死锁,要避免使用。

从图中可以看出,线程从阻塞状态只能进入就绪状态,无法直接进入运行状态。

而就绪状态和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定,处于就绪状态的线程后的处理器的执行权后,线程进入运行状态。

当处于运行状态的线程失去处理器资源时,该线程进入就绪状态,有一个方法例外,即yield()方法可以让运行状态直接转入就绪状态。


【线程死亡】

线程在三种情况下会进入死亡状态:

1.run()方法或call()方法执行结束后,线程正常结束死亡。

2.线程抛出一个未捕获的Exception或Error。

3.直接调用该线程的stop()方法来结束该线程,但是stop()方法容易导致死锁,最好不要用。

[ 提示 ]

测试某个线程是否已经死亡,可以调用对象的isAlive()方法,

当线程处于就绪、运行、阻塞三种状态时,返回true

当线程处于新建、死亡状态时,返回false

[ 注意 ]

不要试图对一个已经死亡的线程调用start()方法使它重启,死亡就是死亡,该线程不可再次作为线程执行,否则会抛出IllegalThreadStateException异常,

新建状态的线程两次调用start()方法也是错误的额,都会引发IllegalThreadStateException异常。


2.ReentrantLock、ArrayBlockingQueue、LinkedBlockingQueue源码

https://www.cnblogs.com/java-zhao/p/5131544.html

https://blog.csdn.net/u014082714/article/details/52215130

https://www.cnblogs.com/leesf456/p/5539071.html

3.自旋锁

https://blog.csdn.net/zqz_zqz/article/details/70233767

4.volatile、内存屏障

https://www.cnblogs.com/dolphin0520/p/3920373.html

https://www.jianshu.com/p/2ab5e3d7e510

volatile:

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序。


 第一:使用volatile关键字会强制将修改的值立即写入主存;

 第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

 第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

内存屏障:


为什么会有内存屏障

        每个CPU都会有自己的缓存(有的甚至L1,L2,L3),缓存的目的就是为了提高性能,避免每次都要向内存取。但是这样的弊端也很明显:不能实时的和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。

        用volatile关键字修饰变量可以解决上述问题,那么volatile是如何做到这一点的呢?那就是内存屏障,内存屏障是硬件层的概念,不同的硬件平台实现内存屏障的手段并不是一样,java通过屏蔽这些差异,统一由jvm来生成内存屏障的指令。

内存屏障是什么

硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

内存屏障有两个作用:

阻止屏障两侧的指令重排序;

强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

java内存屏障

java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。

LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

volatile语义中的内存屏障

volatile的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:

在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;

在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;

由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。

final语义中的内存屏障

对于final域,编译器和CPU会遵循两个排序规则:

新建对象过程中,构造体中对final域的初始化写入和这个对象赋值给其他引用变量,这两个操作不能重排序;(废话嘛)

初次读包含final域的对象引用和读取这个final域,这两个操作不能重排序;(晦涩,意思就是先赋值引用,再调用final值)

总之上面规则的意思可以这样理解,必需保证一个对象的所有final域被写入完毕后才能引用和读取。这也是内存屏障的起的作用:

写final域:在编译器写final域完毕,构造体结束之前,会插入一个StoreStore屏障,保证前面的对final写入对其他线程/CPU可见,并阻止重排序。

读final域:在上述规则2中,两步操作不能重排序的机理就是在读final域前插入了LoadLoad屏障。

X86处理器中,由于CPU不会对写-写操作进行重排序,所以StoreStore屏障会被省略;而X86也不会对逻辑上有先后依赖关系的操作进行重排序,所以LoadLoad也会变省略。

5.线程池

https://www.cnblogs.com/aspirant/p/6920418.html

什么是线程池:  java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池。多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。

 一个线程池包括以下四个基本组成部分:

1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;

2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;

3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;

4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

2.常见线程池

1. newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

2.newFixedThreadPool

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

3. newCachedThreadPool

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,

那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

4.newScheduledThreadPool

创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。


6、submit()和execute()的区别

JDK5往后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,它们的区别是:

execute(Runnable x) 没有返回值。可以执行任务,但无法判断任务是否成功完成。——实现Runnable接口

submit(Runnable x) 返回一个future。可以用这个future来判断任务是否成功完成。——实现Callable接口

7.内核态与用户态

https://blog.csdn.net/xiaofei0859/article/details/51144670

内核态:CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡. CPU也可以将自己从一个程序切换到另一个程序

用户态: 只能受限的访问内存, 且不允许访问外围设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获取

为什么要有用户态和内核态

        由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 --用户态和内核态

用户态与内核态的切换

        所有用户程序都是运行在用户态的, 但是有时候程序确实需要做一些内核态的事情, 例如从硬盘读取数据, 或者从键盘获取输入等. 而唯一可以做这些事情的就是操作系统, 所以此时程序就需要先操作系统请求以程序的名义来执行这些操作.

这时需要一个这样的机制: 用户态程序切换到内核态, 但是不能控制在内核态中执行的指令

这种机制叫系统调用, 在CPU中的实现称之为陷阱指令(Trap Instruction)

他们的工作流程如下:

1.用户态程序将一些数据值放在寄存器中, 或者使用参数创建一个堆栈(stack frame), 以此表明需要操作系统提供的服务.

2.用户态程序执行陷阱指令

3.CPU切换到内核态, 并跳到位于内存指定位置的指令, 这些指令是操作系统的一部分, 他们具有内存保护, 不可被用户态程序访问

4.这些指令称之为陷阱(trap)或者系统调用处理器(system call handler). 他们会读取程序放入内存的数据参数, 并执行程序请求的服务

5.系统调用完成后, 操作系统会重置CPU为用户态并返回系统调用的结果

8.lock()、tryLock()、lockInterupttibly()的区别

lock :调用后一直阻塞到获得锁

tryLock:立即返回,获得锁返回true,没获得锁返回false;带时间限制的tryLock(),拿不到lock,就等一段时间,超时返回false。比较聪明的做法。

tryInterruptibly:调用后一直阻塞到获得锁,但是会响应中断,这个方法优先考虑响应中断,而不是响应锁的普通获取或重入获取。

9.jdk线程池实现原理(ThreadPoolExecutor如何复用线程

https://blog.csdn.net/he90227/article/details/52576452

你可能感兴趣的:(Java并发面试总结)