多线程面试题总结

三. Java 多线程模块
35.并行和并发有什么区别?

答:并行是指在两个或多个事件在同一个时刻发生,并发是指两个或多个事件在同一时间间隔发生;
并行是在不同实体上的多个事件,并发是在同一个实体上的多个事件。

36.线程和进程的区别?

答:根本区别:线程是任务调度和执行的基本单位单位,进程是操作系统资源分配的单位。

开销:每一个进程都拥有独立的代码和数据空间(程序上下文),程序间的切换会有较大的开销,而同一类的线程是共享程序上
下文的,但每个线程都拥有独立的线程栈和程序计数器,线程间的切换消耗比较小。

所处环境:操作系统(多核)能同时运行多个进程,同一个进程中可以同时有多个线程执行(通过cpu调度,在每个时间片只有一个线程执行)。

内存分配:操作系统会为每个进程分配不同的内存空间,而对线程而言,除cpu外,系统不会为线程分配内存,线程使用的资源来自
其多属的进程,线程组间进行资源共享。

包含关系:没有线程的进程可以看作是单线程的,执行过程是一条线的,如果一个进程内有多个线程,则执行过程由多条执行路径
完成;线程是进程的一部分,所以线程也被称为轻量级进程。

37.守护线程是什么?

答:守护线程是运行在后台的一种特殊线程,他独立于终端控制并且周期性的执行某种任务或等待处理某些发生的事件。比如java的垃
圾回收线程就是特殊的守护线程。守护线程拥有自动结束自己生命周期的特性,而非守护线程没有。

守护线程使用场景:如果希望某个线程在程序退出时自动关闭,此时便可以使用守护线程。

38.多线程有几种实现方式?

答:我所知的由四种:
1、通过继承Thread类重写run()方法进行创建
2、通过实现Runnable接口进行实现
3、实现Callable接口通过FutureTask进行包装来创建Thread线程
4、使用ExecutorService、Callable、Future实现又有返回结果的线程

39.说一下 Runnable和 Callable有什么区别?

答:Runnable实现的线程是无返回结果的,Callable可以和Future/FutureTask配合实现的是带返回结果的线程。
Runnable的run()方法不能够往上抛出异常,只能内部消化,Callable允许抛出异常。

Callable接口支持返回执行结果,调用FutureTask.get()获取结果,但是该方法会阻塞主线程继续执行下去,不调用不阻塞。

带返回结果的优势:由于多线程比单线程更加的复杂和难的一个重要原因是多线程充满未知性,如某条线程执行了吗?它执行
了多久?它的结果满足我们的预期了吗?这些我们无法得知,只能等待多线程任务执行完毕,而Callable+Future/FutureTask可以
获取线程运行的结果,并且可以在指定时间内获取不到结果就将线程任务取消掉。

40.线程有哪些状态?

答:由初始态、就绪态、运行态、阻塞态、死亡态

状态间的相互转换:初始态(Thread.start()方法)-->就绪态
				  就绪态<--->运行态,os选中,线程获取到时间片变为运行态,时间片用完或调用T.yield()方法变为就绪态
				  运行态-->阻塞态,等待用户输入,T.sleep()或其他线程加入调用t.jion(),运行态变为阻塞态
				  阻塞态-->就绪态,用户输入完毕,T.sleep()时间结束或加入的线程t已终止,阻塞态变为就绪态
				  运行态-->死亡态,run()/main()结束
				  
				  运行态到就绪态的转换还有两种就是同步和线程间通信,当没有拿到锁时,进入锁池队列,然后再拿到锁后进入
				  就绪态;在同步代码块中调用了o.wait()方法,放弃cpu,进入等待队列,当被其他线程唤醒时
						  (调用notify()/notifyAll()),线程进入锁池队列,在拿到锁后进入就绪态。

41.sleep() 和 wait() 有什么区别?

答:sleep()是Thread类的方法,wait()是Object类的方法;
调用sleep()会使得线程进入阻塞态,但是不会放弃锁资源,调用wait()会使得线程进入等待队列,会放弃锁资源;
sleep()不要求一定要和synchronized配合使用,但是wait()要求一定要在synchronized锁定的代码块和方法中使用

42.notify()和 notifyAll()有什么区别?

答:notify()是随机唤醒某个线程,notifyAll()是唤醒所有线程;
notify()可能会导致死锁,notifyAll()不会。

43.线程的 run() 和 start() 有什么区别?

答:run()是线程的任务处理逻辑的入口方法,start()是线程启动的方法

44.创建线程池有哪几种方式?

答:可以通过Executors提供的6种方法进行创建:
newFixedThreadPool()创建固定线程数的线程池;
newCachedThreadPool()创建可缓存的线程池,可自动回收空闲线程,线程池的容量不限制
newScheduledThreadPool()创建定长的线程池,可定时执行任务
newSingleThreadPool()创建单线程线程池,线程异常结束时会创建一个新的线程,能确保任务按提交顺序执行
newSingleThreadScheduledExecutor()创建单线程可执行周期性任务的线程池
newWorkStealingPool()创建任务窃取线程池,不保证执行顺序,适合任务耗时差异较大。

