Java八股文面试之多线程篇

线程有哪几种状态。

(1)NEW
线程至今尚未启动
(2)RUNNABLE
线程正在 Java 虚拟机中执行
(3)BLOCKED
受阻塞并等待获得同步代码块的锁
(4)WAITING
无限期地等待另一个线程来执行某一特定操作
(5)TIMED_WAITING
在指定的时间内等待另一个线程来执行某一特定操作
(6)TERMINATED
线程已退出
注意:RUNNABLE 是否真的运行取决于调度器,由操作系统内核来决定,可以细分为Ready就绪和Running运行

创建多线程有几种方法?

通过继承Thread类或者实现Runnable接口、Callable接口(JDK>=1.5)都可以实现多线程,不过实现Runnable接口与实现Callable
接口的方式基本相同,只是Callable接口里定义的方法返回值,可以声明抛出异常而已。因此,将实现Runnable接口
和实现Callable接口可以归为一种方式。
(1)实现Runnable、Callable接口的方式创建线程的优缺点
①优点:线程类只是实现了Runnable接口或者Callable接口,还可以继承其他类。这种方式下,多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。
②缺点:编程稍微复杂一些,如果需要访问当前线程,则必须使用Thread.currentThread()方法
(2)继承Thread类的方式创建线程的优缺点
①优点:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获取当前线程
②缺点:因为线程类已经继承了Thread类,java语言是单继承的,所以就不能再继承其他父类了。

如何停止一个正在运行的线程

(1)当线程退出后,会自动正常退出,也就是当run方法完成后线程终止
(2)使用stop方法强行终止,但是不推荐;因为stop方法是个废弃方法
(3)使用interrupt方法中断线程,但并没有真正的结束线程,所以一般用于中断正在休眠的线程

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

(1)sleep()方法属于Thread类,可以在任何地方调用;wait()方法属于Object类,需要和 synchronized 一起用。
(2)sleep()方法不会释放锁,线程会一直占用CPU资源。wait()方法会释放锁,让出CPU资源,并且线程进入等待状态,直到被其他线程唤醒。
(3)sleep()方法会自动苏醒,不需要其他线程唤醒。wait()方法需要被notify()或notifyAll()方法唤醒。
(4)sleep()方法通常用于让线程暂停执行一段时间,例如模拟延迟操作。wait()方法通常用于线程间通信和协作,例如生产者-消费者模型。

Thread 类中的start() 和 run() 方法有什么区别?

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的
效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()
方法才会启动新线程。

Thread类中的yield方法有什么作用?

yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法
而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可
能在进入到暂停状态后马上又被执行。

乐观锁和悲观锁的理解以及实现

(1)**乐观锁:**每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断
一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
(2)乐观锁的实现方式:
①使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可
以采取丢弃和再次尝试的策略。
②java中的Compare and Swap即CAS ,当多个线程尝试使用CAS同时更新同一个变量时,只有其
中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争
中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比
较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自
动将该位置值更新为新值B。否则处理器不做任何操作
**(3)悲观锁:**总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据
的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做
操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
(4)悲观锁的实现方式:synchronized和lock
①synchronized是Java关键字,而lock是一个类
②synchronized获取锁的时候,假设A线程获得锁,B等待,若A阻塞,B会自治等待;而lock在这种情况下
B会尝试去获取锁
③synchronized的锁的状态是无法判断的,而lock是可以判断的
④synchronized和lock都是对象锁,都支持重入锁
⑤lock可以实现synchronized不具备的特性,如响应中断,支持超时,公平锁和共享锁
(共享锁:在同一时刻,可以有多个线程用有锁;独占锁:synchronized,在任一时刻,只有一个线程可以拥有此锁)

有三个线程T1,T2,T3如何保证顺序执行

可以使用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的
顺序应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。先启动三个线程中哪一个都行,因
为在每个线程的run()方法中都用join方法限定了三个线程的执行顺序。

