Java多线程相关面试题

仅对以下资料做整理!!! 如果版权问题,立刻删除!

参考资料:
新版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 有适合不同场景的实现,如 ReentrantLockReentrantReadWriteLock
  • 性能层面
    • 在没有竞争时,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、runnablecallable 有什么区别

  1. Runnable 接口run方法没有返回值;Callable接口call方法有返回值,是个泛
    型,和Future、FutureTask配合可以用来获取异步执行的结果
  2. Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
  3. Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常、只能在内部消化,不能继续上抛

5、线程的 run()start() 有什么区别?

  • start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次
  • run(): 封装了要被线程执行的代码,可以被调用多次。

6、线程包括哪些状态,状态之间是如何变化的?

  • 线程的状态可以参考JDK中的Thread类中的枚举State

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PpAG9f5D-1689326073272)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/531f1ef9-53fb-407f-b673-b6f6f04a8b1f/Untitled.png)]

7、新建 T1T2T3 三个线程,如何保证它们按顺序执行?

  • 通过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、在 javawaitsleep 方法的不同?

共同点: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内存模型吗?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EKvPCQuN-1689326073274)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ec5e48c3-f7db-4a54-b651-8b469e050a3a/Untitled.png)]

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队列中等待。

  • 工作机制:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TxOLPU2Z-1689326073276)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/824b9f8b-7e98-4b1a-b591-ecd343cc1479/Untitled.png)]

    • 在AQS中维护了一个使用了volatile修饰的state属性来表示资源的状态,0表示无锁,1表示有锁。
    • 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList。
    • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的WaitSet。

进一步探究:AQS是公平锁吗,还是非公平锁?

  • 新的线程与队列中的线程共同来抢资源,是非公平锁。
  • 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁。
  • 比较典型的AQS实现类ReentrantLock,它默认就是非公平锁,新的线程与队列中的线程共同来抢资源。

15、你知道ReentrantLock的特点和实现原理吗?

特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置公平锁
  • 支持多个条件变量
  • 与synchronized一样,都支持重入

实现原理:主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。

实现流程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XVJcn54D-1689326073277)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/48cb15f1-f398-44f0-b4be-a1568988d065/Untitled.png)]

  • 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功。
  • 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部。
  • 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程。
  • 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁。

16、如何进行死锁诊断?

  • 使用jps查看运行的线程

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dyqY2YZY-1689326073278)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2f0403c0-e67d-4d25-aab9-7e5eef742048/Untitled.png)]

  • 使用jstack查看线程运行的情况 jstack -l 46032

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qi3ltI5f-1689326073279)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/35eedf93-f1ec-43c4-9976-f4cf8212a11d/Untitled.png)]

  • jconsole:用于对jvm的内存,线程,类 的监控,是一个基于 jmx 的 GUI 性能监控工具。打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 。
  • VisualVM:故障处理工具,能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈。打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe。

17、ConcurrentHashMap你知道如何保证线程安全的吗?

  • JDK1.7底层采用分段的数组+链表实现。在进行操作数据的之前,会先判断当前segment对应下标位置是否有线程进行操作,为了线程安全使用的是ReentrantLock进行加锁,如果获取锁是被会使用cas自旋锁进行尝试。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DXJRhYgR-1689326073280)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0ed962de-db69-4631-8571-ad1b79e0e8cd/Untitled.png)]

  • JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iaj9eoGJ-1689326073280)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2e5d4194-8f2c-48fb-b331-332b9bda7b8e/Untitled.png)]

    • CAS控制数组节点的添加
    • synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题 , 效率得到提升。

18、导致并发程序出现问题的根本原因是什么?以及如何解决?

  • 破坏原子性、内存可见性以及有序性。
  • 原子性:synchronized或者lock加锁
  • 内存可见性:加锁或者volatile(推荐)
  • 有序性:volatile

19、线程池的种类有哪些?

  • 第一个是: newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。(核心线程0,应急线程无限个)
  • 第二个是: newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列 中等待。
  • 第三个是newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。
  • 第四个是newSingleThreadExecutor: 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

20、线程池的核心参数有哪些?

  1. corePoolSize 核心线程数目 - 池中会保留的最多线程数。
  2. maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目。
  3. keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放。
  4. unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等。
  5. workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务。
  6. threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等。
  7. handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略。

继续探究:有哪些拒绝策略?

当线程数过多以后,第一种是抛异常、第二种是由调用者执行任务(main主线程执行)、第三是丢弃当前的任务,第四是丢弃最早排队任务。默认是直接抛异常。

继续探究:线程池中有哪些常见的阻塞队列?

1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的。
4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VLujUNUq-1689326073281)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/23e8276d-28b6-459d-b58b-1aa1b5dc87b2/Untitled.png)]

21、如何确定核心线程池呢?

  • IO密集型任务(并发不高、任务执行时间长):2N+1 (N为计算机的CPU核数)
  • CPU密集型任务(高并发、任务执行时间短):N+1
  • 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器
    是第二步。

22、线程池的执行原理知道吗?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ouGq3364-1689326073282)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1d5f32db-0446-4773-962f-2e21b86dc612/Untitled.png)]

首先判断线程池里的核心线程是否都在执行任务,如果不是则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任 务。如果已经满了,则交给拒绝策略来处理这个任务。

23、为什么不建议使用Executors创建线程池呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CnhRfUAu-1689326073283)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/21547a57-2052-41b0-b75a-52035b6a98f7/Untitled.png)]

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,这样就能避免内存溢出。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值