45.线程池都有哪些状态?

答:有5种,分别为:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATE
状态转换:
RUNNING->SHUTDOWN,调用shutdown()方法转换,SHUTDOWN状态下线程池不接受新的任务,但是会执行队列中已已有的任务
RUNNING/SHUTDOWN->STOP,调用shutdownNow()方法转换,STOP状态下线程池不接受新的任务,也不会执行队列中已有的任务,并且中断正在执行的任务
SHUTDOWN->TIDYING,当任务队列为空和线程池执行的任务为空时进行转换,TIDYING状态下会执行terminate()钩子函数
STOP->TIDYING,当线程池执行的任务为空时进行转换
TIDYING->TERMINATE,当执行完terminate()方法后转换为TERMINATE状态,线程池彻底终止

46.线程池中 submit() 和 execute() 方法有什么区别?

答:submit()能够接受的参数类型有Runnable、Callable,而executor()能够接受的参数类型只有Runnable;
submit()有返回值Future,executor()没有返回值;
submit()方便处理异常(因为重写Callable的call()方法允许抛出异常,而重写Runnable的run()方法不允许抛出异常)

47.在 Java 程序中怎么保证多线程的运行安全?

答:线程安全性问题体现在:原子性:一个或多个操作在cpu执行的过程中被不打断
可见性:一个线程对共享变量的修改,能被另一个线程看见
有序性:程序执行的顺序按照代码的先后顺序执行

导致原因:线程切换导致的原子性问题
		  缓存导致的可见性问题
		  编译器优化导致的有序性问题
解决办法:JDK中Automic开头的原子类、synchronized、Lock解决原子性问题
		  使用volatile域解决可见性问题
		  使用happens-before规则解决有序性问题

happens-before规则:程序次序规则:一个线程内,按照程序控制流顺序,书写在前面的操作先行发生于后面的操作
					管程锁规则:一个unlock操作先行发生于同一个锁的lock操作
					volatile变量规则:对一个volatile变量的写操作先行于后面对它的读操作
					线程启动规则:Thread的start()方法先行发生于此线程中的每一个动作
					线程终止规则:线程中的所有动作都先行发生于对线程的终止检测
					线程中断规制:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
					对象终结规制:一个对象的初始化先行发生于它的finalize()方法开始前

48.多线程中 synchronized 锁升级的原理是什么?

答:首先synchronized的原理:编译后在同步代码块的前后添加上monitorenter和monitorexit字节码指令获取线程的执行权(同步方法通
过加 ACC_SYNCHRONIZED 标识实现线程的执行权的控制),而这两个字节码指令都需要一个指定锁定和解锁的对象reference,如果
synchronzied修饰的是对象,那reference就指向这个对象,如果是类方法就指向这个类的Class对象,如果是实例方法就指向该实
例对象。

对象头和锁:synchronized的锁存在于java对象头中,在HotSpot虚拟机中对象头分两部分信息,一部分存储对像的运行时数据,如
hashcode和GC分代年龄,这部分数据长32bit/64bit,又称为“Mark Word”,这部分是实现锁的关键;另一部分存储的是指向方法区
对象类型数据的指针。对象头是与对象自身数据无关的额外存储成本,因此需要考虑到空间效率,未锁定状态下的对象32bit长的
Mark Word其中25bit用来存储hashcode,4bit用来存储gc分代年龄,2bit用于存储锁标记位,1bit固定为0.

java中,锁共有4中状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁,这几个状态会随着锁竞争的情况逐渐升级,
但是锁只能升级倒是不能够降级。

锁升级:
无锁->偏向锁:对象的初始锁状态为可偏向锁状态,对象的偏向锁偏向于第一个使用它的线程,当第一个线程访问同步块并取得锁时,
			  会在对象头和栈帧中的锁记录中存储偏向的线程id,以后该线程在进入和退出同步代码块时就不需要进行CAS操作来
			  加锁和解锁,只需检查Mark Word中存储的线程id是否为当前线程的id,如果是则表明已获得锁,否则需要测试mark word
			  中的偏向锁标志位是否为1,如果不是,则使用CAS操作竞争锁,如果是则尝试使用CAS将对象头的偏向锁指向当前进程
偏向锁->轻量级锁:偏向锁使用的是一种等待竞争出现才会释放锁的机制,一个线程持有对象锁,当另外一个线程尝试获取该锁时,偏向锁
				  并不会主动释放,此时第二个线程时能够观察到对象的偏向状态,此时就已出现了竞争,这个时候就会去检查持有锁
				  的线程是否存活,如果线程挂掉了,则可以将对象变为无锁对状态,然后重新偏向新的线程;如果持锁线程依旧存活,
				  则马上去执行线程的操作栈,检查该对象的使用情况,如果还需要持有偏向锁,此时偏向锁就会升级为轻量级锁;如果
				  不需要了则可以将对象恢复成无锁状态,然后重新偏向。
