JAVA多线程面试题及答案

**一、线程和进程的区别

1.同一个进程可以包含多个线程,一个进程中至少包含一个线程,一个线程只能存在于一个进程中。
2.同一个进程下的所有线程能够共享该进程下的资源。
3.进程结束后,该进程下的所有线程将销毁,而一个线程的结束不会影响同一进程下的其他线程。
4.线程是轻量级的进程,它的创建和销毁所需要的时间比进程小得多,所有操作系统的执行功能都是通过创建线程去完成的。
5.线程在执行时是同步和互斥的,因为他们共享同一个进程下的资源。
6.在操作系统中,进程是拥有系统资源的独立单元,它可以拥有自己的资源。一般而言,线程不能拥有自己的资源,但是它能够访问其隶属进程的资源。

二、并行和并发的区别**

	1.概念
	   	并发: 指在一台CPU上“同时”(也就是快速切换)处理多个任务。
		并行:指在多台CPU上同时处理多个任务(好比hadoop分布式集群)。
		
	2.描述
		并发:指应用能够交替执行不同的任务,其实并发有点类似于多线程的原理,多线程并非是同时执行多个任务而是快速切换着执行。如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到“同时执行效果”,其实并不是“同时”,只是计算机的速度太快,我们无法察觉到而已。例如,你吃一口饭、喝一口水、再吃一口饭、再喝一口水,以正常速度来看,完全能够看的出来,当你把这个过程以n倍速度执行时…可以想象一下是不是就是一直在喝水和吃饭了。
		并行:指应用能够同时执行不同的任务。例如,吃饭的时候可以边吃饭边听音乐,这两件事情就可以同时执行。
		
	3.总结
		并发的关键是你有处理多个任务的能力,但不是同时。
		并行的关键是你在同时处理多个任务。
		
	4.关键词
		所以我认为它们最关键的点就是:是否是同时。
		两者区别:并发是交替执行,并行是同时执行。

三、什么是线程安全

	1.简单来说,线程安全就是: 在多线程环境中,能永远保证程序的正确性
	2.当一个类被多个线程进行访问并且正确运行,它就是线程安全的
	3.当多个线程访问某各类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

四、创建线程几种方式

    1.继承 Thread类
	    a.重写run方法。该run()方法的方法体就代表了线程需要完成的任务。 
		b.创建Thread子类的实例。 
		c.调用线程对象的start()方法来启动该线程。
    2.实现runable接口
		a、创建自定义类并实现Runable接口,并实现接口中的run方法
		b、实例化自定的类
		c、将自定义类的实例作为参数Thread类,创建thread实例
		d、调用Thread实例的start方法,启动子线程
    3.实现callable 接口
	    a.创建Callable接口的实现类,并实现Call()方法,该方法将作为线程执行体,且该方法有返回值,再创建Callable实现类的实例。
	    b.使用FutureTask来包装Callable对象,该FutureTask对象封装了该Callable对象的call方法的返回值。 
	    c.使用FutureTask对象作为Thread对象的target创建并启动新线程。 
	    d.调用FutureTask对象的get()方法来获取子线程执行结束后的返回值。
    4.线程池 

五、创建线程几种方式的优缺点

优缺点
	1.继承Thread类
		优点 :代码简单 。
		缺点 :该类无法继承别的类。
	2.实现Runnable接口
		优点 :继承其他类。同一实现该接口的实例可以共享资源。
		缺点 :代码复杂
	3.实现Callable
		优点 :可以获得异步任务的返回值
		缺点 :代码复杂
	4.线程池 、实现自动化装配,易于管理,循环利用资源。

六、说一下 runnable 和 callable 有什么区别**

1.Runnable没有返回值;Callable可以返回执行结果,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果

2.Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能在内部消化,不能往上继续抛

注:Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

七、线程有哪些状态

1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):表示线程阻塞于锁。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。

https://blog.csdn.net/pange1991/article/details/53860651

八、sleep() 和 wait() 有什么区别

1、每个对象都有一个锁来控制同步访问,Synchronized关键字可以和对象的锁交互,来实现同步方法或同步块。sleep()方法正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),在sleep指定时间后CPU再回到该线程继续往下执行(注意:sleep方法只让出了CPU,而并不会释放同步资源锁!!!)。

wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。(注意:notify的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度);

2、sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用;

3、sleep()是线程线程类(Thread)的方法,调用会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复;wait()是Object的方法,调用会放弃对象锁,进入等待队列,待调用notify()/notifyAll()唤醒指定的线程或者所有线程,才会进入锁池,不再次获得对象锁才会进入运行状态;

