- 当前位置:
- 首页
- VIP_八股文
- aaaaVIP_面试突击专题
- 正文
Java多线程+并发突击八股文
多线程+并发学习指南
这块主要是中大厂问的多,你可以理解为,问这些的,公司给的薪资不会低于 20w。
这里分成 多线程基础 + 并发基础,一般来说,Java并发不会问的很深和很细,更多的是会结合你项目安全相关的来问你,而且我说个实话,面试官很多也都忘了,所以更多的是会问你大致原理,比如线程池有啥用,线程数是越多越好吗?等等。
所以我觉得,Java并发大家先优先掌握 CAS + synchronized + volatile + 悲观锁/乐观锁 + 线程池 + 死锁案例 这些就差不多了。
需要掌握的知识概括:
一、多线程基础部分
1、理解什么是线程安全,能够举例来说明线程不安全
2、能够用 Java 写一个出现死锁的例子。
3、掌握 Java 进程与线程的区别
4、自己用 Thread 和 Runnable 创建下多进程,并且知道他们的区别
5、run() 方法和 start() 的区别,最好自己点进去看看远吗
6、wait,notify,sleep 之间的关系,也要自己用一用
7、掌握线程的几种状态
8、轻量级锁 + 重量级锁(自旋锁 + 自适应锁)
9、Volatile 和 synchronized
10、CAS原理 + 优缺点
二、多线程进阶部分
1、ReentrantLock 底层
4、Lock
3、AQS(比较难)
4、几种常见JUC包:CountDownLatch,Semaphore,CyclicBarrier,FutureTask,记得自己随便写个 demo 用一下
5、几个常见的原子类:AtomicBoolean、AtomicInteger、AtomicReference
6、threadLocal 应用场景 + 原理
参考学习文章以及资料
我会在对应的面试题那里,补充对应的文章,专栏,视频和书籍,是一个持续补充的过程,大家有看到好的文章也可以发我,然后我也会给大家推荐对应的书籍 + 咱们训练营的专栏,作为一个进阶补充,有时间你就都看。
系统资料推荐:多线程基础,不需要看什么系统内容,跟着课程入门即可之后跟着这些面试题就行,问的不深入,并发部分属于学无止境,推荐看 Java并发编程课程说明,后面有余力,看《Java并发编程艺术》这本书看个三四遍,基本对并发问题无所畏惧。
正文
【多线程基础专题】关于线程安全 + 锁的一些概念性问题(重要)🌟🌟🌟🌟🌟
说明:这里主要问一些概念问题,侧重「理解」,理解一个东西,举例子最为重要哦,然后面试一般就这些问题的比较多。
1、你是怎么理解线程不安全的?线程不安全会带来哪些问题?(回答指导:采用经典的 i++ 来回答不安全,至于带来什么问题,如果可以距离实际项目那最好了)
2、多线程环境下,我们可以怎么解决线程不安全的问题?
3、刚才说到了锁,那你觉得锁怎是么实现线程安全?(回答指导:这个得从操作系统角度回答了)
4、刚才我们说到了锁,乐观锁和悲观锁了解吗?可以说一说他们的区别吗?
5、Java 中有些是基于乐观锁实现的,有些是基于悲观锁实现的,你了解到的有哪些呢?
6、死锁了解吗?可以简单说下死锁以及怎么解决死锁问题的不?
7、java里面的线程和操作系统的线程一样吗?比如我在Java中创建了一个线程,那么操作系统也会创建一个线程吗?
【参考文章以及资料补充】:
参考回答:
1、你是怎么理解线程不安全的?线程不安全会带来哪些问题?(回答指导:采用经典的 i++ 来回答不安全,至于带来什么问题,如果可以距离实际项目那最好了)
线程不安全就是指在多线程环境下,多个线程同时访问和修改共享数据时,可能会数据出现错误或者不一致的情况。比如多个线程同时对一个 int i 变量进行 i++ 时,会出现丢失更新。具体来说,i++ 可以分解为三个动作:读取当前值、对该值加1、将该值写回。这可能会导致多个线程同时读到相同的值并对其加 1,导致最终结果一共只增加 1,而不是每个线程都加 1。同时由于线程之间的执行顺序是不确定的,每次运行时都可能会得到不同的结果,使程序具有不确定性。
项目中有一个用户可以对某个专栏点赞的功能,多个用户同时进行点赞的过程就类似点赞数量++ 的过程,可能会出现 “点赞被吞” 的情况。这时可以对 “点赞” 的操作加锁,也可以直接使用 Java 并发包提供的原子更新类 atomicInteger 类,底层使用 CAS 完成自增操作,效率高。
2、多线程环境下,我们可以怎么解决线程不安全的问题?(说了大概率会被问,)
最先想到的是可以用 synchronized 关键字为方法或者代码块加锁,保证同一时刻只有一个线程可以访问共享资源。还可以使用 volatile,它是轻量级的 synchronized,能够保证所修饰变量的可见性和有序性。
除了这两个关键字,JDK 还提供了一些线程安全的数据结构,如 ConcurrentHashMap,ConcurrentLinkedQueue 等等,可以直接使用。还有并发包下的原子操作类 atomicInteger 类等,并发工具类 CountDownLatch,Semaphore 类等等,都可以辅助我们实现线程安全。
3、刚才说到了锁,那你觉得锁怎是么实现线程安全?(回答指导:这个得从操作系统角度回答了)
一个简单的锁模型是,在临界区代码前后分别加上加锁和解锁的操作。线程在进入临界区之前会先尝试加锁,如果成功则进入临界区,此时这个线程持有锁。否则当前线程等待,指导持有锁的线程执行完临界区的代码后解锁。
Java 中的 synchronized 实现就跟这个思路类似,它是在编译后的代码前后分别添加 monitorenter 和 monitorexit 指令,JVM 确保某个对象都会对应一个 monitor,当线程执行到 monitorenter 时会尝试获取当前的 monitor 对象,也就是加锁操作。同样的,执行到 monitorexit 指令时会释放这个 monitor,也就是解锁操作。
4、刚才我们说到了锁,乐观锁和悲观锁了解吗?可以说一说他们的区别吗?
乐观锁在读取数据时并不会对数据进行加锁,而是在提交更新时检查是否有其他线程对数据进行了修改。如果没有发生冲突,那么更新成功,否则就要采取一些冲突解决策略,比如回滚或或者重试。
悲观锁则是一种比较保守的策略,它在访问数据之前,会先对资源进行加锁,确保其他线程无法同时访问,必须等待锁释放后才能继续执行。
5、Java 中有些是基于乐观锁实现的,有些是基于悲观锁实现的,你了解到的有哪些呢?
并发包下的原子更新类比如 AtomicInteger 类就是典型的乐观锁,底层使用 CAS 操作实现。
悲观锁有 synchronized,对整个方法或代码块加锁;ReetrantLock,比 synchronized 更加灵活,可以手动控制加锁和解锁的过程;ReetrantReadWriteLock,允许多个线程同时读,但只允许一个线程写。
6、死锁了解吗?可以简单说下死锁以及怎么解决死锁问题的不?
死锁就是:一组互相竞争资源的线程因互相等待,导致永久阻塞的现象。
死锁出现时一定会满足四个条件:第一个是互斥,即共享资源只能被一个线程占用;第二个是占有且等待,即线程在持有一个共享资源,并等待另一个共享资源时,不会释放当前的共享资源;第三个是不可抢占,即任何线程不能抢占其他线程的共享资源;第四个是环路等待,最终形成死锁时,各个线程会形成一个进程到资源的环形链。
因为锁一定是互斥的,所以解决死锁只需破坏以上后三个条件任意一个即可。对于占有且等待,可以让线程加锁时一次性申请所有的资源,这样就不存在线程等待了。对于不可抢占,可以让线程获取不到资源时,主动释放当前已经占有的资源。对于环路等待,可以对资源进行按序申请。也就是说,线程在申请资源时优先申请序号小的,再申请序号大的,这样就不会存在等待环路了。
7、java里面的线程和操作系统的线程一样吗?比如我在Java中创建了一个线程,那么操作系统也会创建一个线程吗?
JDK 1.2 以前采用的是绿色线程,由 JVM 管理和调度,发生在操作系统的用户空间。这对操作系统是透明的,操作系统只能看到进程,而不能看到其中的线程。
这种模式的缺点为,因为操作系统不知道线程的存在,CPU 的时间片是以进程为调度的,如果进程中的某个线程阻塞,则会导致整个进程折射。优点也很明显,可以在不支持多线程的操作系统上实现多线程,并且线程切换只需在用户态就可以实现,免去了用户态到内核态的来回切换。
JDK 1.2 以后 Java 线程就是直接依赖操作系统实现的,是 1:1 的关系。也就是说 Java 中的线程,实质上就是操作系统中的线程。
因此,就目前来看,在 Java 中创建了一个线程,操作系统也会创建一个线程,是对的。
【多线程基础专题】关于Java线程的一些关键字问题🌟🌟🌟
说明:这里主要是针对 Java 的一些具体问题,比如如何创建线程,以及这些sleep, wait-notify等等,这些一般面试问的会少一些,并且问的时候比较简单,就问下基础,一般不会有连环炮啥的,不过最好自己用过吧。
1、在 Java 中,一个线程的生命周期有哪些?
2、在 Java 的中,创建线程的方式有哪些?
3、你觉得Thread类和Runable接口的最大区别是什么?
4、说一说run() 方法和 start() 的区别?
5、说一说 wait和sleep的区别?
6、说一说 notify 和 notifyAll 的区别以及使用场景?
上面这些没啥好说的,最好可以自己简单使用一些,相对问的少一些
【参考文章以及资料补充】:
参考回答:
1、在 Java 中,一个线程的生命周期有哪些?
大致有六种,我串起来说一下:当一个线程对象被创建时,它会处于创建状态。当调用线程的 start( ) 后,线程进入就绪状态,此时线程还没有真正的执行,需要等待分配 CPU 时间片,只后才会进入真正的运行状态,开始执行 run( ) 中的代码。
在线程运行时,可能会因为需要等待锁的释放等而进入阻塞状态。或者会调用自身的 wait( ) 和 join( ) 进入等待状态。
最后线程正常执行完,或者发生了未捕获的异常,会进入终止状态,结束。
2、在 Java 的中,创建线程的方式有哪些?(一定要自己会创建,因为有时候会遇到面试官让你创建一个线程)
Java 中一共有三种方式创建一个线程:
第一种也是最常见的一种就是通过实现 Runnable 接口,覆写其中的 run( )。
第二种是继承 Thread 类,Thread 类本身就实现了 Runnable 接口,所以也只需覆写 run 方法即可。
第三种是实现 Callable 接口,它与 Runnale 的区别在于 Callable 为线程的执行提供了一个返回值 FutureTask 类,可以使用这个 FutureTask 类的 get 等方法查询这个线程是否执行完毕。
3、你觉得Thread类和Runable接口的最大区别是什么?
Runnable 是一个接口,Thread 是一个实现了 Runnable 接口的类。按照 Java 中的单继承、多实现的特性,如果使用继承 Thread 类来创建进程,则无法再继承其他类,会降低代码的灵活性。这时用实现 Runnable 接口比较好。
4、说一说run() 方法和 start() 的区别?(PS:最好自己点进去源码,看看他们的区别)
Thread 的 run( ) 只是在主线程中的一个普通方法调用,而 start( ) 则会创建一个新的子线程来执行,它底层使用 native 方法实现的,用来开启一个新线程并执行自己定义的 run 方法。
5、说一说 wait和sleep的区别?
sleep( ) 是 Thread 类的方法,wait( ) 是 Object 类的方法。
sleep( ) 可以在任何地方使用,wait( ) 则只能在同步方法或同步块中使用,并且 sleep( ) 只是让出了 CPU,并没有释放同步资源锁,sleep 结束之后会继续执行。
而 wait( ) 则是让当前线程退出同步资源锁,进入等待队列,让其他正在等待这个锁的线程争夺资源并运行。在这之后,只有调用了 notify( ) / notifyAll( ) 唤醒这个线程,它才会进入锁池,并加入到争夺资源的过程中。
6、说一说 notify 和 notifyAll 的区别以及使用场景?
notify( ) 会随机唤醒一个处于等待池中的线程,进入锁池去竞争获取锁的机会。
notifyAll( ) 会唤醒所有处于等待池中的线程,一起进入锁池去竞争获取锁的机会。
在生产-消费模型中,生产者生产出消息时,可以调用 notify( ) 通知消费者消费。当多个线程需要同时满足某个条件才能运行时,可以使用 notifyAll( ) 唤醒所有等待线程,同时执行任务。
【并发专题】CAS连环炮🌟🌟🌟🌟🌟
说明:一般中大厂才会问这个了,而且问的时候,可能就是连环炮,不过 CAS 的问题不算难
0、简单介绍一下 CAS,什么是 CAS ?
1、CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?
2、CAS是如何解决 ABA 问题的?
3、比起其他锁,CAS 这种锁有哪些优缺点呢?CAS 适用于哪些应用场景呢?
4、我们说了,在高并发情况下,CAS 或许效率更低,那么对此,你觉得可以怎么优化 CAS 呢?(PS:Java8 就有进行了一些优化)
【参考文章以及资料补充】:
漫画:什么是CAS机制?(进阶篇)
漫画:什么是 CAS 机制?
参考回答:
0、简单介绍一下 CAS,什么是 CAS ?
CAS 是比较并交换的缩写,是并发编程中的一个原子操作。它包含三个操作数:内存位置、预期原值和新值。在操作期间会先比较「内存位置上的值」和「预期原值」是否相等,如果相等则交换成新值,如果不相等则不交换。
1、CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?
这个主要是通过操作系统底层硬件级别的支持,来实现 CAS 操作的原子性的。比如现代 x86 架构的处理器,是用 cmpxchg 指令将 CAS 的多个操作通过一条处理器指令实现。
再往下说,处理器指令的原子性通常由处理器提供「总线锁定」和「缓存锁定」两个机制来保证。
总线锁定本质上就是一个 LOCK# 信号,当一个处理器在总线上发出这个信号时,其它处理器的请求将被阻塞,那么此时这个处理器就可以独占共享内存。这种锁定方式的开销很大,直接把 CPU 和内存之间的通信给锁住了,导致在锁定期间其他处理器也不能操作其它内存地址的数据。
缓存锁定主要是利用了 MESI ,也就是缓存一致性协议,通过跟踪和维护各处理器缓存行的状态,确保多处理器对同一内存地址读写的一致性。这个过程是处理器直接操作数据的内存地址,不会影响其它处理器对其它内存地址的操作。
2、CAS是如何解决 ABA 问题的?
可以在每次修改值时带上版本号就行。比如 JUC atomic 包下的 AtomicStampedReference 类,通过引入一个整数戳,在对对象的引用修改时,也会改对应的整数戳,避免了 ABA 问题。
3、比起其他锁,CAS 这种锁有哪些优缺点呢?CAS 适用于哪些应用场景呢?
CAS 是乐观锁的一种实现,是非常轻量级的操作,效率很高。但也有缺点。首先就是 ABA 问题,可以带上版本号解决;还有就是循环时间长开销大,高并发情况下,自旋 CAS 如果长时间不成功,会白白浪费 CPU;同时 CAS 页只能保证一个共享变量的原子操作。当需要对多个共享变量操作时,就带考虑使用锁,或者把多个变量放在一个对象里,使用 AtomicReference 保证引用对象操作的原子性。
CAS 操作可以用于实现乐观锁,像 AtomicInteger 之类的。也可以用于一些并发容器底层同步状态的设置,比如 JDK 中 AQS 和 ReetrantLock 都有的 tryAcquire( ),都是用 CAS 操作独占式地获取并设置当前线程的同步状态。还有 Java 中的偏向锁和轻量级锁底层也用到了 CAS,作用类似。
4、我们说了,在高并发情况下,CAS 或许效率更低,那么对此,你觉得可以怎么优化 CAS 呢?(PS:Java8 就有进行了一些优化)
高并发下主要是 CAS 自旋带来的性能问题,可以对线程加入自旋次数的限制,超过限制时可以根据情况调整自旋次数。
还有就是 Java 8 中引入了 LongAdder 类,它用「分段计数」的方法来优化 CAS,具体来说,LongAdder 将内部的数值分为多个段,每个段都是独立的计数器。当多个线程同时更新数据时,会将这些更新分发给不同的段,减少了竞争。
【并发专题】Volitale 连环炮🌟🌟🌟🌟🌟
说明:一般来说,这个要嘛就问的比较表面,要嘛会问的比较底层,大家至少掌握表面的部分,底层的一般会涉及操作系统了,这道题的问法也比较常规。
1、Volitale 了解吗?简单介绍一下(指导:回答它的三个作用)
2、Volitale 可见性底层实现了解不?简单说一下
3、你觉得什么样的场景下需要用到 volitale,可以你见过的一些例子吗?
【参考文章以及资料补充】:
参考回答:
1、Volitale 了解吗?简单介绍一下(指导:回答它的三个作用)
volatie 用于保证所修饰变量对所有线程的可见性和顺序性。
可见性就是指当一个线程修改一个共享变量时,另外一个线程能立马读到这个修改的值。顺序性就是指对 volatile 变量的读写操作都是按顺序执行的,不会出现重排序情况。
没有原子性是因为,它只对单个 volatile 变量的读写具有原子性,如果是多个 volatile 操作或是 volatile++ 这种复合操作,整体上不保证原子性。
2、Volitale 可见性底层实现了解不?简单说一下
其实就是在对 volatile 变量进行写操作的汇编代码之前,加了一条 Lock 前缀指令。这个 Lock 前缀指令在多核处理器下会将:当前处理器缓存行的数据写回到内存并使其它 CPU 里缓存了该内存地址的数据无效。后者主要是通过处理器遵循 MESI 协议来实现的,每个处理器通过嗅探总线来检查自己的数据是否过期,并做对应的处理。
3、你觉得什么样的场景下需要用到 volitale,可以你见过的一些例子吗?
volatile 基于可见性,比较适合用于标记一个状态变量。比如在多线程间控制循环或者任务的执行。当一个线程修改了 volatile 修饰的状态变量后,其他线程能立马感知并做对应的处理。
除此之外,单例模式的一种实现是利用双重检查锁定来创建对象,其中就用到了 volatile 来修饰那个单例实例,目的就在于避免 new 对象时可能出现的指令重排序,导致错误结果。这个过程主要用到了 volatile 的顺序性。
【并发专题】synchronized 连环炮🌟🌟🌟🌟🌟
PS:synchronized 这个关键字,和 Volitale 类似,一般来说,可以问的问题不多,大概就是问一下作用,以及底层大概怎么实现的
1、简单说一下 synchronized 的作用?
2、synchronized 加在方法上和加载在对象上,有什么不同?
3、讲一下 synchronized 的底层是怎么实现的?(PS:加在实例上和加在方法上,实现不一样,都要说明,一个从字节码角度说,一个从对象头角度说)
4、在 Java 中,synchronized 属于重量级锁,为了让 synchronized 效率更快,JDK做了哪些升级?
【参考文章以及资料补充】:
参考回答:
1、简单说一下 synchronized 的作用?
synchronized 主要用来实现同步,被 synchronized 修饰的代码块或方法称为临界区代码,在进入临界区之前,线程会尝试获取锁,如果锁可用,则线程会获取锁并进入临界区,执行相应的代码。否则线程将被阻塞,直到获取到锁为止。
2、synchronized 加在方法上和加载在对象上,有什么不同?
对于普通同步方法,锁的是当前的实例对象;对于静态同步方法,锁的是当前类的 Class 对象;对于同步代码块,锁的是 synchronized 括号里配置的对象。
3、讲一下 synchronized 的底层是怎么实现的?(PS:加在实例上和加在方法上,实现不一样,都要说明,一个从字节码角度说,一个从对象头角度说)
对于方法级的同步来说,JVM 利用方法常量池中的 ACC_SYNCHRONIZED 标志来区分一个方法是否为同步方法。当线程访问同步方法时,首先会检查这个标志是否被设置,如果是,则需要先获得监视器锁,然后开始执行方法,执行结束会释放监视器锁。
对于同步代码块来说,JVM 是在编译后代码块的开始和结束处,插入 monitorenter 和 monitorexit 指令来实现的。任何对象都会对应一个 monitor,线程执行到 monitorenter 时会尝试获取对象所对应的 monitor 的所有权,也就是加锁。执行到 monitorexit 时会释放 monitor 的所有权,也就是解锁。
4、在 Java 中,synchronized 属于重量级锁,为了让 synchronized 效率更快,JDK做了哪些升级?
Java 1.6 以后主要引入了偏向锁,轻量级锁和适应性自旋等特性。总体上来说,锁升级的过程为:无锁->偏向锁->轻量级锁->重量级锁。
对于偏向锁来说,首次获取时需要用 CAS 设置 Java 对象头中 mark word 字段的线程 id ,只后持有偏向锁的线程每次进入这个锁相关的同步块时,只需比对一下是否为本线程,如果是则直接获取锁成功。这适用在一个线程反复获取同一个锁的情况,可以提高带有同步但无竞争的程序性能。
偏向锁中发生线程竞争,会升级为轻量级锁。轻量级锁在每次获取时都需要用 CAS 比较并替换对象头中的整个 Mark Wrod 字段,如果 CAS 成功则代表获取锁成功。由于绝大部分的锁在整个生命周期内都不会存在竞争,那么在多线程交替执行同步块的情况下,轻量级锁可以避免重量级锁引起的性能消耗。
轻量级锁释放时若存在其它线程竞争,锁将膨胀为重量级锁,任何没有竞争到锁的线程都会被阻塞,直到被其它线程唤醒。但线程唤醒的过程涉及到操作系统的调用,会有额外的开销。
为了避免这个问题,在线程竞争轻量级锁失败后,膨胀为重量级锁之前线程会尝试适应性自旋,自旋的次数根据前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果这个锁很少自旋成功,那么以后有可能省略掉自旋过程以避免 CPU 资源浪费。
【并发专题】AQS 连环炮🌟🌟
说明:一般来说,即时有问AQS,也不会问很难,我们只需要从整体的角度,知道他是如何保证线程安排的就可以了,AQS的底层也是基于CAS的。
1、了解 AQS 吗?AQS 的全名叫什么?(PS:这个问题,面试官还真问过)
2、大致讲解一下 AQS 是怎么实现线程安全的?
3、基于AQS的实现的同步工具,你了解哪些?(PS:简单介绍两三个就可以了)
【参考文章以及资料补充】:
参考回答:
1、了解 AQS 吗?AQS 的全名叫什么?(PS:这个问题,面试官还真问过)
AQS 是用来构建锁或者其它同步组件的基础框架,内部用一个 int 变量来表示同步状态,并用 FIFO 队列来完成资源获取线程的排队工作。
它全称为 AbstractQueuedSynchronizer 抽象队列同步器,是一个抽象类,通常由子类继承 AQS 并实现它的抽象方法来管理同步状态。
2、大致讲解一下 AQS 是怎么实现线程安全的?
可以从三个方面来说:底层数据结构,独占式同步状态的获取与释放和共享式同步状态的获取与释放。
首先 AQS 底层使用 volatile int 变量 state 来表示同步状态,通过内部的 FIFO 双向队列来实现多线程的排队工作。
对于独占式同步状态的获取和释放来说,队列的一个节点代表一个线程及其等待状态,若某个线程请求获取同步状态,成功则将其作为队列的头结点,失败则将其添加到队列的尾部并尝试自旋。当头结点的线程释放同步状态时,会同时唤醒头结点的后继节点,后继节点在获取同步状态成功后会将自己设置为头结点,以此类推。需要注意的是,这整个过程需要用 CAS 操作保证同步状态的获取,队列头尾节点的设置都是线程安全的。
共享式获取与独占式获取的区别在于,共享式访问资源时,其它共享式的访问都被允许,而独占式访问将被阻塞。正是由于同时可能有多个线程拥有同步状态,共享式释放同步状态也需要用 CAS 确保线程安全。
3、基于AQS的实现的同步工具,你了解哪些?(PS:简单介绍两三个就可以了)
比如重入锁 ReentrantLock,读写锁 ReetrantReadWriteLock 和等待多线程完成的 CountDownLatch,底层都是基于 AQS 实现的。
ReentrantLock 可以支持一个线程对某个资源的重复加锁,底层是在同一个线程加锁时递增,释放锁时递减同步状态的值。
ReetrantReadWriteLock 内部维护了一对锁,一个读锁和一个写锁。多个线程可以同时获得读锁,并发地进行读取操作。但是,只有一个线程能够获得写锁,同时其他线程获得读锁或写锁将被阻塞,确保了在写操作期间没有其他线程能够读取或写入数据。
CountDownLatch 用于一个或多个线程等待其他线程完成操作。内部维护了一个计数值,任一线程在调用 await( ) 时,如果当前计数值不为 0 则会被阻塞,直到计数值减至 0 为止。而其他线程可以调用countDown( ) 来减小计数值。
ReentrantReadWriteLock 示例
public class ReentrantReadWriteLockExample {
private final Map files; // 虚拟文件系统中的文件
private final ReentrantReadWriteLock lock; // 读写锁
public ReentrantReadWriteLockExample() {
files = new HashMap<>();
lock = new ReentrantReadWriteLock();
}
public String readFile(String fileName) {
// 获取读锁, 允许多个线程同时读
lock.readLock().lock();
try {
System.out.println("Reading file: " + fileName);
// 模拟读取文件的耗时操作
Thread.sleep(1000);
return files.getOrDefault(fileName, null);
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
// 释放读锁
lock.readLock().unlock();
}
}
public void writeFile(String fileName, String content) {
// 获取写锁, 允许一个线程独占写
lock.writeLock().lock();
try {
System.out.println("Writing file: " + fileName);
// 模拟写入文件的耗时操作
Thread.sleep(1000);
files.put(fileName, content);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放写锁
lock.writeLock().unlock();
}
}
public static void main(String[] args) {
ReentrantReadWriteLockExample fileSystem = new ReentrantReadWriteLockExample();
// 开启十个写线程
for (int i = 0; i < 10; ++i) {
int finalI = i;
new Thread(() -> {
fileSystem.writeFile("file" + finalI, "Hello, World!");
}).start();
}
// 开启四个读线程
for (int i = 0; i < 4; ++i) {
int finalI = i;
new Thread(() -> {
String content = fileSystem.readFile("file" + finalI);
System.out.println("Read content: " + content);
}).start();
}
}
}
运行后,控制台可以看到十个写线程是顺序打印输出(独占),四个读线程是一块打印输出(共享)。
CountDownLatch 示例
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
// 创建一个 CountDownLatch 实例,参数为需要等待的线程数量
CountDownLatch latch = new CountDownLatch(3);
// 创建三个工作线程
List workers = Stream
.generate(() ->
new WorkerThread("Worker", latch)
)
.limit(3)
.collect(toList());
// 启动所有线程
workers.forEach(Thread::start);
// 等待所有工作线程完成操作
latch.await();
// 所有工作线程完成后执行的代码
System.out.println("工作线程执行完成");
}
static class WorkerThread extends Thread {
private final CountDownLatch latch;
public WorkerThread(String name, CountDownLatch latch) {
super(name);
this.latch = latch;
}
@Override
public void run() {
try {
// 模拟工作线程执行任务
System.out.println(getName() + " is performing task...");
// 模拟任务执行时间
Thread.sleep(2000);
System.out.println(getName() + " has finished its task.");
// 任务完成后调用 countDown() 方法,减少计数器
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
【并发专题】ReentrantLock连环炮🌟🌟
1、简单介绍下 ReentrantLock?
2、synchronized和ReentrantLock有什么区别?
3、既然说到了可重入锁,那你觉得为啥需要可重入锁?
4、ReentrantLock 底层是怎么实现的?
【参考文章以及资料补充】:
待补充
参考回答:
1、简单介绍下 ReentrantLock?
ReentrantLock 是 Java 中重入锁的一种实现,可以支持一个线程对某个资源的重复加锁,底层是基于 AQS 实现,在同一个线程加锁时递增,释放锁时递减同步状态的值。
2、synchronized和ReentrantLock有什么区别?
两者相同之处在于都支持重入性,synchronized 支持隐性的重进入,比如线程可以在递归执行同步块里的代码而不被阻塞,ReentrantLock 需要显示调用 lock( )。
不同之处在于 ReentrantLock 比 synchronized 更加灵活。ReentrantLock 提供有 tryLock( ),可以尝试获取锁而不阻塞,并且可以设置超时时间。除此之外 ReentrantLock 还支持获取锁时的公平和非公平性选择。
3、既然说到了可重入锁,那你觉得为啥需要可重入锁?
可重入锁主要是用来解决线程在持有锁的情况下,再次请求同一个锁时发生的死锁问题。如果没有可重入锁,当一个线程已经获取了锁后,再次请求该锁会导致自己要等待自己释放锁,进而产生死锁。
具体的场景比如,递归函数中需要获取锁,如果没有重入锁,线程在每次递归调用时都会被自己阻塞。还有多个方法可能都需要获取相同的锁,没有重入锁的话,它们之间就不能相互调用。
4、ReentrantLock 底层是怎么实现的?
ReentrantLock 需要在 AQS 的基础上多考虑两个问题,一个是获取锁的线程再次获取锁的判断,此时需要将底层表示同步状态 volatile int 类型的值 + 1。另一个是锁的最终释放,线程重复获取 n 次锁,那么释放时也要重复释放 n 次,对应同步状态的值自减,直至为 0 代表锁已成功释放。
5、使用例子
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock(); // 创建一个ReentrantLock实例
private int count = 0; // 共享资源
public void increment() {
lock.lock(); // 获取锁
try {
// 在临界区中更新共享资源
count++;
// 调用使用同一个锁来保护共享资源的方法: getCount()
System.out.println("Incremented count: " + getCount());
} finally {
lock.unlock(); // 释放锁
}
}
public int getCount() {
lock.lock(); // 获取锁
try {
// 在临界区中访问共享资源
return count;
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
// 创建一个线程并启动
Thread incrementThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.increment();
}
});
incrementThread.start();
// 等待线程执行完毕
try {
incrementThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印最终的计数值
System.out.println("Final count: " + example.getCount());
}
}
【并发专题】threadLocal 连环炮🌟🌟
PS:这个的话,问的也不多,但是有时候在项目中会用到这个东西,如果你在项目中写上了,那么碰到懂的面试官,还是会问
1、介绍一下 threadLocal,简单说一下他的使用场景?
2、ThreadLocal的key是哪种引用类型?为啥这么设计?
3、ThreadLocal 是怎么防止内存泄漏?
【参考文章以及资料补充】:
参考回答:
1、介绍一下 threadLocal,简单说一下他的使用场景?
hreadLocal 用于提供线程的局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程,是一个以 ThreadLocal 对象为键,任意对象为值的存储结构。底层是通过一个 ThreadLocalMap 来存储和管理的。
它可以用于多线程处理多任务的场景,每个线程使用 ThreadLocal 单独保存自己的任务上下文,然后执行计算。
2、ThreadLocal的key是哪种引用类型?为啥这么设计?
弱引用。这样设计是为了防止某些情况下 ThreadLocal 可能会导致的内存泄漏。
3、ThreadLocal 是怎么防止内存泄漏?
简单来说是「弱引用」加「手动调用 remove( ) 」方法。
因为 ThreadLocalMap 主要有一个 Entry 数组,每一个 Entry 是继承了 ThreadLocal 类型的弱引用,并将其作为 key。value 是 Entry 中的一个 Object 成员变量。那么我们在创建 ThreadLocal 变量时,每一个 ThreadLocal 对象就有两个引用,一个是堆栈对它的强引用,一个是 ThreadLocalMap 中的 Entry 对它的弱引用。
现在假设 Entry 是对 ThreadLocal 的强引用而不是弱引用,在将 ThreadLocal 置空后,堆栈上的 ThreadLocal 强引用就会消失,但 Entry 对它的强引用还在,无法被回收,并且 Entry 的 value 值也是个强引用,也无法被回收,这就导致了内存泄漏。
将 Entry 继承自 ThreadLocal 的弱引用可以解决 ThreadLocal 的回收问题,但还是无法解决 value 的回收问题。ThreadLocal 在设计时就考虑到了这个问题,并且有一些防护措施:在调用 ThreadLocal 的 get( ),set( ) 和 remove( ) 的时候都会清除当前线程 ThreadLocalMap 中所有 key 为 null 的 value。所以在使用 ThreadLocal 的时候,每次用完都最好调用一下 remove( ) 方法,来防止内存泄漏。
4、value 内存泄漏示例:
public class Test {
// 对象被回收时触发
@Override
protected void finalize() throws Throwable {
System.err.println("对象被回收...");
}
public static void main(String[] args) {
ThreadLocal local = new ThreadLocal();
// 设置 key 为当前线程,value 为 Test 对象的键值对
local.set(new Test());
// 将 local 置空
local = null;
// 手动触发 GC
System.gc();
// GC 是异步执行的,主线程 sleep 一会,等待对象回收
// 如果 Test 被回收,会打印出回收语句
ThreadUtil.sleep(1000);
}
}
控制台无输出,value 内存泄漏
正常示例演示:
public class ThreadLocalExample {
private static ThreadLocal threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 创建并启动两个线程
Thread thread1 = new Thread(new Worker());
Thread thread2 = new Thread(new Worker());
thread1.start();
thread2.start();
}
static class Worker implements Runnable {
@Override
public void run() {
// 生成一个随机数,并存储到ThreadLocal变量中
threadLocal.set(new Random().nextInt(100));
// 在同一个线程中获取并设置ThreadLocal变量的值
System.out.println("Thread: " + Thread.currentThread().getName() +
", Random Number: " + threadLocal.get());
threadLocal.set(new Random().nextInt(100));
// 等待一段时间,模拟一些其他操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再次在同一个线程中获取并打印ThreadLocal变量的值
System.out.println("Thread: " + Thread.currentThread().getName() +
", Random Number: " + threadLocal.get());
// 清除ThreadLocal变量,以防止内存泄漏
threadLocal.remove();
}
}
}
控制台输出:
Thread: Thread-1, Random Number: 94
Thread: Thread-0, Random Number: 33// 等待了 1s
Thread: Thread-1, Random Number: 41
Thread: Thread-0, Random Number: 60
【并发专题】线程池 连环炮🌟🌟🌟
1、为什么要使用线程池?线程池是怎么提升效率的?
2、简单说一下线程池都有哪些可选参数?
3、线程数是越大越好吗?讲一下核心线程数与最大线程数之间的关系?
4、运行的时候,核心线程数能不能修改?
5、一个任务进来后,线程池是怎么处理的?
6、线程池的拒绝策略有哪几种?讲一下常见的几种
7、平时可以怎么创建一个线程池?
8、线程池有哪些关闭方式?他们的区别?
【参考文章以及资料补充】:
参考回答:
1、为什么要使用线程池?线程池是怎么提升效率的?
使用线程池可以避免手动创建线程带来的难以管理和频繁创建开销大的缺点。合理的使用线程池可以降低资源消耗,提高响应速度和提高线程的可管理性。
具体来说,线程池可以重复利用已创建的线程,来降低线程创建和销毁带来的开销;同时也可以使任务到达时就可以立刻执行,响应速度快;线程池还可以对创建的线程进行统一分配,调度和监控。
2、简单说一下线程池都有哪些可选参数?
Java 可以使用 ThreadPoolExecutor 类来手动创建线程池,它的构造参数有七个,有:线程池的核心线程数和最大线程数,空闲线程的存活时长和时长单位,存放任务的阻塞队列,线程工厂和拒绝执行策略。
其中存放任务的阻塞队列常见的有四种可选,前两种分别是基于数组或链表的队列,都按 FIFO 排序任务。第三种是不存储元素的阻塞队列,每个任务的加入都必须阻塞等待,直到腾出来线程执行。第四种是一个具有优先级的无限阻塞队列。
3、线程数是越大越好吗?讲一下核心线程数与最大线程数之间的关系?
不是的,如果程序是 CPU 密集型任务,线程数不能太大,避免线程间的频繁切换;如果程序是 IO 密集型任务,可以适当增加线程数,来充分利用 CPU 等待 IO 的时间,提高吞吐量。但即使是 IO 密集型任务线程数也不能过大,否则也会出现线程频繁切换带来的额外开销。
核心线程数表示线程池中保持活动状态的线程数量,即空闲线程的最小数量,这些线程执行完任务后不会被销毁。
最大线程数是指线程池中允许的最大线程数量,包括核心线程数和非核心线程数。非核心线程数是在任务队列已满且核心线程数已满的情况下,创建的额外线程数量,这些线程在执行完任务后就会被销毁。
4、运行的时候,核心线程数能不能修改?
可以,ThreadPoolExecutor 类提供了 setCorePoolSize( ),可以在线程池运行时动态修改核心线程数。如果我们调用这个方法尝试减少核心线程数,那么就会中断一部分空闲线程。如果是增加,则需要根据当前工作队列是否有任务来决定是否增加核心线程。
5、一个任务进来后,线程池是怎么处理的?
首先需要判断核心线程池是否已满,如果没满则创建一个核心线程执行任务。
如果核心线程池里的线程都在执行任务,接着要判断工作队列是否已满,如果没满则将这个任务添加到工作队列中。
如果工作队列也满了,还需判断线程池中所有的线程是否超出了最大线程数,如果没有,则创建一个非核心线程执行任务。
如果整个线程池都满了,则需要交给创建线程时指定的「拒绝执行策略」去处理。
6、线程池的拒绝策略有哪几种?讲一下常见的几种
常见的有四种策略:
第一种是异常策略(AbortPolicy),直接抛出异常。
第二章是调用者运行策略(CallerRunsPolicy),将该任务交给调用者所在的线程去执行。
第三种是丢弃最久策略(DiscardOldestPolicy),丢弃队列中最早提交的任务,并执行当前任务。
第四种是直接丢弃策略(DiscardPolicy),直接丢弃不处理。
7、平时可以怎么创建一个线程池?
最好是用 ThreadPoolExecutor 创建,不推荐使用 Executors 创建。因为从 Executors 返回的线程池对象,请求队列的长度或者允许创建的线程数为 Integer.MAX_VALUE,可能会堆积大量的请求或者线程,导致 OOM。
8、线程池有哪些关闭方式?他们的区别?
有两种方式,使用 shutdown( ) 或者 shutdownNow( ) 来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程。但也有一定的区别,shutdown( ) 只是线程池的状态设置成 SHUTDOWN,然后中断所有没有执行任务的线程,而 shutdownNow( ) 是先将线程池状态设为 STOP,然后尝试停止所有的正在执行任务的线程,并返回等待执行任务的列表。
无论调用这两个方法的哪一个,isShutdown( ) 都会返回 true。当所有的任务都执行完后,才真正表示线程池关闭成功,此时 isTerminaed( ) 返回 true。实际中如果期望此时正在执行的任务继续执行完,可以用 shutdown( ),反之可以用 shutdownNow( )。
9、使用例子展示
public class ThreadPoolExample {
public static void main(String[] args) {
int corePoolSize = 5; // 核心线程数
int maxPoolSize = 10; // 最大线程数
long keepAliveTime = 5000; // 线程空闲时间
BlockingQueue workQueue = new ArrayBlockingQueue<>(10); // 任务队列
ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 线程工厂
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略
// 创建 ThreadPoolExecutor 对象
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.MILLISECONDS,
workQueue, threadFactory, handler);
// 提交任务给线程池执行
for (int i = 0; i < 12; i++) {
Runnable worker = new WorkerThread("Task " + (i + 1));
executor.execute(worker);
}
// 使用 shutdown() 关闭线程池, 大家可以自己试试 shutdownNow() 的效果
System.out.println("--Start to shutdown the pool!");
executor.shutdown();
while (!executor.isTerminated()) {
// 等待所有任务完成
}
System.out.println("--All tasks are completed.");
}
}
class WorkerThread implements Runnable {
private final String taskName;
public WorkerThread(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println("Start executing: " + taskName);
try {
// 模拟任务执行时间
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Finished executing: " + taskName);
}
}
控制台输出:
Start executing: Task 2
Start executing: Task 1
Start executing: Task 3
Start executing: Task 4
Start executing: Task 5
--Start to shutdown the poo
// 等待了 2s
Finished executing: Task 2
Finished executing: Task 3
Finished executing: Task 1
Finished executing: Task 4
Start executing: Task 6
Finished executing: Task 5
Start executing: Task 8
Start executing: Task 7
Start executing: Task 9
Start executing: Task 10
// 等待了 2s
Finished executing: Task 7
Finished executing: Task 9
Finished executing: Task 6
Finished executing: Task 8
Start executing: Task 12
Start executing: Task 11
Finished executing: Task 10
// 等待了 2s
Finished executing: Task 12
Finished executing: Task 11
--All tasks are completed.