Java并发编程之锁的艺术:面试与实战指南(四)
文章目录
🌈你好呀!我是 山顶风景独好
💝欢迎来到我的博客,很高兴能够在这里和您见面!
💝希望您在这里可以感受到一份轻松愉快的氛围!
💝不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
🚀 欢迎一起踏上探险之旅,挖掘无限可能,共同成长!
前言
本系列地址:
Java并发编程之锁的艺术:面试与实战指南(一)
Java并发编程之锁的艺术:面试与实战指南(二)
Java并发编程之锁的艺术:面试与实战指南(三)
Java并发编程之锁的艺术:面试与实战指南(四)
二十七、什么是AQS(AbstractQueuedSynchronizer)?它在Java并发包中是如何应用的?
AbstractQueuedSynchronizer(AQS) 是Java并发包(java.util.concurrent.locks)中的一个核心类,用于构建依赖于先进先出(FIFO)等待队列的阻塞锁和相关的同步器,如ReentrantLock、Semaphore、CountDownLatch、FutureTask等。
AQS的主要特点:
- 内部状态(State):AQS使用一个volatile int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。
- FIFO队列:AQS使用一个内部的FIFO队列来完成等待队列的管理,当前线程获取同步状态失败时,AQS会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入等待队列,同时会阻塞当前线程,当同步状态释放时,会把节点从队列中唤醒出来,使得线程能够再次尝试获取同步状态。
- 模板方法:AQS通过定义一系列模板方法,使得子类可以通过继承AQS并重写这些模板方法来管理同步状态,从而简化同步器的实现。
AQS在Java并发包中的应用:
- 独占模式(Exclusive Mode):
在独占模式下,只有一个线程能够获得同步状态,其他尝试获取同步状态的线程将被阻塞。如ReentrantLock就是基于AQS的独占模式实现的。
AQS通过acquire(int arg)和release(int arg)两个模板方法分别实现独占模式下的获取和释放同步状态。 - 共享模式(Shared Mode):
在共享模式下,多个线程可以同时获得同步状态。如Semaphore、CountDownLatch就是基于AQS的共享模式实现的。
AQS通过acquireShared(int arg)和releaseShared(int arg)两个模板方法分别实现共享模式下的获取和释放同步状态。 - AQS的模板方法:
- tryAcquire(int acquires): 尝试以独占模式获取同步状态。
- tryRelease(int releases): 尝试以独占模式释放同步状态。
- tryAcquireShared(int acquires): 尝试以共享模式获取同步状态。
- tryReleaseShared(int releases): 尝试以共享模式释放同步状态。
子类需要重写上述方法中的一部分或全部,以实现自定义的同步器。
示例:
以ReentrantLock为例,它内部使用了一个继承自AQS的Sync类来实现锁的获取和释放。当线程调用lock()方法时,会调用AQS的acquire(int arg)模板方法,该方法会先调用tryAcquire(int acquires)尝试获取同步状态,如果获取失败,则通过addWaiter(Node node)将当前线程封装成节点并加入等待队列,并通过自旋或阻塞的方式等待获取同步状态。当线程调用unlock()方法时,会调用AQS的release(int arg)模板方法,该方法会调用tryRelease(int releases)尝试释放同步状态,并唤醒等待队列中的节点。
二十八、什么是锁的顺序和锁偏序?
-
锁的顺序是指多个线程在尝试获取锁时遵循的特定顺序。如果所有线程都按照相同的顺序请求锁,那么可以避免出现循环等待的情况,从而避免死锁。也就是说,当多个线程需要访问多个共享资源时,它们应该按照一致的顺序来请求这些锁,这样可以确保不会出现循环等待,从而防止死锁的发生。
-
锁偏序则是指由于锁的获取顺序不一致导致的非确定性行为。在并发编程中,当多个线程需要访问多个共享资源时,如果它们以不同的顺序获取这些锁,就可能导致锁偏序问题。这种不一致的锁获取顺序可能会导致程序的行为变得不确定,因为线程的执行顺序和锁的获取顺序都可能影响到程序的最终结果。为了避免锁偏序问题,开发人员应该尽量确保线程按照一致的顺序来获取锁,或者采用其他并发控制策略来确保程序的正确性和可预测性。
二十九、Java中的锁策略有哪些?它们各自适用于什么场景?
- 悲观锁与乐观锁:
- 悲观锁:悲观锁预期数据冲突的可能性很大,因此在数据被处理前就会对数据进行加锁,并在整个数据处理过程中保持锁状态。这种策略适用于写操作频繁的场景,因为写操作往往伴随着数据的修改,冲突的可能性较大。Java中的synchronized关键字和ReentrantLock类就是悲观锁的实现。
- 乐观锁:乐观锁预期数据冲突的可能性很小,因此它不会立即对数据进行加锁,而是在数据提交更新时,才会正式对数据冲突与否进行检测。如果发生冲突,则返回失败信息,让用户决定如何去做。这种策略适用于读操作频繁的场景,因为读操作不会修改数据,冲突的可能性较小。Java中的AtomicInteger、AtomicStampedReference以及CAS(Compare-and-Swap)算法等都是乐观锁的实现。
- 重量级锁与轻量级锁:
- 重量级锁:加锁解锁的开销比较大,通常涉及到内核态的切换。这种锁适用于竞争激烈的场景,因为当多个线程同时请求锁时,重量级锁可以通过阻塞等待的方式,确保线程安全地访问共享资源。然而,由于重量级锁的开销较大,因此可能会降低程序的性能。Java中的synchronized关键字在JDK 1.6之前主要是重量级锁的实现。
- 轻量级锁:加锁解锁的开销比较小,通常只在用户态完成。这种锁适用于竞争不激烈的场景,因为轻量级锁可以通过自旋等待的方式,尝试获取锁,从而避免线程被阻塞和唤醒的开销。当自旋达到一定次数仍未获取到锁时,轻量级锁会升级为重量级锁。Java中的synchronized关键字在JDK 1.6及以后版本对锁的实现进行了优化,引入了偏向锁和轻量级锁等概念,使得在大多数情况下,synchronized的性能得到了显著提升。
- 公平锁与非公平锁:
- 公平锁:表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最先获得锁。这种策略可以保证线程的公平性,但是可能会降低程序的吞吐量,因为线程可能需要等待更长的时间才能获取到锁。Java中的ReentrantLock类可以通过设置构造函数中的fair参数为true来实现公平锁。
- 非公平锁:与公平锁相反,非公平锁不保证线程获取锁的顺序。这种策略可能会提高程序的吞吐量,因为线程在等待较短的时间后就有可能获取到锁。然而,非公平锁可能会导致某些线程长时间得不到执行,从而影响程序的公平性。Java中的ReentrantLock类默认实现的是非公平锁。
此外,还有偏向锁、行锁、表锁、页锁等锁策略,它们各自适用于不同的场景。例如,偏向锁适用于只有一个线程访问共享资源的场景;行锁适用于需要对数据库表中的某一行进行加锁的场景;表锁适用于需要对整个数据库表进行加锁的场景;页锁则介于行锁和表锁之间,适用于需要对数据库表中的某一页进行加锁的场景。
三十、什么是线程状态?Java中线程有哪些状态?
- 新建状态(NEW):
当线程对象被创建后,即进入了新建状态,此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量的值,线程对象本身是还没有生命的。和其他Java对象不同的是,线程对象是一个特殊的对象,它被创建后,系统必须为其分配资源才能执行,当调用线程的start()方法时,此线程才进入可运行状态,不同线程的启动均由Java运行系统统一调度和控制的。 - 就绪状态(Runnable):
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 - 运行状态(Running):
线程获得CPU资源并执行程序代码时,线程便处于运行状态。此时,它不一定正在执行程序代码,因为Java采用的是抢占式调度模式,线程之间根据优先级自动调度。当然,也可以手动调用Thread.yield()方法来让出CPU使用权,或者调用Thread.sleep(int milltime)方法使当前线程休眠一段时间。 - 阻塞状态(Blocked):
阻塞状态是指线程因为某种原因放弃了CPU使用权,也即让出了CPU time slice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得CPU time slice转到运行(running)状态。阻塞的情况分三种:- 等待阻塞:运行状态的线程执行wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
- 同步阻塞:运行状态的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
- 其他阻塞:运行状态的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
- 等待状态(Waiting):
进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。 - 超时等待状态(Timed Waiting):
该状态不同于等待(Waiting)状态,它可以在指定的时间后自行返回。 - 终止状态(Terminated):
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
三十一、请解释Java中的Happens-Before规则。
Java中的Happens-Before规则是Java内存模型(Java Memory Model, JMM)中的一个核心概念,用于描述和保证多线程环境下操作的可见性和顺序性。它定义了一种偏序关系,即当一个操作A Happens-Before操作B时,意味着A的操作结果对B是可见的,并且A的执行顺序排在B之前。
Happens-Before规则可以确保在并发环境下,操作之间的依赖关系得到正确的执行和可见性保证。这个规则并不是按照字面意思理解的“先行发生”,而是可以理解为“先行可见”,即A操作的结果在B操作开始之前就已经对B可见。
Happens-Before规则包括以下几种情况:
- 程序顺序规则:在一个线程内,按照程序代码顺序,前面的操作Happens-Before于后续的任意操作。这是最基本的规则,它确保了单线程内操作的顺序性。
- 监视器锁规则:对一个监视器锁的解锁操作Happens-Before于随后对这个监视器锁的加锁操作。这个规则确保了持有同一个监视器锁的线程之间的操作顺序。
- volatile变量规则:对一个volatile变量的写操作Happens-Before于后续对这个volatile变量的读操作。这个规则保证了volatile变量的可见性。
- 线程启动规则:Thread对象的start()方法调用Happens-Before于该线程的每一个动作。这确保了线程启动后,主线程对该线程对象的修改对其他线程是可见的。
- 线程终止规则:线程中的所有操作都Happens-Before于任何线程检测到该线程的终止。这确保了在线程终止前,其他线程对该线程对象的修改是可见的。
- 线程中断规则:对线程interrupt()方法的调用Happens-Before于被中断线程的代码检测到中断事件的发生。这确保了中断操作的及时性和可见性。
- 对象的终结规则:一个对象的初始化完成(构造函数执行结束)Happens-Before于它的finalize()方法的开始。这确保了对象在被回收前,其状态对其他线程是可见的。
三十二、什么是Java的内存屏障?它如何保证并发安全性?
内存屏障的主要作用包括:
- 禁止重排序:内存屏障可以防止编译器和处理器对指令进行重排序,确保按照程序员编写的代码顺序执行。这是非常重要的,因为重排序可能会导致线程之间的数据不一致和不可预测的行为。
- 保证写入的可见性:内存屏障可以确保写入一个共享变量的操作在后续的读取操作之前对其他线程可见。这保证了数据的正确性和一致性,避免了脏读和脏写等问题。
在Java中,内存屏障主要通过以下几种方式保证并发安全性:
- volatile关键字:当使用volatile修饰变量时,Java会在该变量的写操作和读操作之间插入StoreLoad屏障。这确保了volatile变量的写操作在所有后续读操作之前完成,从而保证了volatile变量的可见性。
- synchronized关键字:synchronized块内部也会插入内存屏障,以确保在同步块内的写操作在离开同步块时将数据更新到内存,并且后续读操作只能从内存中读取数据,而不能从缓存中读取。这保证了synchronized块内的数据对其他线程是可见的。
三十三、Java中的锁升级是什么?为什么需要锁升级?
为什么需要锁升级?
锁升级的主要目的是为了优化多线程环境下的性能,并平衡线程安全和性能之间的需求。具体来说,锁升级有以下几个方面的作用:
- 提高性能:在锁竞争不激烈的情况下,使用低级别的锁(如偏向锁、轻量级锁)可以减少线程挂起和上下文切换的开销,从而提高系统的整体性能。当锁竞争变得激烈时,再逐渐升级到高级别的锁(如重量级锁),以保证线程安全。
- 减少线程阻塞:偏向锁和轻量级锁都试图通过减少线程挂起和上下文切换来减少线程阻塞。偏向锁会在无竞争的情况下将锁偏向于第一个访问该锁的线程,而轻量级锁则使用自旋等待的方式尝试获取锁,而不是立即挂起线程。这些策略都可以减少线程阻塞,提高系统的响应速度。
- 平衡线程安全和性能:在多线程环境下,线程安全和性能往往是相互矛盾的。锁升级策略可以根据实际的竞争情况动态调整锁的级别,从而在保证线程安全的前提下尽可能提高系统的性能。
锁升级的过程:
- 无锁状态:这是锁的初始状态,表示没有线程持有该锁。
- 偏向锁:当第一个线程访问某个同步代码块或方法时,JVM会尝试为该线程加上偏向锁。如果后续没有其他线程访问该同步块,那么持有偏向锁的线程将一直执行,无需再进行任何同步操作。
- 轻量级锁:当有其他线程尝试访问已经被持有偏向锁的同步块时,偏向锁会升级为轻量级锁。轻量级锁会尝试使用自旋等待的方式获取锁,而不是立即挂起线程。如果自旋等待成功,则线程获取到锁并执行同步代码块;如果自旋等待失败,则锁会升级为重量级锁。
- 重量级锁:当轻量级锁自旋等待失败或竞争情况非常激烈时,锁会升级为重量级锁。重量级锁会挂起正在等待的线程,并将线程放入等待队列中。当持有锁的线程释放锁时,会从等待队列中唤醒一个线程并赋予其锁的持有权。