九、notify()和 notifyAll()有什么区别

notify唤醒一个等待的线程;notifyAll唤醒所有等待的线程

注意:
1. 你可以使用wait和notify函数来实现线程间通信。你可以用它们来实现多线程(>3)之间的通信。

2. 永远在synchronized的函数或对象里使用wait、notify和notifyAll,不然Java虚拟机会生成 IllegalMonitorStateException。

3. 永远在while循环里而不是if语句下使用wait。这样,循环会在线程睡眠前后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。

4. 永远在多线程间共享的对象(在生产者消费者模型里即缓冲区队列)上使用wait。

5.notifyAll(),而不是 notify()。

十、线程的 run()和 start()有什么区别

1.调用 start() 方法是用来启动线程的,轮到该线程执行时,会自动调用 run();直接调用 run() 方法,无法达到启动多线程的目的,相当于主线程线性执行 Thread 对象的 run() 方法。
2.一个线程对线的 start() 方法只能调用一次,多次调用会抛出 java.lang.IllegalThreadStateException 异常;run() 方法没有限制。

十一、如何有效的保证线程安全

1、加锁:(同步代码块、同步方法、Lock锁机制)
  a、锁能使其保护的代码以串行的形式来访问,当给一个复合操作加锁后,能使其成为原子操作。一种错误的思想是只要对写数据的方法加锁,其实这是错的,对数据进行操作的所有方法都需加锁,不管是读还是写

  b、加锁时需要考虑性能问题,不能总是一味地给整个方法加锁synchronized就了事了,应该将尽量的缩小范围

  c、加锁的含义不仅仅局限于互斥,还包括可见性。为了确保所有线程都能看见最新值,读操作和写操作必须使用同样的锁对象

2、不共享状态:
  无状态对象: 无状态对象一定是线程安全的,因为不会影响到其他线程

  线程关闭: 仅在单线程环境下使用

3、不可变对象:
  可以使用final修饰的对象保证线程安全,由于final修饰的引用型变量(除String外)不可变是指引用不可变,但其指向的对象是可变的,所以此类必须安全发布,也即不能对外提供可以修改final对象的接口
  

十二、创建线程池有哪几种方式

1.Executors
2.ThreadPoolExecutor

十三、线程池七大参数介绍

 1.corePoolSize:线程池中常驻核心线程数

 2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1

 3.keepAliveTime:多余的空闲线程存活时间。当前线程池数量超过corePoolSize时,当空闲时间到达keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。

 4.unit:keepAliveTime的时间单位

 5.workQueue:任务队列,被提交但尚未执行的任务

 6.threadFactory:表示生成线程池中的工作线程的线程工厂,用于创建线程,一般为默认线程工厂即可

 7.handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝来请求的Runnable的策略

十四、线程池四大拒绝策略

1.AbortPolicy拒绝策略:这种策略在拒绝任务时,会直接抛出一个类型为RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。

2.DiscardPolicy 拒绝策略:这种策略是当任务提交时直接将刚提交的任务丢弃,而且不会给与任何提示通知,所以这种策略使用要慎重,因为有一定的风险,对我们来说根本不知道提交的任务有没有被丢弃

3.DiscardOldestPolicy 拒绝策略:这种策略和上面相似。不过它丢弃的是队列中的头节点,也就是存活时间最久的

4.CallerRunsPolicy 拒绝策略:这种策略算是最完善的相对于其他三个,当线程池无能力处理当前任务时,会将这个任务的执行权交予提交任务的线程来执行,也就是谁提交谁负责,这样的话提交的任务就不会被丢弃而造成业务损失,同时这种谁提交谁负责的策略必须让提交线程来负责执行,如果任务比较耗时,那么这段时间内提交任务的线程也会处于忙碌状态而无法继续提交任务,这样也就减缓了任务的提交速度,这相当于一个负反馈。也有利于线程池中的线程来消化任务

十五、线程池运行原理

1.如果线程池中的线程数量少于corePoolSize(核心线程数量),那么会直接开启一个新的核心线程来执行任务,即使此时有空闲线程存在.
2.如果线程池中线程数量大于等于corePoolSize(核心线程数量),那么任务会被插入到任务队列中排队,等待被执行.此时并不添加新的线程.
3.如果在步骤2中由于任务队列已满导致无法将新任务进行排队,这个时候有两种情况:
	a.线程数量 [未] 达到maximumPoolSize(线程池最大线程数) , 立刻启动一个非核心线程来执行任务.
	b.线程数量 [已] 达到maximumPoolSize(线程池最大线程数) , 拒绝执行此任务.ThreadPoolExecutor会通过RejectedExecutionHandler,抛出RejectExecutionException异常.

