【面试八股|JAVA多线程】JAVA多线程常考面试题详解
这里根据个人说话口吻等编写JAVA多线程常见面试题用于记录复习,后续会持续更新补充,欢迎点赞收藏。
多线程
线程基础知识
线程与进程区别
1.进程是正在运行的线程实例,进程里包含了线程,每个线程执行不同任务
2.不同进程使用不同内存空间,同一进程的所有线程共享内存空间
3.线程更轻量,上下文切换成本较低
并行与并发的区别
并行是同一时间处理多个任务的能力,比如四核cpu同时执行四个线程
并发是同一时间应对多个任务的能力,比如一个cpu被多个线程轮流使用
创建线程四种方式
1.继承thread类
2.实现runnable接口
3.实现callable接口
4.线程池创建线程
runnable与callable接口区别
1.runnable接口run方法没有返回值。callable接口call方法有返回值,是个泛型,可通过futuretask配合获取异步执行结果
2.runnable接口run方法的异常只能内部消化,不能上抛,callable接口的call方法允许抛出异常
3.callabe接口获取执行结果,需要通过futuretask.get()得到,但会阻塞主线程
futuretask的作用
1.thread类只接受runnable,futuretask实现runnable来封装callable
2.实现future,提供管理callable任务的能力
run和start的区别
start原来启动线程,通过该线程调用run方法逻辑,只能调用一次。
run则封装了被线程执行的代码,可以被调用多次
线程有哪些状态,之间如何变换的
1.在thread类中的枚举state中定义了六种状态,分别是:新建,可运行,终结,阻塞,等待,有时限等待
2.当线程对象被创建但为start则处于新建状态,当线程start则进入可运行状态,当线程内代码执行完毕则进入终结状态
3.当线程获取锁失败,则由可运行转换为阻塞状态,如果后续获取了锁则再加入可运行状态
4.当线程获取锁,但由于条件不满足,调用了wait方法,则进入等待状态。当其它持锁线程调用了notify或notifyall,则再恢复为可运行状态
5.当线程获取锁,但由于条件不满足,使用sleep方法或传参的wait方法则进入有时限等待状态。在时间结束后会再重新进入可运行状态
如何保证新建的多个线程顺序执行
可通过线程类的join方法在一个线程中启动另一个线程,另一个线程完成后该线程才会执行,以此控制顺序
notify与notifyall的区别
notify,随机唤醒一个线程
notifyall,唤醒所有wait线程
在 java 中 wait 和 sleep 方法的不同
共同点:都是让线程暂时放弃cpu使用权,进入阻塞状态
不同点:
方法归属不同:sleep是Thread的静态方法。wait是object成员方法,每个对象都有。
醒来时机不同:wait有无参方法和有参方法,wait方法可以被notify唤醒,无参构造如果不被唤醒会一直等下去。sleep无法被notify唤醒。两个都可以被打断唤醒。
锁特性不同:wait调用必须获取对象锁,执行后会释放对象锁。sleep不需要提前获取对象,执行后也不会释放对象锁。
如何停止一个正在运行的线程?
1.使用退出标志,在run方法完成后线程终止
2.使用interrupt方法中断线程
3.使用stop方法强行终止(已废除)
线程中并发锁
synchronized关键字底层原理
第一,synchronized底层使用了jvm级别的monitor来管理当前线程是否获取锁。属于悲观锁并且相对性能较低。
第二,monitor存在于每个java对象的对象头中,也因此java任何对象都可以作为锁。
第三,monitor内部维护了三个变量,分别是waitset,entrylist,owner。当线程获取锁,则会关联owner队列。当线程阻塞,则会关联到entryset。当对象等待时,则关联到wait队列中。
第四,当获取锁的对象释放锁后,entryset等待的线程将竞争锁,此过程是非公平的
synchronized锁进阶原理
第一,synchronized对象锁有偏向锁,轻量级锁,重量级锁三种形式。分别对应单个线程持有锁,多个线程交替持有锁,多个线程竞争锁的情况
第二,重量级锁通过Monitor实现,需要用户态与内核态的切换,进程上下文切换,成本较高,性能较低
第三,轻量级锁借助LockRecord实现,通过cas操作修改对象头锁标志,大幅提高性能。
第四,偏向锁只有第一次获取锁时是cas操作,后续再获取锁只需要判断对象头中是否保存为当前线程id即可。
第五,如果发生锁竞争都会升级为重量级锁
对象内存结构
对象在内存中的存储可被分为三部分:对象头,实例数据,对齐数据(为了使整体达到8的整数倍)

