仅对以下资料做整理!!! 如果版权问题,立刻删除!
参考资料:
新版Java面试专题视频教程,java八股文面试全套真题+深度详解(含大厂高频面试真题)_哔哩哔哩_bilibili
1、实现 Runnable 接口比继承 Thread 类所具有的优势?
- 避免了单继承的局限性
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
2、synchronized 与 Lock 的对比?
- 语法层面
- synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现。
- Lock 是接口,源码由 jdk 提供,用 java 语言实现。
- 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁。
- 功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能。
- Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断(当一个线程去竞争资源的时候,会进入(阻塞/等待)队列,如果可以用一种方法,让它从(阻塞/等待)队列出来,就叫可打断锁。)、可超时、多条件变量(在使用条件变量时,需要先获取关联的锁,并调用Condition的await()方法进入等待状态。此时,线程会释放锁并进入等待队列,直到另一个线程调用**signal()**或signalAll()方法,通知该线程条件已经满足。此时,该线程重新获取锁并继续执行。Java中的条件变量机制可以用于多个线程之间的同步和通信,例如生产者-消费者模型、读写锁模型等。)
- Lock 有适合不同场景的实现,如
ReentrantLock
,ReentrantReadWriteLock
。
- 性能层面
- 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖。
- 在竞争激烈时,Lock 的实现通常会提供更好的性能。
3、创建线程的四种方式?
-
继承Thread类
public class MyThread extends Thread { @Override public void run() { System.out.println("MyThread...run..."); } public static void main(String[] args) { // 创建MyThread对象 MyThread t1 = new MyThread() ; MyThread t2 = new MyThread() ; // 调用start方法启动线程 t1.start(); t2.start(); } }
-
实现runnable接口
public class MyRunnable implements Runnable{ @Override public void run() { System.out.println("MyRunnable...run..."); } public static void main(String[] args) { // 创建MyRunnable对象 MyRunnable mr = new MyRunnable() ; // 创建Thread对象 Thread t1 = new Thread(mr) ; Thread t2 = new Thread(mr) ; // 调用start方法启动线程 t1.start(); t2.start(); } }
-
实现Callable接口(带有返回值)
public class MyCallable implements Callable<String> { @Override public String call() throws Exception { System.out.println("MyCallable...call..."); return "OK"; } public static void main(String[] args) throws ExecutionException, InterruptedException { // 创建MyCallable对象 MyCallable mc = new MyCallable() ; // 创建F FutureTask<String> ft = new FutureTask<String>(mc) ; // 创建Thread对象 Thread t1 = new Thread(ft) ; Thread t2 = new Thread(ft) ; // 调用start方法启动线程 t1.start(); // 调用ft的get方法获取执行结果 String result = ft.get(); // 输出 System.out.println(result); } }
-
线程池创建线程
public class MyExecutors implements Runnable{ @Override public void run() { System.out.println("MyRunnable...run..."); } public static void main(String[] args) { // 创建线程池对象 ExecutorService threadPool = Executors.newFixedThreadPool(3); threadPool.submit(new MyExecutors()) ; // 关闭线程池 threadPool.shutdown(); } }
4、runnable 和 callable 有什么区别
- Runnable 接口run方法没有返回值;Callable接口call方法有返回值,是个泛
型,和Future、FutureTask配合可以用来获取异步执行的结果 - Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常、只能在内部消化,不能继续上抛。
5、线程的 run() 和 start() 有什么区别?
- start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
- run(): 封装了要被线程执行的代码,可以被调用多次。
6、线程包括哪些状态,状态之间是如何变化的?
-
线程的状态可以参考JDK中的Thread类中的枚举State
7、新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
-
通过join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
public class JoinTest { public static void main(String[] args) { // 创建线程对象 Thread t1 = new Thread(() -> { System.out.println("t1"); }) ; Thread t2 = new Thread(() -> { try { t1.join(); // 加入线程t1,只 有t1线程执行完毕以后,再次执行该线程 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2"); }) ; Thread t3 = new Thread(() -> { try { t2.join(); // 加入线程 t2,只有t2线程执行完毕以后,再次执行该线程 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t3"); }) ; // 启动线程 t1.start(); t2.start(); t3.start(); } }
8、在 java 中 wait 和 sleep 方法的不同?
共同点:wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态。
不同点:
- 方法归属不同
- sleep(long) 是 Thread 的静态方法。
- wait(),wait(long) 都是 Object 的成员方法,每个对象都有。
- 醒来时机不同
- 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来。
- wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
- 它们都可以被打断唤醒。
- 锁特性不同
- wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制。
- wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,但你们还可以用)。
- 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃cpu,你们也用不了)。
9、如何停止一个正在运行的线程?
-
使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
-
使用stop方法强行终止(不推荐,方法已作废)
-
使用interrupt方法中断线程
//2.打断正常的线程 Thread t2 = new Thread(()->{ while(true) { Thread current = Thread.currentThread(); boolean interrupted = current.isInterrupted(); if(interrupted) { System.out.println("打断状态:"+interrupted); break; } } }, "t2"); t2.start(); Thread.sleep(500);
10、synchronized关键字的底层原理是什么?
- Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
- 它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor
- 在monitor内部有三个属性,分别是owner、entrylist、waitset。其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程
- synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。一旦锁发生了竞争,都会升级为重量级锁。
- 重量级锁:底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的、上下文切换,成本较高,性能比较低。
- 轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性。
- 偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令。
11、你了解Java内存模型吗?
1、所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
2、 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
3.、线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
12、CAS 你知道吗?
- Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。在JUC( java.util.concurrent )包下实现的很多类都用到了CAS操作,如AQS、AtomicXXX类。
- 自旋锁操作
- 因为没有加锁,所以线程不会陷入阻塞,效率较高
- 如果竞争激烈,重试频繁发生,效率会受影响
- CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令
13、volatile关键字的作用你了解吗?
- 当前代码禁用了即时编辑器,保证线程间的可见性。
- 禁止进行指令重排序
- 写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下。因此,写变量让volatile修饰的变量的在代码最后位置。
- 读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上。因此,读变量让volatile修饰的变量的在代码最开始位置。
14、什么是AQS?
-
AbstractQueuedSynchronizer
,是阻塞式锁和相关的同步器工具的框架,它是构建锁或者其他同步组件的基础框架。 -
在去修改state状态的时候,使用的cas自旋锁来保证原子性,确保只能有一个线程修改成功,修改失败的线程将会进入FIFO队列中等待。
-
工作机制:
- 在AQS中维护了一个使用了volatile修饰的state属性来表示资源的状态,0表示无锁,1表示有锁。
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList。
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的WaitSet。
进一步探究:AQS是公平锁吗,还是非公平锁?
- 新的线程与队列中的线程共同来抢资源,是非公平锁。
- 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁。
- 比较典型的AQS实现类ReentrantLock,它默认就是非公平锁,新的线程与队列中的线程共同来抢资源。
15、你知道ReentrantLock的特点和实现原理吗?
特点:
- 可中断
- 可以设置超时时间
- 可以设置公平锁
- 支持多个条件变量
- 与synchronized一样,都支持重入
实现原理:主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。
实现流程:
- 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功。
- 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部。
- 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程。
- 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁。
16、如何进行死锁诊断?
-
使用jps查看运行的线程
-
使用jstack查看线程运行的情况
jstack -l 46032
- jconsole:用于对jvm的内存,线程,类 的监控,是一个基于 jmx 的 GUI 性能监控工具。打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 。
- VisualVM:故障处理工具,能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈。打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe。
17、ConcurrentHashMap你知道如何保证线程安全的吗?
-
JDK1.7底层采用分段的数组+链表实现。在进行操作数据的之前,会先判断当前segment对应下标位置是否有线程进行操作,为了线程安全使用的是ReentrantLock进行加锁,如果获取锁是被会使用cas自旋锁进行尝试。
-
JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
- CAS控制数组节点的添加
- synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题 , 效率得到提升。
18、导致并发程序出现问题的根本原因是什么?以及如何解决?
- 破坏原子性、内存可见性以及有序性。
- 原子性:synchronized或者lock加锁
- 内存可见性:加锁或者volatile(推荐)
- 有序性:volatile
19、线程池的种类有哪些?
- 第一个是:
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。(核心线程0,应急线程无限个) - 第二个是:
newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列 中等待。 - 第三个是
newScheduledThreadPool
:创建一个定长线程池,支持定时及周期性任务执行。 - 第四个是
newSingleThreadExecutor
: 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
20、线程池的核心参数有哪些?
corePoolSize
核心线程数目 - 池中会保留的最多线程数。maximumPoolSize
最大线程数目 - 核心线程+救急线程的最大数目。keepAliveTime
生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放。unit
时间单位 - 救急线程的生存时间单位,如秒、毫秒等。workQueue
- 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务。threadFactory
线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等。handler
拒绝策略 - 当所有线程都在繁忙,workQueue
也放满时,会触发拒绝策略。
继续探究:有哪些拒绝策略?
当线程数过多以后,第一种是抛异常、第二种是由调用者执行任务(main主线程执行)、第三是丢弃当前的任务,第四是丢弃最早排队任务。默认是直接抛异常。
继续探究:线程池中有哪些常见的阻塞队列?
1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的。
4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
21、如何确定核心线程池呢?
- IO密集型任务(并发不高、任务执行时间长):2N+1 (N为计算机的CPU核数)
- CPU密集型任务(高并发、任务执行时间短):N+1
- 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器
是第二步。
22、线程池的执行原理知道吗?
首先判断线程池里的核心线程是否都在执行任务,如果不是则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任 务。如果已经满了,则交给拒绝策略来处理这个任务。
23、为什么不建议使用Executors创建线程池呢?
24、如果控制某一个方法允许并发访问线程的数量?
- 使用jdk提供的Semaphore类(信号量)。
semaphore.acquire()
请求信号量,可以限制线程的个数,是一个正数,如果信号量是-1,就代表已经用完了信号量,其他线程需要阻塞了。semaphore.release()
代表是释放一个信号量,此时信号量的个数+1。
25、你在项目中哪里用了多线程?
- es数据批量导入:在我们项目上线之前,我们需要把数据量的数据一次性的同步到es索引库中,但是当时的数据好像是1000万左右,一次性读取数据肯定不行(oom异常),如果分批执行的话,耗时也太久了。所以,当时我就想到可以使用线程池的方式导入,利用
CountDownLatch
(计数器)+Future
(线程返回值) 来控制,就能大大提升导入的时间。 - 数据汇总:在用户下单之后需要查询订单信息,也需要获得订单中的商品详细信息(可能是多个),还需要查看物流发货信息。因为它们三个对应的分别三个微服务,如果一个一个的操作的话,互相等待的时间比较长。所以,我当时就想到可以使用线程池,让多个线程同时处理,最终再汇总结果就可以了,当然里面需要用到Future来获取每个线程执行之后的结果才行。
- 异步工作: 我当时做了一个文章搜索的功能,用户输入关键字要搜索文章,同时需要保存用户的搜索记录(搜索历史),这块我设计的时候,为了不影响用户的正常搜索,我们采用的异步的方式进行保存的,为了提升性能,我们加入了线程池,也就说在调用异步方法的时候,直接从线程池中获取线程使用。
26、说说ThreadLocal的功能和底层实现?
主要功能:实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题;实现了线程内的资源共享。
底层原理:
- 在ThreadLocal内部维护ThreadLocalMap 类型的成员变量,用来存储资源对象。
- 当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为value,放入当前线程的 ThreadLocalMap 集合中。
- 当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值。
- 当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值。
继续探究:ThreadLocal为什么会导致内存溢出?
- ThreadLocalMap 中的 key 被设计为弱引用,它是被动的被GC调用释放key,不过关键的是只有key可以得到内存释放,而value不会,因为value是一个强引用。
- 在使用ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收,建议主动的remove 释放 key,这样就能避免内存溢出。