十六、多线程锁的升级原理是什么
锁的级别:

无锁->偏向锁->轻量级锁->重量级锁

锁分级别原因:

没有优化前,sychroniezed是重量级锁(悲观锁),使用wait、notify、notifyAll来切换线程状态非常消耗系统资源,线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以JVM对sychronized关键字进行了优化,把锁分为无锁、偏向锁、轻量级锁、重量级锁。

1.、无锁

	没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其它修改失败的线程会不断重试直到修改成功。
	
2、偏向锁

	对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。
	
	偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停偏向锁的线程,然后判断锁对象是否处于被锁定状态,如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁。
	
	如果线程处于活动状态,升级为轻量级锁的状态。

3、轻量级锁

	轻量级锁是指当锁是偏向锁的时候,被第二个线程B访问,此时偏向锁就会升级为轻量级锁,线程B会通过自旋的形式尝试获取锁,线程不会阻塞,从未提升性能。
	
	当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定次数时,轻量级锁边会升级为重量级锁,当一个线程已持有锁,另一个线程在自旋,而此时第三个线程来访时,轻量级锁也会升级为重量级锁。
	
	注:自旋是什么?
	
	自旋(spinlock)是指当一个线程获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

4、重量级锁

	指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
	
	重量级锁通过对象内部的监听器(monitor)实现,而其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

5、锁状态对比:

	偏向锁	轻量级锁	重量级锁
	使用场景	只有一个线程进入同步块	虽然很多线程,但没有冲突,线程进入时间错开因而并未争抢锁	发生了锁争抢的情况,多条线程进入同步块争用锁
	本质	取消同步操作	CAS操作代替互斥同步	互斥同步
	优点	不阻塞,执行效率高(只有第一次获取偏向锁时需要CAS操作,后面只是比对ThreadId)	不会阻塞	不会空耗CPU
	缺点	
	适用场景太局限。若竞争产生,会有额外的偏向锁撤销的消耗

	长时间获取不到锁空耗CPU	阻塞,上下文切换,重量级操作,消耗操作系统资源
6、CAS是什么呢?

	CAS即即比较和替换,当使用CAS替换新值不成功时,自旋,重新获得原值和新值再试一次直到成功为止。
	
	CAS通过无锁操作提高了系统的吞吐率,高效的解决了原子操作问题。

十七、死锁产生的4个必要条件?

产生死锁的必要条件:

1.互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
2.请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
4.环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

十八、预防死锁:

1.资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
2.只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
3.可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
4.资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
https://blog.csdn.net/hd12370/article/details/82814348

十九、说一下 synchronized 底层实现原理

1、jvm基于进入和退出Monitor对象来实现方法同步和代码块同步。

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor, 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。

代码块的同步是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

这里要注意:

synchronized是可重入的,所以不会自己把,自己锁死
synchronized锁一旦被一个线程持有,其他试图获取该锁的线程将被阻塞。

关于这两条指令的作用,我们直接参考JVM规范中描述:

monitorenter :

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

这段话的大概意思为:

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit: 

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

这段话的大概意思为:

执行monitorexit的线程必须是objectref所对应的monitor的所有者。

指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。 

  

通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

二十、synchronized 和 volatile 的区别是什么

1、volatile只能作用于变量,使用范围较小。synchronized可以用在方法、类、同步代码块等,使用范围比较广。
2、volatile只能保证可见性和有序性,不能保证原子性。而可见性、有序性、原子性synchronized都可以包证。
3、volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。

二十一、ynchronized 和 Lock 有什么区别
在这里插入图片描述
区别如下:

1.来源:
lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;

2.异常是否释放锁:
synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)

2.是否响应中断
lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;

3.是否知道获取锁
Lock可以通过trylock来知道有没有获取锁,而synchronized不能;

4.Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)

5.在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

6.synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度

二十二、synchronized 和 ReentrantLock 区别是什么?

1.synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果
2.synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间
3.synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁
4.synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法
5.synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现
6.synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放

二十三、说一下 atomic 的原理

二十四、volatile 变量和 atomic 变量有什么不同

首先,volatile 变量和 atomic 变量看起来很像,但功能却不一样。Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用volatile修饰count变量那么 count++ 操作就不是原子性的。而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值