并发编程常见面试题
1.进程和线程还有协程之间的关系
1.1 进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己独立的地址空间,有自己的堆,上级挂靠单位是操作系统。
操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。
1.3 协程,协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。
协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
协程在子程序内部可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行。
参考:https://www.cnblogs.com/starluke/p/11795342.html
2.并发和并行之间的区别
并发:指统一时间内,宏观上处理多个任务
并行:指统一时间内,真正上处理多个任务
3.Java中多线程实现的方式
3.1 继承Thread类,重写run方法
3.2 实现Runnable接口,重写run方法
3.3 通过Callable和FutureTask创建线程
3.4 通过线程池创建线程
4.Callable和Future模式
Callable
在Java中,创建线程一般有两种方式,一种是继承Thread类,一种是实现Runnable接口。然而,这两种方式的缺点是在线程任务执行结束后,无法获取执行结果。我们一般只能采用共享变量或共享存储区以及线程通信的方式实现获得任务结果的目的。
不过,Java中,也提供了使用Callable和Future来实现获取任务结果的操作。Callable用来执行任务,产生结果,而Future用来获得结果。
Callable接口的定义如下:
public interface Callable<V> { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception; }
与Runnable接口不同之处在于,call方法带有泛型返回值V。
Future模式
Future模式的核心在于:去除了主函数的等待时间,并使得原本需要等待的时间段可以用于处理其他业务逻辑
Futrure模式:对于多线程,如果线程A要等待线程B的结果,那么线程A没必要等待B,直到B有结果,可以先拿到一个未来的Future,等B有结果是再取真实的结果。
在多线程中经常举的一个例子就是:网络图片的下载,刚开始是通过模糊的图片来代替最后的图片,等下载图片的线程下载完图片后在替换。而在这个过程中可以做一些其他的事情。
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; /** * @author wn: * @version 上午16:53:57 * 类说明 */ public class ThreadCallTest { public static void main(String[]args){ ExecutorService executor=Executors.newCachedThreadPool(); Task task=new Task(); Future<Integer> result=executor.submit(task); if (executor != null) executor.shutdown(); try { System.out.println("call result"+result.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println("over"); } } class Task implements Callable<Integer>{ @Override public Integer call() throws Exception { System.out.println("3.开始 ...."); Thread.sleep(3000); System.out.println("4.结束 ...."); return "xyz"; } }
当Task启动后不影响主线程运行,result.get()会等待3秒后返回结果xyz
Future常用方法
V get() :获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成。
V get(Long timeout , TimeUnit unit) :获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。
boolean isDone() :如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。 => result.isDone()
boolean isCanceller() :如果任务完成前被取消,则返回true。
boolean cancel(boolean mayInterruptRunning) :如果任务还没开始,执行cancel(...)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;
当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;
当任务已经完成,执行cancel(...)方法将返回false。mayInterruptRunning参数表示是否中断执行中的线程。
实际上Future提供了3种功能:
- (1)能够中断执行中的任务
- (2)判断任务是否执行完成
- (3)获取任务执行完成后的结果
5.线程池创建的方式(一般不适用Excecutors.newxxxx创建,一般使用ThreadPoolExecutor)
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
6.Java当中线程状态有哪些
6.1 新建状态(New): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
6.2 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
6.3 运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
6.4 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- (01) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
- (02) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
- (03) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
6.5 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
7.多线程中的常用方法
Java多线程中的常用方法有如下几个
start,run,sleep,wait,notify,notifyAll,join,isAlive,currentThread,interrupt
1)start方法
用于启动一个线程,使相应的线程进入排队等待状态。一旦轮到它使用CPU的资源的时候,它就可以脱离它的主线程而独立开始
自己的生命周期了。注意即使相应的线程调用了start方法,但相关的线程也不一定会立刻执行,调用start方法的主要目的是使
当前线程进入排队等待。不一定就立刻得到cpu的使用权限...
2)run方法
Thread类和Runnable接口中的run方法的作用相同,都是系统自动调用而用户不得调用的。
3)sleep和wait方法
Sleep:是Java中Thread类中的方法,会使当前线程暂停执行让出cpu的使用权限。但是监控状态依然存在,即如果当前线程
进入了同步锁的话,sleep方法并不会释放锁,即使当前线程让出了cpu的使用权限,但其它被同步锁挡在外面的线程也无法获
得执行。待到sleep方法中指定的时间后,sleep方法将会继续获得cpu的使用权限而后继续执行之前sleep的线程。
Wait:是Object类的方法,wait方法指的是一个已经进入同步锁的线程内,让自己暂时让出同步锁,以便其它正在等待此同步
锁的线程能够获得机会执行。,只有其它方法调用了notify或者notifyAll(需要注意的是调用notify或者notifyAll方法并不释放
锁,只是告诉调用wait方法的其它 线程可以参与锁的竞争了..)方法后,才能够唤醒相关的线程。此外注意wait方法必须在同步关
键字修饰的方法中才能调用。
4) notify和notifyAll
释放因为调用wait方法而正在等待中的线程。notify和notifyAll的唯一区别在于notify唤醒某个正在等待的线程。而notifyAll会唤醒
所有正在等等待的线程。需要注意的是notify和notifyAll并不会释放对应的同步锁哦。
5) isAlive
检查线程是否处于执行状态。在当前线程执行完run方法之前调用此方法会返回true。
在run方法执行完进入死亡状态后调用此方法会返回false。
6) currentThread
Thread类中的方法,返回当前正在使用cpu的那个线程。
7) intertupt
吵醒因为调用sleep方法而进入休眠状态的方法,同时会抛出InterrruptedException哦。
8)join
线程联合 例如一个线程A在占用cpu的期间,可以让其它线程调用join()和本地线程联合。
9)yield()
调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获 取CPU执行时间的机会。
注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
8.线程状态流程图
9.volatile关键字有什么用途,和Synchronize有什么区别
volatile关键字的作用
其实volatile关键字的作用就是保证了可见性和有序性(不保证原子性),如果一个共享变量被volatile关键字修饰,那么如果一个线程修改了这个共享变量后,其他线程是立马可知的。为什么是这样的呢?比如,线程A修改了自己的共享变量副本,这时如果该共享变量没有被volatile修饰,那么本次修改不一定会马上将修改结果刷新到主存中,如果此时B去主存中读取共享变量的值,那么这个值就是没有被A修改之前的值。如果该共享变量被volatile修饰了,那么本次修改结果会强制立刻刷新到主存中,如果此时B去主存中读取共享变量的值,那么这个值就是被A修改之后的值了。
volatile能禁止指令重新排序,在指令重排序优化时,在volatile变量之前的指令不能在volatile之后执行,在volatile之后的指令也不能在volatile之前执行,所以它保证了有序性。
synchronized关键字的作用
synchronized提供了同步锁的概念,被synchronized修饰的代码段可以防止被多个线程同时执行,必须一个线程把synchronized修饰的代码段都执行完毕了,其他的线程才能开始执行这段代码。
因为synchronized保证了在同一时刻,只能有一个线程执行同步代码块,所以执行同步代码块的时候相当于是单线程操作了,那么线程的可见性、原子性、有序性(线程之间的执行顺序)它都能保证了。
volatile关键字和synchronized关键字的区别
(1)、volatile只能作用于变量,使用范围较小。synchronized可以用在变量、方法、类、同步代码块等,使用范围比较广。
(2)、volatile只能保证可见性和有序性,不能保证原子性。而可见性、有序性、原子性synchronized都可以保证。
(3)、volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。
10.指令重排和先行发生原则
Java指令重排序
Java先行发生原则(Happen-Before)
11.并发编程线程安全三要素
保证线程安全的三个方面
12.进程和线程间调度算法:
12.1先来先服务(FCFS)
先来先服务(first-come first-served,FCFS):是最简单的调度算法,按照请求的先后顺序进行调度。
特点:
非抢占式的调度算法,易于实现,但性能不好
有利于长作业,不利于短作业。 因为短作业必须等待前面的长作业执行完毕才能执行,会造成短作业的等待时间过长。
12.2短作业优先(SJF)
短作业优先(shortest job first,SJF):按估计运行时间最短的顺序进行调度。
特点:
非抢占式的调度算法,优先照顾短作业,具有很好的性能,降低平均等待时间,提高吞吐量。
不利于长作业,长作业可能一直处于等待状态,造成长作业饿死。
没有考虑作业的优先紧迫程度,不能用于实时系统。
12.3最短剩余时间优先(SRTN)
最短剩余时间优先 (shortest remaining time next, SRTN):
最短剩余时间优先是短作业优先的抢占式版本,按剩余运行时间最短的剩余进行调度。
新的进程到来时,将新进程的运行时间与当前进程的剩余运行时间进行比较。
如果新进程的运行时间更短,则挂起当前进程,运行新进程;否则新进程等待。
特点: 确保一旦新的短进程进入系统,可以尽快处理。
12.4时间片轮转
时间片轮转(round robin,RR):
使用队列,按照FCFS的原则将就绪进程排序,后来的进程插入到队列末尾。
选择队列中的首进程,为其分配CPU时间片。时间片用完,计时器发出时钟中断,停止该进程的运行,将其送回队列末尾。
重复步骤1和2,直到完成进程调度。
特点:
时间片轮转的效率与时间片的大小有很大关系。 时间片太小,进程切换频繁反而使CPU利用率变低;时间片太大,系统的实时性不能得到保证。
时间片轮转算法,大多用于分时系统。
12.5优先级调度
优先级调度:为每个进程分配一个优先级,优先级高的先调度,优先级低后调度。
根据当前进程执行时,遇到较高优先级进程的处理方式,分为抢占式优先级调度、非抢占式优先级调度。
抢占式优先级调度:进程在执行期间,具有更高优先级的进程到来,则中断当前进程,执行具有更高优先级的进程。
非抢占式优先级调度:进程在执行期间,具有更高优先级的进程到来,仍然继续执行直到完成。
为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
12.6多级反馈队列
多级反馈队列:
设置多个队列,每个队列的时间片大小不同,例如1, 2, 4, 8 ...。
进程就绪时首先进入一级队列,被调度执行后,如果还需执行,则进入二级队列,依次类推。如果到了最后一级队列还未执行完毕,则仍然进入该队列。
每一级队列中的进程按照FCFS进行调度,只有当上一级队列中没有进程等待执行时,才能调度当前队列中的进程。
多级反馈队列是为连续执行多个时间片的进程考虑的,通过更改每级队列的时间片大小,减少该进程的调度次数。
参考:操作系统之进程和线程(进程调度算法、进程间通信)
Java中采用抢占式
13.Java开发中用过哪些锁:
13.1 乐观锁
乐观锁顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 比较并交换)实现的
乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升;
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
13.2 悲观锁
悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步原语synchronized关键字的实现就是悲观锁。
悲观锁适合写操作非常多的场景;
悲观锁在Java中的使用,就是利用各种锁;
13.3 独享锁
独享锁是指该锁一次只能被一个线程所持有。
独享锁通过AQS来实现的,通过实现不同的方法,来实现独享锁。
对于Synchronized而言,当然是独享锁。
13.4 共享锁
共享锁是指该锁可被多个线程所持有。
读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
共享锁也是通过AQS来实现的,通过实现不同的方法,来实现共享锁。
13.5 互斥锁
互斥锁在Java中的具体实现就是ReentrantLock。
13.6 读写锁
读写锁在Java中的具体实现就是ReadWriteLock。
13.7 可重入锁
重入锁也叫作递归锁,指的是同一个线程外层函数获取到一把锁后,内层函数同样具有这把锁的控制权限;
synchronized和ReentrantLock就是重入锁对应的实现;
synchronized重量级的锁 ;
ReentrantLock轻量级的锁;
13.8 公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
13.9 非公平锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
13.10 分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7和JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
13.11 偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
13.12 轻量级锁
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
13.13 重量级锁
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。
13.14 自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
14.synchronized关键字理解
代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。它包括两种用法:synchronized 方法和 synchronized 块。
在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
15.CAS无锁机制
CAS无锁机制
16.AQS
AQS简介
17.ReentrantLock底层实现
ReentrantLock主要利用CAS+CLH队列来实现。它支持公平锁和非公平锁,两者的实现类似。
- CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。
- CLH队列带头结点的双向非循环链表
ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入CLH队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:
- 非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
- 公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁
18.ReentrantLock和Synchronized之间的区别
相似点:
两个都是可重入锁,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。
功能区别:
相同点:
1.它们都是加锁方式同步;
2.都是重入锁;
3. 阻塞式的同步;也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善);
不同点:
参考:https://blog.csdn.net/qq_40551367/article/details/89414446
19.ReentrantReadWriteLock
读写锁简介
20.BlockingQueue和ConcurrentLinkedQueue简介
BlockingQueue
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列,这两个附加的操作是:
在队列为空时,获取元素的线程会等待队列变为非空;
当队列满时,存储元素的线程会等待队列可用;
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器拿元素;
在java中,BlockingQueue的接口位于java.util.concurrent包中,阻塞队列是线程安全的;
在新增的concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题,通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利;
常用的队列主要由以下两种:
1.先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能,从某种程度上来说这种队列也体现了一种公平性;
2.后进后出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件;
ConcurrentLinkedQueue
ConcurrentLinkedQueue : 是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue.
它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的,该队列不允许null元素。
ConcurrentLinkedQueue重要方法:
add 和offer() 都是加入元素的方法(在ConcurrentLinkedQueue中这俩个方法没有任何区别)
poll() 和peek() 都是取头元素节点,区别在于前者会删除元素,后者不会。
peek和poll当队列当中没有数据时,获取的数据为null,不会产生阻塞