面试官:Synchronized与 ReentrantLock区别是什么?
yuyutoo 2024-12-12 15:54 4 浏览 0 评论
基本概念
Synchronized:Synchronized是 Java 中的一个关键字,用于实现线程同步。它可以修饰方法或者代码块,当一个线程访问被synchronized修饰的方法或者代码块时,其他线程必须等待该线程释放锁之后才能访问。例如,在一个多线程环境下的类中:
class SynchronizedExample {
private int count = 0;
// 修饰方法的加锁形式
public synchronized void increment() {
count++;
}
}
在这个例子中,increment方法被synchronized修饰,这是一种对方法整体加锁的形式。当一个线程执行这个方法时,其他线程不能同时执行这个方法,必须等待正在执行的线程完成并释放锁后才能执行。另外,还可以对代码块加锁,如下所示:
class SynchronizedBlockExample {
private Object lock = new Object();
private int count = 0;
public void increment() {
// 对代码块加锁,指定锁对象为lock
synchronized (lock) {
count++;
}
}
}
这种方式可以更灵活地控制加锁的范围,只对需要同步的代码块进行加锁。释放锁是自动完成的,当synchronized修饰的方法或者代码块执行完毕后,锁会自动释放。
- ReentrantLock:ReentrantLock是java.util.concurrent.locks包下的一个类,它也用于实现线程同步。它提供了比synchronized更灵活的锁机制。例如:
import java.util.concurrent.locks.ReentrantLock;
class ReentrantLockExample {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
这里通过ReentrantLock的lock方法获取锁,在finally块中使用unlock方法释放锁,确保锁最终会被释放,防止死锁。这是ReentrantLock典型的加锁形式,在需要同步的代码块开始处调用lock方法获取锁,在代码块结束后的finally块中释放锁,保证无论是否发生异常,锁都能被正确释放。
功能特性方面的区别
可中断性
- Synchronized:不可以中断。如果一个线程正在等待获取被synchronized修饰的资源锁,那么这个等待过程是不可中断的。例如,在一个长时间等待获取锁的场景中,线程无法被外部信号中断等待状态,只能一直等待直到获取到锁。
- ReentrantLock:可以中断。ReentrantLock提供了lockInterruptibly方法,允许一个线程在等待获取锁的过程中被中断。例如,在一个多线程任务中,如果某个线程等待锁的时间过长,可以通过中断机制来终止这个线程的等待状态,这样可以更好地处理一些可能出现的异常或者超时情况。
公平性
- Synchronized:是非公平锁。这意味着当多个线程竞争锁时,没有任何机制保证等待时间最长的线程先获取锁。例如,假设有三个线程 A、B、C 同时竞争一个被synchronized修饰的资源,线程 A 可能在刚刚释放锁后,新请求锁的线程 C 就获取到了锁,而不是等待时间更长的线程 B。
- ReentrantLock:可以通过构造函数设置为公平锁或者非公平锁。当设置为公平锁时,多个线程竞争锁时,等待时间最长的线程会优先获取锁。这样在一些对公平性要求较高的场景下,如资源分配系统,使用公平锁可以保证每个线程都能按照请求锁的顺序来获取锁,避免线程饥饿现象。
锁绑定多个条件(Condition)的能力
- Synchronized:没有直接提供类似于ReentrantLock中Condition的功能。在synchronized中,如果需要实现类似的功能,可能需要通过Object类的wait、notify和notifyAll方法来间接实现,但是这种方式相对比较复杂,而且不够灵活。
- ReentrantLock:可以通过newCondition方法创建多个Condition对象,每个Condition对象可以和锁绑定,用于实现更精细的线程等待和唤醒机制。例如,在一个生产者 - 消费者模型中,可以使用不同的Condition对象来分别控制生产者线程和消费者线程的等待和唤醒,使得代码逻辑更加清晰。
灵活性区别
Synchronized:
- 灵活性相对较差。因为它是 Java 语言的关键字,其使用方式比较固定。一旦使用synchronized修饰方法或者代码块,它的行为就由 Java 语言规范确定,很难进行定制化的操作。例如,无法在运行时动态地改变锁的公平性或者中断等待获取锁的线程。而且,在处理多个等待条件时,通过Object类的wait、notify和notifyAll方法来实现,需要严格遵循一定的规则,否则容易出现死锁或者程序逻辑错误。
- 对于代码结构的影响较大。synchronized关键字直接作用于方法或者代码块,可能会导致代码的结构不够灵活。如果需要对同步的范围或者条件进行修改,可能需要对代码进行较大的改动。例如,如果要将一个被synchronized修饰的方法拆分成多个小方法,并且保证同步逻辑正确,需要仔细考虑每个小方法的同步方式,可能会涉及到重新设计整个同步策略。
ReentrantLock:
- 提供了更高的灵活性。开发者可以根据具体的需求在代码中灵活地控制锁的获取、释放、公平性以及与多个条件的关联等操作。例如,可以在不同的业务逻辑分支中,根据实际情况选择不同的锁获取方式(如lock方法或者lockInterruptibly方法)。而且,可以在运行时动态地改变锁的公平性设置,以适应不同的业务场景。
- 能够更好地适应复杂的业务逻辑变化。由于其灵活的特性,在业务逻辑发生变化时,如需要添加新的等待条件或者改变线程的等待和唤醒策略,ReentrantLock可以通过创建新的Condition对象或者修改已有的Condition对象的操作来实现,而不需要对整个代码结构进行大规模的修改。这种灵活性使得ReentrantLock在处理复杂的多线程同步场景时更加得心应手。
条件队列和加锁、释放锁形式的区别
Synchronized的条件队列相关操作
- 等待条件(wait):在synchronized块中,可以使用Object类的wait方法让当前线程等待。例如,在一个生产者 - 消费者模型中,消费者线程在获取到锁进入synchronized块后,如果发现没有数据可消费,会调用wait方法。这个方法会释放当前持有的锁,并将线程添加到对象的等待队列中,线程进入等待状态,直到被notify或notifyAll方法唤醒。等待队列是和对象关联的,每个对象都有自己的等待队列。
- 唤醒操作(notify和notifyAll):notify方法会随机唤醒等待队列中的一个线程,而notifyAll方法会唤醒等待队列中的所有线程。这些操作必须在持有与wait方法相同对象锁的synchronized块中执行。例如,生产者线程生产了新的数据后,在synchronized块中调用notify或notifyAll来唤醒等待的消费者线程。这种方式相对比较粗糙,因为无法精准地控制唤醒特定条件下的线程。
ReentrantLock的条件队列相关操作(Condition)
- 创建条件队列(newCondition):ReentrantLock通过newCondition方法创建Condition对象,每个Condition对象都代表一个条件队列。例如,在生产者 - 消费者模型中,可以创建两个Condition对象,一个用于生产者线程等待消费者线程消费完数据(producerCondition),另一个用于消费者线程等待生产者线程生产数据(consumerCondition)。
- 等待条件(await)和唤醒操作(signal和signalAll):线程在获取ReentrantLock锁后,可以通过Condition对象的await方法将自己添加到对应的条件队列中等待。例如,消费者线程获取锁后,如果没有数据可消费,通过consumerCondition.await()等待。signal方法会唤醒条件队列中的一个线程,signalAll方法会唤醒所有线程。这些操作更加灵活,因为可以根据不同的Condition对象精准地控制线程的等待和唤醒,实现更复杂的同步逻辑。
加锁和释放锁形式对条件队列的影响
- Synchronized:由于加锁和释放锁是隐式的(方法执行完或者代码块结束自动释放),在使用wait和notify/notifyAll时,必须严格遵循在持有锁的情况下调用这些方法的规则。否则会抛出IllegalMonitorStateException异常。这种隐式的加锁和释放锁形式使得在处理复杂的条件等待和唤醒逻辑时容易出错。
- ReentrantLock:加锁和释放锁是显式的,开发者可以更好地控制锁的范围和时机。在使用Condition对象的await、signal和signalAll操作时,由于锁的显式控制,可以更清晰地保证在正确的时机执行这些操作,避免了synchronized中可能出现的因加锁和释放锁时机不当导致的问题。同时,ReentrantLock可以在不同的业务逻辑阶段灵活地获取和释放锁,方便与Condition对象配合,实现更精细的线程调度和同步。
锁的实现机制
Synchronized:
- 对象头和锁状态:synchronized的实现依赖于对象头中的标记位来存储锁状态信息。在 Java 对象头中,有一部分空间用于存储对象的哈希码、分代年龄等信息,同时也用于表示锁的状态。锁状态主要有偏向锁、轻量级锁和重量级锁。当没有线程竞争锁时,对象可能处于无锁状态或者偏向锁状态。偏向锁是一种优化机制,它会偏向于第一个获取锁的线程,将对象头中的部分标记位设置为该线程的 ID,这样当这个线程再次访问同步块时,不需要进行复杂的锁获取操作,直接就可以进入同步块。
- 锁升级过程:当有其他线程尝试竞争偏向锁时,会触发锁升级。首先会升级为轻量级锁,轻量级锁是通过在栈帧中创建一个锁记录(Lock Record)来实现的。线程会将对象头中的部分信息复制到锁记录中,然后使用 CAS(Compare - and - Swap)操作尝试将对象头中的指针指向自己的锁记录,这个过程是原子操作。如果 CAS 操作成功,线程就获取了轻量级锁;如果失败,表示有其他线程也在竞争锁,此时会进入自旋状态,即线程会不断地尝试获取锁,而不是立即阻塞。如果自旋一定次数后(这个次数可以通过 JVM 参数配置)仍然无法获取锁,轻量级锁会进一步升级为重量级锁。重量级锁会使线程进入阻塞状态,等待操作系统的调度,这个过程涉及到操作系统的内核态和用户态切换,开销较大。
ReentrantLock:
- 基于 AQS 的实现:ReentrantLock是基于AbstractQueuedSynchronizer(AQS)实现的。AQS 是一个用于构建锁和同步器的框架,它维护了一个等待队列(FIFO 队列),用于存储等待获取锁的线程。当一个线程调用ReentrantLock的lock方法获取锁时,会通过 AQS 的acquire方法尝试获取锁。如果获取成功,线程就可以执行同步块中的代码;如果获取失败,线程会被封装成一个Node对象添加到等待队列中,并且线程会进入阻塞状态。AQS 通过一个int类型的变量来表示锁的状态,对于ReentrantLock来说,这个变量用于记录锁的重入次数。当一个线程已经获取了锁,再次调用lock方法时,重入次数会增加;当线程调用unlock方法时,重入次数会减少,直到重入次数为 0 时,锁才会被真正释放,并且会唤醒等待队列中的下一个线程。
- 公平锁和非公平锁的实现差异:在公平锁的实现中,当一个线程调用lock方法获取锁时,会首先检查等待队列中是否有等待时间更长的线程。如果有,它会将自己添加到队列的末尾,等待前面的线程获取并释放锁后,再按照队列顺序获取锁。而在非公平锁的实现中,线程调用lock方法时,会首先尝试通过 CAS 操作直接获取锁,而不考虑等待队列中的其他线程。如果获取成功,就直接获取了锁;如果失败,再按照公平锁的方式将自己添加到等待队列中等待获取锁。这种实现方式使得非公平锁在性能上可能会优于公平锁,因为它减少了线程排队等待的时间,但是可能会导致某些线程长时间无法获取锁,产生线程饥饿现象。
性能方面的区别
在低竞争场景下
- Synchronized:在 Java 早期版本中,synchronized的性能相对较差,但在 Java 6 及以后的版本中,对synchronized进行了大量优化,在低竞争(即很少有线程同时竞争锁)的场景下,synchronized的性能已经和ReentrantLock相近,因为它的实现已经比较高效,并且 JVM 可以对其进行一些优化,如偏向锁、轻量级锁等。
- ReentrantLock:在低竞争场景下,ReentrantLock由于其额外的功能(如可中断、公平性设置等)会带来一些性能开销,它的性能可能稍逊于synchronized,不过这种差异在大多数情况下并不明显。
在高竞争场景下
- Synchronized:在高竞争(多个线程频繁竞争锁)场景下,由于synchronized是基于对象头的锁机制,当锁竞争激烈时,可能会频繁地进行锁升级(从偏向锁到轻量级锁再到重量级锁),这会带来一定的性能损耗。而且一旦升级为重量级锁,线程的阻塞和唤醒等操作会涉及到操作系统层面的系统调用,开销较大。
- ReentrantLock:ReentrantLock在高竞争场景下相对更有优势。它的内部实现采用了一种更灵活的自旋锁(spin lock)机制,在一定程度上可以减少线程阻塞和唤醒的开销。并且,开发者可以根据具体的场景对ReentrantLock进行优化,如合理设置公平性和使用合适的锁获取方法等,以提高在高竞争场景下的性能。
使用场景方面的区别
- 简单同步场景Synchronized:适合简单的线程同步场景,尤其是在代码结构比较简单,对锁的功能要求不高(如不需要中断锁等待、不需要精细的条件控制等)的情况下。例如,在一个单例模式的实现中,使用synchronized来保证线程安全的实例创建是一个简单有效的方式。
- 复杂同步场景ReentrantLock:适用于复杂的多线程同步场景,当需要使用可中断锁、公平锁或者需要基于多个条件进行线程等待和唤醒时,ReentrantLock是更好的选择。例如,在一个多线程资源调度系统中,需要根据不同的资源状态和任务优先级来灵活地控制线程的等待和执行,ReentrantLock结合Condition可以很好地满足这种复杂的需求。
相关推荐
- 全局和隐式 using 指令详解(全局命令)
-
1.什么是全局和隐式using?在.NET6及更高版本中,Microsoft引入了...
- 请停止微服务,做好单体的模块化才是王道:Spring Modulith介绍
-
1、介绍模块化单体是一种架构风格,代码是根据模块的概念构成的。对于许多组织而言,模块化单体可能是一个很好的选择。它有助于保持一定程度的独立性,这有助于我们在需要的时候轻松过渡到微服务架构。Spri...
- ASP.NET程序集引用之痛:版本冲突、依赖地狱等解析与实战
-
我是一位多年后端经验的工程师,其中前几年用ASP.NET...
- .NET AOT 详解(.net 6 aot)
-
简介AOT(Ahead-Of-TimeCompilation)是一种将代码直接编译为机器码的技术,与传统的...
- 一款基于Yii2开发的免费商城系统(一款基于yii2开发的免费商城系统是什么)
-
哈喽,我是老鱼,一名致力于在技术道路上的终身学习者、实践者、分享者!...
- asar归档解包(游戏arc文件解包)
-
要学习Electron逆向,首先要有一个Electron开发的程序的发布的包,这里就以其官方的electron-quick-start作为例子来进行一下逆向的过程。...
- 在PyCharm 中免费集成Amazon CodeWhisperer
-
CodeWhisperer是Amazon发布的一款免费的AI编程辅助小工具,可在你的集成开发环境(IDE)中生成实时单行或全函数代码建议,帮助你快速构建软件。简单来说,AmazonCodeWhi...
- 2014年最优秀JavaScript编辑器大盘点
-
1.WebstormWebStorm是一种轻量级的、功能强大的IDE,为Node.js复杂的客户端开发和服务器端开发提供完美的解决方案。WebStorm的智能代码编辑器支持JavaScript,...
- 基于springboot、tio、oauth2.0前端vuede 超轻量级聊天软件分享
-
项目简介:基于JS的超轻量级聊天软件。前端:vue、iview、electron实现的PC桌面版聊天程序,主要适用于私有云项目内部聊天,企业内部管理通讯等功能,主要通讯协议websocket。支持...
- JetBrains Toolbox推出全新产品订阅授权模式
-
捷克知名软件开发公司JetBrains最为人所熟知的产品是Java编程语言开发撰写时所用的集成开发环境IntelliJIDEA,相信很多开发者都有所了解。而近期自2015年11月2日起,JetBr...
- idea最新激活jetbrains-agent.jar包,亲测有效
-
这里分享一个2019.3.3版本的jetbrains-agent.jar,亲测有效,在网上找了很多都不能使用,终于找到一个可以使用的了,这里分享一下具体激活步骤,此方法适用于Jebrains家所有产品...
- CountDownTimer的理解(countdowntomars)
-
CountDownTimer是android开发常用的计时类,按照注释中的说明使用方法如下:kotlin:object:CountDownTimer(30000,1000){...
- 反射为什么性能会很慢?(反射时为什么会越来越长)
-
1.背景前段时间维护一个5、6年前的项目,项目总是在某些功能使用上不尽人意,性能上总是差一些,仔细过了一下代码发现使用了不少封装好的工具类,工具类里面用了好多的反射,反射会影响到执行效率吗?盲猜了一...
- btrace 开源!基于 Systrace 高性能 Trace 工具
-
介绍btrace(又名RheaTrace)是抖音基础技术团队自研的一款高性能AndroidTrace工具,它基于Systrace实现,并针对Systrace不足之处加以改进,核心改进...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- .NET 奇葩问题调试经历之3——使用了grpc通讯类库后,内存一直增长......
- 全局和隐式 using 指令详解(全局命令)
- 请停止微服务,做好单体的模块化才是王道:Spring Modulith介绍
- ASP.NET程序集引用之痛:版本冲突、依赖地狱等解析与实战
- .NET AOT 详解(.net 6 aot)
- 一款基于Yii2开发的免费商城系统(一款基于yii2开发的免费商城系统是什么)
- asar归档解包(游戏arc文件解包)
- 在PyCharm 中免费集成Amazon CodeWhisperer
- 2014年最优秀JavaScript编辑器大盘点
- 基于springboot、tio、oauth2.0前端vuede 超轻量级聊天软件分享
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)