public class JoinTest2 {
// 1.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行
public static void main(String[] args) {
	final Thread t1 = new Thread(new Runnable() {
		@Override
		public void run() {
			System.out.println("t1");
	}
});
	final Thread t2 = new Thread(new Runnable() {
		@Override
		public void run() {
			try {
		// 引用t1线程,等待t1线程执行完
				t1.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
				System.out.println("t2");
		}
	});
	Thread t3 = new Thread(new Runnable() {	
	@Override
	public void run() {
		try {
			// 引用t2线程,等待t2线程执行完
			t2.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("t3");
	}

线程池是什么?线程池ThreadPoolExecutor的参数分别表示什么?

https://editor.csdn.net/md/?articleId=128745907

常用的线程池有哪些?分别有什么特点?

(1)newSingleThreadExecutor:创建一个单线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行
(2)newFixedThreadPool:是一种固定线程数量的线程池
特点:核心线程和最大线程数量都是一个固定的值。如果任务比较多工作线程处理不过来,就会加入到阻塞队列
里面等待。
(3)newCachedThreadPool:是一种可以缓存的线程池,它可以用来处理大量短期的突发流量
特点:①最大线程数是Interger.MaxValue②线程存活时间是60秒③阻塞队列用的是SynchronousQueue
(4)newScheduledThreadPool:具有延迟执行功能的线程池,可以用来实现实时调度
(5)newWorkStealingPool:Java8里面新加入的一个线程池,它内部会创建一个ForkJoinPool,利用工作窃取的算法并行处理请求

线程池的拒绝策略

JDK 提供了 4 种内置的拒绝策略:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy 和
DiscardPolicy;
①AbortPolicy:这是线程池默认的拒绝策略,当任务不能再提交的时候抛出异常,让开发人员及时知道程序运行状态,这样能在系统不能承载更大的并发量时,及时通过异常信息发现;
②DiscardPolicy:当任务不能再提交的时候直接丢弃任务,不抛出异常,这样可能会使我们无法发现系统的异
常状态,对于一些无关紧要的业务可以采用此策略;
③DiscardOldestPolicy:当任务不能再提交的时候丢弃任务队列中靠最前的任务,然后重新提交被拒绝的任务,对于一些允许丢弃老任务的业务可以采用此策略;
④CallerRunsPolicy: 当任务不能再提交的时候交由任务的调用线程(提交任务的线程,如main线程)来执行当前任务;这种拒绝策略会让所有任务都能得到执行,适合大量计算类型的任务执行。
⑤除了上面的四种拒绝策略,还可以通过实现 RejectedExecutionHandler 接口,实现自定义的拒绝策略

线程池的工作流程

在这里插入图片描述

提交任务后:
(1)判断当前线程池里的核心线程是否达到最大。
①如果不是,创建一个新的工作线程来执行任务。
②否则,则进入下个流程。
(2)判断线程池任务队列是否已满。
①如果任务队列没有满,则将任务添加到任务队列中等待线程执行。
②如果任务队列已满,则进入下个流程。
(3)判断线程池里的线程是否达到最大线程数。
①如果没有,则创建非核心线程执行任务。
②如果已满,则执行饱和策略。

ThreadLocal

说说ThreadLocal原理?

(1)ThreadLocal相当于一个线程本地变量,他会在每个线程中都创建一个副本;在线程之间访问内部
副本变量,就可以做到线程之间互相隔离,相比于synchronized它是在用空间来换时间。
(2)ThreadLocal中有一个静态内部类ThreadLocalMap,ThreadLocalMap又存在一个Entry数组,Entry具备保存key、value键值对的能力。Entry是一个弱引用,他的key是指向ThreadLocal的弱引用,
(3)使用弱引用的目的是为了防止内存泄露,如果使用的是强引用的话,除非线程结束否则ThreadLocal对象始终无
法被回收,使用弱引用的话会则在下一次GC的时候被回收,但是这样还是会存在内存泄露的问题。假如key和
ThreadLocal对象被回收之后,entry中存在key为null,但是value有值的entry对象,就会造成永远没办法被
访问到,除非线程运行结束。但是只要我们合理使用,在每次使用完之后都调用remove方法删除
Entry对象,实际上是不会出现这个问题的。

Thread、ThreadLocal、ThreadLocalMap关系

https://editor.csdn.net/md/?articleId=137903828

ThreadLocal会造成内存泄漏吗?如何避免内存泄漏

(1)ThreadLocal会造成内存泄漏吗?
①ThreadLocalMap内部维护了一个Entry类来存储键值对的映射关系,Entry将ThreadLocal作为Key,值作为value保存,它继承自弱引用WeakReference。由于ThreadLocal对象是弱引用,如果外部没有强引用指向它,它就会被GC回收,导致Entry的Key为null,如果这时value外部也没有强引用指向它,那么value就永远也访问不到了,按理也应该被GC回收,但是由于Entry对象还在强引用value,导致value无法被回收,这时「内存泄漏」就发生了,value成了一个永远也无法被访问,但是又无法被回收的对象。
(2)如何避免内存泄漏:
①将ThreadLocal变量存储的对象设置是不可变,因为不可变的对象不需要频繁更新
②尽量避免存储大对象,如果非要存,那么尽量在访问完成后及时调用remove()删除掉。

ThreadLocal的使用场景

(1)当我们在写API接口的时候,通常Controller层会接受来自前端的入参,当这个接口功能比较复杂的时候,可能我们调用的Service层内部还调用了 很多其他的很多方法,通常情况下,我们会在每个调用的方法上加上需要传递的参数。
但是如果我们将参数存入ThreadLocal中,那么就不用显式的传递参数了,而是只需要ThreadLocal中获取即可。
(2)当用户登录后,会将用户信息存入Token中返回前端,当用户调用需要授权的接口时,需要在header中携带 Token,然后拦截器中解析Token,获取用户信息,调用自定义的类(AuthNHolder)存入ThreadLocal中,当请求结束的时候,将ThreadLocal存储数据清空, 中间的过程无需在关注如何获取用户信息,只需要使用工具类的get方法即可。
(3)我们项目有个DateUtils工具类,这个工具类主要是对时间进行格式化,格式化/转化的实现是用的SimpleDateFormat。SimpleDateFormat不是线程安全的,所以我们就用ThreadLocal来让每个线程装载着自己的SimpleDateFormat对象

多线程有什么用?

(1)发挥多核CPU的优势
随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至
16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费
了75%。单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过
线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线
程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用
CPU的目的。
(2)防止阻塞
从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运
行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,
就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取
某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止
运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,
也不会影响其它任务的执行。
(3)便于建模
这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,
建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务
D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

死锁是什么?什么条件下会产出死锁,如何避免死锁?

(1)死锁,是指两个或者两个以上的线程在执行过程中,去争夺同一份共享资源导致相互等待无法继续执行。
(2)产生死锁的原因:
①第一个:互斥条件,共享资源a和b只能被一个线程占用;
②第二个:请求和保持条件,线程T1已经获取共享资源a,在等待共享资源b的时候,不释放共享资源a;
③第三个:不可抢占条件,其他线程不能强行抢占线程T1占有的资源;
④第四个:循环等待条件,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,这形成了循环等待。
(4)线程产生死锁之后,只能通过外部干预来解决问题,比如重启程序,或者Kill线程。所以,我们只能在写代码时规避死锁的产生。那么如何避免死锁产生呢?根据产生死锁的四个必要条件,我们只需要破坏其中任何一 个条件就可以解决。
①第一个互斥条件是没有办法被破坏的,因为它是互斥锁的基本约束。其他三个条件都可以通过人工干预来破坏。
②第二个请求保持条件,我们可以在首次执行一次性申请所有的资源,这样就不存在等待锁的问题了。
③第③个,对于不可抢占条件来说,占用部分资源的线程在进一步申请其他资源的时候如果申请不到,我们可以主动释放它占有的资源。这样不可抢占这个条件就被破坏了。
④第④个,对于循环等待条件来说,可以通过按序申请资源来预防死锁的产生。所谓按序申请,就是给资源编号,所有线程可以按照线性化的序号顺序去申请共享资源,先申请序号小的,再申请序号大的,这样循环等待自然就不存在了。

锁的升级过程(无锁->偏向锁->轻量级锁->重量级锁)

(1)偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入偏向锁。只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有
(2)轻量级锁:
轻量级锁是由偏向所升级来的,如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是synchronized
(3)重量级锁
如果在尝试加轻量级锁的过程中,CAS操作无法成功,说明有其他线程为此对象已经加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

synchronized和lock锁的区别

(1)synchronized是关键字,Lock类是一个接口
(2)Lock只有代码块锁,synchronized有代码块锁和方法锁
(3)synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁
(4)synchronized会自动释放锁(a:线程执行完同步代码会释放锁 ;b:线程执行过程中发生异常会释放锁);Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁
(5)用synchronized关键字修饰的两个线程1和线程2,如果线程1阻塞,线程2则会一直等待下去。Lock锁可以设置超时时间,不用一直等待下去。
(6)synchronized的锁是可重入、不可中断、非公平。Lock锁可重入、可中断、可公平(也可非公平)
(7)synchronized锁适合代码少量的同步问题,Lock锁适合大量代码的同步问题

CountDownLatch(倒计时锁)

用来进行线程同步协作,等待所有线程完成倒计时才恢复运行

public static void main(String[] args) throws InterruptedException {
	CountDownLatch latch = new CountDownLatch(3);
	ExecutorService service = Executors.newFixedThreadPool(4);
 	service.submit(() -> {
		log.debug("begin...");
 		sleep(1);
 		latch.countDown();
 		log.debug("end...{}", latch.getCount());
 	});
 	service.submit(() -> {
 		log.debug("begin...");
 		sleep(1.5);
 		latch.countDown();
 		log.debug("end...{}", latch.getCount());
 	});
 	service.submit(() -> {
 		log.debug("begin...");
 		sleep(2);
 		latch.countDown();
 		log.debug("end...{}", latch.getCount());
 	});
 	service.submit(()->{
 		try {
			log.debug("waiting...");
 			latch.await();
 			log.debug("wait end...");
 		} catch (InterruptedException e) {
 			e.printStackTrace();
 		}
 	});
}

并发编程为什么有可见性问题

(1)一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性.
(2) 在单核时代,线程都是在一个CPU 上执行,操作的是同一个 CPU 的缓存,一个线程对缓存的写,对另外一个线程来说一定是可见的.
(2) 在多核时代,线程会在不同的 CPU 上执行时,操作的是不同的 CPU 缓存;当出现 线程 A 操作的是 CPU-1上的缓存,而线程 B 操作的是 CPU-2上的缓存,这个时候线程 A 对共享变量 V 的操作对于线程 B 而言就不具备可见性了

volatile关键字如何解决的可见性问题

(1)Java代码的运行是先将Java代码编译成字节码,然后JVM加载字节码最终转换成汇编指令在CPU上运行,被volatile修饰的字段会在其对应的汇编操作指令上加个lock前缀指令,这个指令就可以解决上述的两种可见性问题。
(2)处理器在接收到lock指令的时候会做两件事:
①将缓存行缓存的数据写回到系统内存中。
②这个写回到系统内存中的数据,如果在其他CPU的缓存行中存在相同的数据,则将其置为失效状态。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值