对象头(MarkWord)又在不同锁的情况下被划分为不同部分

-
hashcode:25位的对象标识Hash码
-
age:对象分代年龄占4位
-
biased_lock:偏向锁标识,占1位 ,0表示没有开始偏向锁,1表示开启了偏向锁
-
thread:持有偏向锁的线程ID,占23位
-
epoch:偏向时间戳,占2位
-
ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位
-
ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位
-
标记为gc表示该对象将被回收
ReentrantLock实现原理
第一,ReentrantLock底层使用CAS+AQS队列实现,提供state表示资源状态,exclusiveownerthread表示获取锁的线程,以及fifo队列表示阻塞线程
第二,reentrantlock是可重入锁,再次调用lock不会阻塞而是内部添加重入次数
第三,reentranlock支持公平锁与非公平锁,构造方法接受可选的公平参数,设置为true为公平锁,否则为非公平锁
ReentrantLock与synchronized的区别
1.功能层面:两者都是可重入锁,悲观锁,但reentrantlock增加了一些高级功能,如等待可中断,可实现公平锁,可借助condition实现选择性通知
2.语法层面:synchronized是关键字,依赖于jvm实现,退出同步代码块锁会自动释放。reentrantlock是依赖于jdk层面实现,需要手动调用unlock释放锁。
3.性能层面:没有竞争时,synchronized有需要优化比如偏向锁,轻量级锁等。竞争激烈时,reentrantlock实现提供了更好性能
谈谈JMM
第一,jmm是java虚拟机规范中定义的重要的内存模型,主要用来描述线程共享变量的访问,存储,读取等规则。
第二,jmm规范了共享变量,如实例变量类变量,都是存储在主内存中,也就是计算机的ram。
第三,jmm规范了每个线程都有自己的工作内存,线程对变量的操作都必须在自己工作内存中完成,而不能直接读取主内存的变量
第四,jmm规范了不同线程不能直接读取对方工作内存的变量,如果线程间需要传递变量值,这个过程必须通过主内存完成
介绍一下CAS
第一,CAS的全程是compare and swap,先比较再交换。体现的是一种乐观锁思想,在无锁状态下保证线程操作数据的原子性。
第二,在操作共享变量时会使用自旋锁,如果更新失败会不断尝试获取新值,直到更新成功
第三,CAS应用非常广泛,在AQS框架,原子类等地方都有使用。
第四,CAS底层使用的是Unsafe类中的方法,由操作系统提供,非java语言实现
介绍一下volatile
volatile是一个可修饰成员变量,类变量的关键字,主要有两个功能
第一,保证了不同线程对于被修饰变量的可见性。即某线程修改了该变量,volatile将强制将被修改的值写入主存对其它线程可见。
第二,禁止指令的重排序。通过添加内存屏障可禁止内存屏障前后的指令执行重排序优化。
介绍一下AQS
AQS其实就是jdk提高的一个类AbstractQueuedSynchronzier,是阻塞式锁和相关同步器工具的框架,用于提供提供「state 状态管理、FIFO 队列、线程阻塞 / 唤醒」的通用逻辑。
其常见实现类有ReentrantLock(可重入锁),Semaphore(信号量),CountDownLatch(倒计时锁)等。
其内部有个被volatile修饰的state属性来表示资源状态,默认state等于0,表示没有获取锁,state为1时表示获取了锁。通过cas操作修改state保证原子性。
AQS内部还提高了基于FIFO的等待队列管理阻塞线程,通过head指向队列最久的元素,tail指向队列最后一个元素
死锁产生条件是什么
当两个线程互相持有对方需要的锁则会产生死锁现象。
死锁产生的必要条件有,互斥条件,请求和保持条件,不剥夺条件,循环等待条件
如何进行死锁诊断
可以通过jdk自带的工具解决。首先通过jps查看当前java程序运行的进程id,然后通过jstack查看当前进程id,定位出代码具体行号进行解决。
我们也可以通过jconsole,visualvm等可视化工具解决。
导致并发程序出现问题的根本原因是什么
1.与java并发编程的三大特性相关,原子性,可见性,有序性
2.对于原子性,我们可以通过synchronized关键字或lock锁
3.对于可见性,可使用synchronized关键字,lock锁或volatile关键字解决
4.对于有序性,可通过volatile关键字避免指令重排序
线程池
说说线程池核心参数
1.共七个参数分别是:核心线程数,线程最大数量,生存时间,时间单位,阻塞队列,线程工厂,拒绝策略
2.拒绝策略有AbortPolicy直接抛出异常,默认策略,CallerRunsPolicy使用调用者所在线程执行任务,DiacardOldestPolicy丢弃最考前线程,DiscardPolicy直接丢弃任务
3.最常用阻塞队列有两种,ArrayBlockingQueue与LinkedBlockingQueue

