写点什么

Java 面试题超详细整理《多线程篇》,kafka 架构和原理

用户头像
极客good
关注
发布于: 刚刚
  • 公平锁:锁的分配机制是公平的,加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得

  • 非公平锁:加锁时不考虑排队等待问题,JVM 按随机、就近原则分配锁的机制则称为不公平锁,非公平锁实际执行的效率要远远超出公平锁(5-10 倍),除非程序有特殊需要,否则最常用非公平锁的分配机制。

[](

)线程中的多个流程是否可以获取同一把锁:可重入锁(递归锁)


可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁。

[](

)多线程之间能不能共享同一把锁:共享锁和排他锁


  • 共享锁:共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。

  • 排他锁:每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。

[](

)锁状态:无锁、偏向锁、轻量级锁、重量级锁


锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。


**锁升级:随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,


也就是说只能从低到高升级,不会出现锁的降级)。**


  • 无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

  • 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能

  • 轻量级锁:当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

  • 重量级锁:升级为重量级锁时,锁标志的状态值变为“10”,此时 Mark Word 中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。




[](

)死锁


两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法让程序进行下去!


如何查看线程死锁:通过jstack命令进行查看,jstack 中会显示发生死锁的线程


数据库中查看死锁:


查看是否有表锁:show OPEN TABLES where In_use > 0;


查询进程:show processlist;


查看正在锁的事务:SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;


查看等待锁的事务:SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS_WAITS;




[](


)什么是守护线程?




守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。(GC 垃圾回收线程就是一个经典的守护线程)


应用场景:为其他线程提供服务支持、需要正常且立刻关闭某个线程时


守护线程不能用于访问固有资源,比如读写操作或计算逻辑,因为它在任何时候甚至在一个操作的中间发生中断。




[](


)创建线程有哪几种方式?




继承 Thread 类,重写 run 方法:


public class MyThread extends Thread {


@Override


public void run() {


System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");


}


}


实现 Runnable 接口,实现 run 方法:


public class MyRunnable implements Runnable {


@Override


public void run() {


System.out.println(Thread.currentThread().getName() + " run()方法执行中...");


}


}


实现 Callable 接口,实现 call 方法。通过 FutureTask 创建一个线程,获取到线程执行的返回值:


public class MyCallable implements Callable<Integer> {


@Override


public Integer call() {


System.out.println(Thread.currentThread().getName() + " call()方法执行 中...");


return 1;


}


}


使用 Executors 工具类创建线程池,并开启线程。




[](


)Thread、Runable 和 Callable 三者区别?




  • Thread 是一个抽象类,只能被继承,而 Runable、Callable 是接口,需要实现接口中的方法。继承 Thread 重写 run()方法,实现 Runable 接口需要实现 run()方法,而 Callable 是需要实现 call()方法。

  • Thread 和 Runable 没有返回值,Callable 有返回值,返回值可以被 Future 拿到。

  • 实现 Runable 接口的类不能直接调用 start()方法,需要 new 一个 Thread 并发该实现类放入 Thread,再通过新建的 Thread 实例来调用 start()方法。实现 Callable 接口的类需要借助 FutureTask (将该实现类放入其中),再将 FutureTask 实例放入 Thread,再通过新建的 Thread 实例来调用 start()方法。获取返回值只需要借助 FutureTask 实例调用 get()方法即可!


什么是 FutureTask?


FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是 Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。