轻量级锁->重量级锁:轻量级锁认为竞争存在,但是竞争程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微自旋
					一下另外一个线程就会释放锁,但是如果自旋时间过程,或者一个线程持有锁,一个线程在自旋,此时
					又有新的线程进行访问,此时轻量级锁就会膨胀为重量级锁,重量级锁会使得除持锁线程外的所有线程阻塞,
					以防止cpu空转,但是这样一来便无法发挥出多核处理机的特性,导致性能下降。

49.什么是死锁?

答:死锁时指两个或两个以上的线程,互相持有对方所需的资源,导致这些线程都处于等待状态,无法进行下去。

50.怎么防止死锁?

答:1、加锁顺序:如果一个线程需要获取一些锁,那么就必须按照确定的顺序来取得锁,只有取得排序在前面的锁才能取得后面的锁,
其缺点是需要事先知道要用到的锁,但是有些时候是无法预知的。
2、加锁时限:在线程请求一个锁资源时设置请求超时时间,如果超过这个时间无法获取到锁(超时)则就释放自身所拥有的锁资
源(回退),并在等待一段随机时间后再进行相同的加锁逻辑,在等待的这段时间内可以去执行其他的任务。其缺点
是一旦超时就会回退,一旦线程数变多,这种回退操作就会异常消耗性能,更有可能导致一些线程始终无法取得锁,
从而误判出现死锁。

3、死锁检测:在加锁时记录下线程和锁的关系,当一个线程请求锁时也会被记录下来,然后进行递进式的检测,如线程A请求
			 线程B持有的一个锁资源,但是B请求C一个资源,C又请求D一个资源,D又请求A一个持有的资源,线程A为了检测死锁,
			 它需要递进地检测所有被B请求的锁,故它从B找到C,又从C找到D,最后从D找到A,然后发现自己持有D需要的锁,这时它
			 就知道发生了死锁。只有发生了死锁才进行回退。
			 然而这样还不能够解决大量线程竞争同一批锁重复死锁的问题,因为回退会出现多个线程同时释放资源又在等待相同时间过后
			 同时请求资源,这样一直重复便无法解决问题。一个更好的方法是设置线程优先级,让一个或几个线程回退,其他保持不变。

51.ThreadLocal 是什么?有哪些使用场景?

答:ThreadLocal是线程本地存储,在每一个线程中都创建了一个ThreadLocalMap对象,每个线程可以访问自己内部ThreadLocalMap对象内
的value。
经典的使用场景是为每个线程分配一个jdbc连接connection,这样就就可以保证每个线程都在各自的connection上进行数据库的操作,
不会出现线程A在线程B的连接上进行操作;还有如session管理等问题。

52.说一下 synchronized 底层实现原理?

答:编译后在同步代码块的前后添加上monitorenter和monitorexit字节码指令获取线程的执行权;
同步方法通过加 ACC_SYNCHRONIZED 标识实现线程的执行权的控制;

53.synchronized 和 volatile 的区别是什么?

答:synchronized解决的是执行(顺序)控制问题(在并发下),volatile解决的是内存可见性问题;
volatile仅能使用在变量级别,synchronized则能够在变量,方法和类级别使用;
volatile的本质是告诉jvm当前寄存器的值是不确定的,需要到主存中去取,synchronized则锁定当前变量,只有当前线程能够访问,其他线程会被阻塞;
volatile仅能实现修改变量的可见性,但是不保证原子性(如复合操作i++),synchronized则可以保证变量修改的可见性和原子性;

volatile不会造成线程阻塞,synchronized可能会造成线程阻塞;
volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化

54.synchronized 和 Lock 有什么区别?

答:synchronized是关键字实现锁的层面是jcm,Lock是类实现的锁层面是api;
synchronized会在使用完毕锁或抛出异常时由jvm主动释放锁资源,Lock则需要手动去释放,通常需要早try{}finally{}中使用unlock()去释放;
synchronzied实现的锁是不可中断的(除抛出异常和正常结束)、非公平,Lock实现的锁可中断(设置超时和调用interrput()方法)、可以是公平的也可以是非公平的;
synchronized的线程唤醒是随机性的或是全部唤醒,Lock可以进行分组唤醒,可以精确唤醒。

55.synchronized 和 ReentrantLock 区别是什么?

答:同54
56.说一下 atomic 的原理?

答:atomic原子类解决的是基本类型变量操作的原子性问题,底层使用的是CAS操作实现无锁并发,它是从指令层保证操作的可靠性,
不会被多线程干扰;

CAS操作:CAS包含三个参数,分别为V,E,N,其中V代表要更新的变量,E代表预期的值,V代表新值。仅当V的值等于E时,才会
		 将V的值修改为N,如果不相同则说明有线程对其进行了更新,则当前线程什么都不做,CAS返回V的真实值。当更新失败
		 当前线程不会被挂起,仅被告知失败,允许再次尝试去更新或放弃操作。

CAS会出现的ABA问题:CAS的关键是操作值的时候检查值有没有变化,如果一个原来的值为A,变成了B,有被变回了A,这样便可
					以通过CAS检测,但是实际上它是被线程修改过的。
ABA解决办法:jdk1.5后增加stamp进行版本的标记,在进行CAS操作前会先进行版本的对比,如A1-B-A2.

还有就是不断进行检查,造成CAS检查时间可能很长不成功,消耗了性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值