说说常用的阻塞队列
常见的有四种ArrayBlockingQueue,LinkedBlockingQueue,DelayedWorkQueue,SynchronousQueue。
1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的
4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
ArrayBlockingQueue的LinkedBlockingQueue区别
| LinkedBlockingQueue | ArrayBlockingQueue |
|---|---|
| 默认无界,支持有界 | 强制有界 |
| 底层是链表 | 底层是数组 |
| 是懒惰的,创建节点的时候添加数据 | 提前初始化 Node 数组 |
| 入队会生成新 Node | Node需要是提前创建好的 |
| 两把锁(头尾) | 一把锁 |
1.LinkedBlockingQueue默认无界支持有界,ArrayBlockingQueue强制有界
2.ArrayBlockingQueue基于数组实现,因此由于可通过索引访问元素,可能更快。LinkedBlockingQueue基于链表实现,因此其添加和删除元素不需要移动其它元素可能更快。
3.ArrayBlockingQueue使用一把锁控制对队列的访问,读写操作互斥。LinkedBlockingQueue使用两把锁,读写操作不互斥,并发性高
如何确定核心线程数
1.对于高并发,执行时间短的场景,采用cpu+1,减少线程上下文切换
2.对于并发不高,执行时间长的场景,如果是io密集型任务则cpu*2+1,如果是计算密集型任务则cpu+1
3.并发高,业务执行长,则从整体架构方面设计,比如缓存,增加服务器等。
线程池的种类有哪些
newFixedThreadPool 创建一个定长线程池,无救急线程,可控制线程最大并发数,超出的线程会在队列 中等待。
newCachedThreadPool创建一个可缓存线程池,全是救急线程,如果线程池长度超过处理需要,可灵活回 收空闲线程,若无可回收,则新建线程。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
为什么不建议Executors创建线程池
不够灵活,队列长度都是Integer.MAX_VALUE,容易堆积请求导致oom。一般推荐使用threadpoolexecutor创建线程池,将控制权把握在程序员手中
说一说CountDownLatch
countdownlatch用于进行线程的同步协同。
具体使用:
1.构造参数初始化等待计数值
2.await等待计数归零
3.countDown让计数减一
如何控制某个方法运行并发访问线程的数量
通过semaphore类。先通过构造方法传参确定可被访问的线程数量,然后acquire表示请求信号量,当信号量为-1时则阻塞,release表示释放信号量。
介绍ThreadLocal
ThreadLocal 操作当前thread内部的threadlocalmap对象吗。
1.隔离资源对象,使每个线程各用各的资源对象。实现线程内资源共享。
2.ThreadLocal底层维护了一个ThreadLocalMap类型的成员变量。使用set方法时就是把threadlocal,自己作为key,资源对象作为value,存入threadlocalmap里。
3.Threadlocal的key被设计为弱引用,而value是强引用。而ThreadLocal通常是静态的,导致entry的key一直有效,如果不手动释放可能会导致oom