[](


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




通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。


方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。


run() 可以重复调用,而 start()只能调用一次。


为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?


如果直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。




[](


)线程的状态




线程通常有 5 种状态:新建、就绪、运行、阻塞和死亡状态


  • 新建(new):新创建了一个线程对象。

  • 就绪(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取 cpu 的使用权。

  • 运行(running):可运行状态(runnable)的线程获得了 cpu 时间片(timeslice),执行程序代码。

  • 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU 的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。


阻塞的情况分三种:


① 等待阻塞(wait->等待对列):运行状态中的线程执行 wait()方法,JVM 会把该线程放入等待队列(waittingqueue)中,使本线程进入到等待阻塞状态;


② 同步阻塞(lock->锁池):线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),则 JVM 会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;


③ 其他阻塞(sleep/join): 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。


  • 死亡(dead):线程 run()、main()方法执行结束,或者因异常退出了 run()方法,则该线程结束生命周期。死亡的线程不可再次复生。



由上图可以看出:线程创建之后它将处于 new(新建) 状态,调? start() ?法后开始运?,线程这时候处于 ready(可运?) 状态。可运?状态的线程获得了 CPU 时间?(timeslice)后就处于 running(运?) 状态。




[](


)线程基本方法




  • 线程等待 wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;只有等待另外线程的通知或被中断才会返回,wait 方法一般用在同步方法或同步代码块中

  • 线程睡眠 sleep():使一个正在运行的线程处于睡眠状态,但不会释放当前占有的锁。是一个静态方法,调用此方法要处理 InterruptedException 异常;

  • 等待其他线程终止 join():线程进入阻塞状态,马上释放 cpu 的执行权,但依然会保留 cpu 的执行资格,所以有可能 cpu 下次进行线程调度还会让这个线程获取到执行权继续执行

  • 线程让步 yield():使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片,执行后线程进入阻塞状态,例如在线程 B 种调用线程 A 的 join(),那线程 B 会进入到阻塞队列,直到线程 A 结束或中断线程。

  • 线程唤醒 notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;

  • 线程全部唤醒 notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;




[](


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




两者都可以暂停线程的执行


  • 类的不同:sleep() 是 Thread 线程类的静态方法,wait() 是 Object 类的方法。

  • 是否释放锁:sleep() 不释放锁;wait() 释放锁,并且会加入到等待队列中。

  • 是否依赖 synchronized 关键字:sleep 不依赖 synchronized 关键字,wait 需要依赖 synchronized 关键字

  • 用途不同:sleep 通常被用于休眠线程;wait 通常被用于线程间交互/通信,

  • **用法不同:sleep() 方法执行完成后,不需要被唤醒,线程会自动苏醒,或者可以使用 wait(longtim


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


eout)超时后线程会自动苏醒。wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。**




[](


)什么是上下文切换?




巧妙地利用了时间片轮转的方式, CPU 给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务,任务的状态保存及再加载, 这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗 CPU 上执行变成了可能。


上下文:指某一时间点 CPU 寄存器和程序计数器的内容


几种发生上下文切换的情况:


  • 主动让出 CPU,比如调用了 sleep(), wait() 等。

  • 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。

  • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞,被终止或结束运行




[](


)线程之间如何进行通信




  • 通过共享内存或基于网络通信

  • 如果是基于共享内存进行通信,则需要考虑并发问题,什么时候阻塞,什么时候唤醒

  • 想 Java 中的 wait()、notify()就是阻塞唤醒

  • 通过网络就比较简单,通过网络连接将数据发送给对方,当然也要考虑到并发问题,处理方式就是加锁等方式。




[](


)说一下 ThreadLocal




ThreadLocal 是 Java 中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据。


ThreadLocal 底层是通过 TreadLocalMap 来实现的,每个 Thread 对象中都存在一个 ThreadLocalMap,Map 的 key 为 ThreadLocal 对象,Map 的 value 为需要缓存的值 。


  • ThreadLocalMap 由一个个 Entry 对象构成 Entry 继承自 WeakReference< Threadlocal<?>>,一个 Entry 由 Threadlocal 对象和 object 构成,由此可见,Entry 的 key 是 Threadlocal 对象,并且是一个弱引用。当没指向 key 的强引用后,该 key 就会被垃圾收集器回收

  • 当执行 set 方法时, Threadlocal 首先会获取当前线程对象,然后获取当前线程的 ThreadLocalMap 对象。再以当前 Threadlocal 对象为 key,将值存储进 Threadlocalmap 对象中

  • get 方法执行过程类似,Threadloca 首先会获取当前线程对象,然后获取当前线程的 ThreadLocalMap 对象。再以当前 ThreadLocal 对象为 key,获取对应的 value


由于每条线程均含有各自私有的 ThreadLocalMap 容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。


使用场景:


  • 在进行对象跨层传递的时候,使用 ThreadLocal 可以避免多次传递,打破层次间的约束

  • 线程间数据隔离

  • 进行事务操作,用于存储线程事务信息

  • 数据库连接,Session 会话管理


Spring 框架在事务开始时会给当前线程绑定一个 Jdbc Connection,在整个事务过程都是使用该线程定的 connection 来执行数据库操作,实现了事务的隔离性, spring 框架里面就是用的 Threadlocal 来实现这种隔离。




[](


)ThreadLocal 内存泄漏原因,如何避免?




如果在线程池中使用 ThreadLocal 会造成内存泄漏,因为当 ThreadLocal 对象使用完之后,应该要把设置的 key,value 也就是 Entry 对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向 ThreadLocalMap, ThreadLocalMap 也是通过强引用指向 Entry 对象,线程不被回收,Entry 对象也就不会被回收,从而出现内存泄漏。


解决办法:


  • 在使用了 ThreadLocal 对象之后,手动调用 ThreadLocal 的 remove 方法,手动清除 Entry 对象

  • 将 ThreadLocal 变量定义成 private static,这样就一直存在 ThreadLocal 的强引用,也就能保证任何时候都能将通过 ThreadLocal 的弱引用访问到 Entry 的 value 值,进而清除掉。




[](


)线程池的原理?




线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。


重用存在的线程,减少对象创建销毁的开销,且提高了响应速度;有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞,且可以定时定期执行、单线程、并发数控制,配置任务过多任务后的拒绝策略等功能。


线程池类别:


  • newFixedThreadPool :一个定长线程池,可控制线程最大并发数。

  • newCachedThreadPool:一个可缓存线程池。

  • newSingleThreadExecutor:一个单线程化的线程池,用唯一的工作线程来执行任务。

  • newScheduledThreadPool:一个定长线程池,支持定时/周期性任务执行。


线程池尽量不要使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式去创建,因为 Executors 创建的线程池底层也是调用 ThreadPoolExecutor,只不过使用不同的参数、队列、拒绝策略等如果使用不当,会造成资源耗尽问题。直接使用 ThreadPoolExecutor 让使用者更加清楚线程池允许规则,常见参数的使用,避免风险。


主要参数:


  • corePoolSize:核心线程数,默认情况下创建的线程数,默认情况下核心线程会一直存活,是一个常驻线程。

  • maximumPoolSize:线程池维护线程的最大数量,超过将被阻塞!(当核心线程满,且阻塞队列也满时,才会判断当前线程数是否小于最大线程数,才决定是否创建新线程)

  • keepAliveTime:非核心线程的闲置超时时间,超过这个时间就会被回收,直到线程数量等于 corePoolSize。

  • unit:指定 keepAliveTime 的单位,如 TimeUnit.SECONDS、TimeUnit.MILLISECONDS

  • workQueue:线程池中的任务队列,常用的是 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。

  • threadFactory:创建新线程时使用的工厂

  • handler:RejectedExecutionHandler 是一个接口且只有一个方法,线程池中的数量大于 maximumPoolSize,对拒绝任务的处理策略,默认有 4 种策略:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy




[](


)四种拒绝策略




AbortPolicy:中止策略。默认的拒绝策略,直接抛出 RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。


DiscardPolicy:抛弃策略。什么都不做,直接抛弃被拒绝的任务。


DiscardOldestPolicy:抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。如果阻塞队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用。


CallerRunsPolicy:调用者运行策略。在调用者线程中执行该任务。该策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者(调用线程池执行任务的主线程),由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理完正在执行的任务。




[](


)线程池中线程复用的原理




  • 线程池将线程和任务讲行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的个线程必须对应一个任务的限制。

  • 在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread. start()来创建新线程,而是让每个线程去执行一个循环任务,在这个循环任务中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的 run 方法串联起来。




[](


)如果你提交任务时,线程池队列已满,这时会发生什么




  • 如果使用的是无界队列,没关系,继续添加任务到阻塞队列中等待执行

  • 如果使用的是有界队列,任务首先会被添加到队列中,如果队列满了,会根据 maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,队列依然是满的,那么则会使用拒绝策略处理满了的任务,默认是 AbortPolicy。





[](


)阻塞队列的作用




—般的 队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列 通过阻塞可以保留住当前要继续入队的任务。


**阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入 wait 状态,释放 CPU 资源。


阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时线程池利用阻塞队列的 take 方法挂起,从而维持核心线程的存活、不至于一直占用 cpu 资源。**

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
Java面试题超详细整理《多线程篇》,kafka架构和原理