🔥个人主页: 中草药
🩺一.synchronized原理
基本原理
synchronized
关键字的基本原理是通过使用互斥锁(也称为独占锁)来实现线程间的同步。当一个线程获取了一个对象的锁后,其他试图获取该对象锁的线程将会被阻塞,直到锁被释放。
基本特点
- 开始时是乐观锁,如果锁冲突频繁,就转化为悲观锁
- 开始是轻量级锁实现,如果锁被持有的时间较长,就转化为重量级锁
- 实现轻量级锁的时候,大概率用到自旋锁策略
- 是一种不公平锁
- 是一种可重入锁
- 不是读写锁
锁升级
-
无锁状态:
- 初始状态下,对象处于无锁状态,没有任何线程持有锁。
- 当一个线程首次尝试获取锁时,锁会从无锁状态升级到轻量级锁状态。
-
偏向锁状态:
- 偏向锁是Java 6中引入的一种锁优化技术,旨在减少无竞争情况下的同步开销。
- 当一个线程第一次访问一个对象的同步代码块时,该线程会获得一个偏向锁,以后该线程再次访问时,可以直接进入同步代码块而无需加锁。
- 如果有其他线程尝试获取锁,偏向锁将被撤销,并升级为轻量级锁或重量级锁。
-
偏向锁本质上相当于 "延迟加锁" . 能不加锁就不加锁, 尽量来避免不必要的加锁开销.但是该做的标记还是得做的, 否则⽆法区分何时需要真正加锁
-
轻量级锁状态:
- 当线程尝试获取锁时,如果当前没有其他线程持有该锁,线程将尝试使用CASS(Compare and Swap)操作来获取锁。
- CASS操作会将线程ID写入对象头的Mark Word中,如果CASS操作成功,则线程获得了锁,锁的状态变为轻量级锁。
- 如果此时有多个线程尝试获取锁,其中一个线程成功获取了锁,而其他线程的CASS操作将失败,这时轻量级锁将升级为重量级锁。如果更新失败, 则认为锁被占⽤, 继续⾃旋式的等待(并不放弃 CPU)
-
自旋操作是⼀直让 CPU 空转, ⽐较浪费 CPU 资源.因此此处的⾃旋不会⼀直持续进⾏, ⽽是达到⼀定的时间/重试次数, 就不再⾃旋了.也就是所谓的 "自适应"
-
重量级锁状态:
- 如果竞争进⼀步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁,当轻量级锁升级为重量级锁时,锁的实现将使用操作系统层面的互斥锁(Mutex Lock)。
- 重量级锁涉及到线程的挂起和唤醒,开销较大,但可以确保线程间的同步。
- 当重量级锁被释放时,如果还有其他线程等待获取锁,这些线程将被唤醒并竞争锁。
其他的优化操作
锁消除(Lock Elimination)
原理
锁消除是指在编译阶段,JVM编译器通过分析代码来判断某些synchronized
块中的锁是否真的有必要。如果编译器确定某些锁是不必要的,那么它会在编译时直接去除这些锁,从而避免运行时的锁开销。
何时发生
锁消除通常发生在以下几种情况:
-
不可变对象:
- 如果一个对象是不可变的,并且在
synchronized
块中只读取该对象的状态而不修改它,那么编译器可能会消除该锁。 - 因为不可变对象的状态一旦创建就不会改变,所以不需要对其进行同步。
- 如果一个对象是不可变的,并且在
-
局部变量:
- 如果
synchronized
块中使用的对象仅在该块内部使用,并且不会被其他线程访问,那么编译器可能会消除该锁。 - 这是因为局部变量不会被多个线程共享,所以不需要同步。
- 如果
-
单线程访问:
- 如果编译器可以确定一个
synchronized
块只能被单个线程访问,那么它可以消除该锁。 - 这种情况通常出现在非多线程环境中,或者在单线程的回调函数中。
- 如果编译器可以确定一个
有些应⽤程序的代码中, ⽤到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer-线程安全的容器)
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
锁粗化(Lock Coarsening)
锁的粒度
锁的粒度是指在多线程环境下,锁保护的数据范围的大小。锁的粒度决定了锁的精细程度,进而影响并发性能和数据一致性。锁的粒度越细,意味着锁保护的数据范围越小,这样可以减少锁的竞争,提高并发性能。相反,锁的粒度越粗,锁保护的数据范围越大,可能会增加锁的竞争,但可以简化同步逻辑。下面详细解释锁的粒度及其对程序的影响。
原理
锁粗化是指将一系列连续的synchronized
代码块合并成一个大的synchronized
块,以减少锁的获取和释放次数。这是因为锁的获取和释放涉及到一些额外的开销,比如线程上下文切换等。
何时发生
锁粗化通常发生在以下几种情况:
-
连续的
synchronized
块:- 如果一系列连续的
synchronized
块作用于同一个对象,那么JVM可能会将它们合并成一个大的synchronized
块。 - 这样可以减少锁的获取和释放次数,从而提高性能。
- 如果一系列连续的
-
短时间间隔:
- 如果连续的
synchronized
块之间的间隔非常短,那么合并它们通常是有利的。 - 这是因为短时间内多次获取和释放锁的开销大于合并后的锁开销。
- 如果连续的
🩹二.ReentrantLock
ReentrantLock
是Java中一个可重入的互斥锁,它是java.util.concurrent.locks
包中提供的一个类。ReentrantLock
提供了比内置的synchronized
关键字更强大的锁定机制,并且可以显式地控制锁的获取和释放,从而提供了更高的灵活性和更好的性能。下面详细介绍ReentrantLock
的特性和使用方法。
基本特点
-
可重入性:
ReentrantLock
支持可重入,即一个线程可以多次获取同一把锁而不会导致死锁。- 每次获取锁时,锁的计数器都会递增,只有当计数器回到0时,锁才会被释放。
-
公平性和非公平性:
ReentrantLock
支持公平性和非公平性两种模式。- 公平模式:按照线程请求锁的顺序来分配锁,可以避免某些线程长期得不到锁(线程饥饿)。
- 非公平模式:默认模式,不保证获取锁的顺序,可能会出现线程饥饿现象,但通常具有更好的性能。
-
锁的可中断性:
- 线程在等待锁的过程中可以被中断。
- 当线程等待锁时,可以响应
InterruptedException
,从而中断等待状态。
-
条件变量:
ReentrantLock
支持条件变量,可以用于实现更复杂的同步结构。- 通过
newCondition()
方法可以创建一个条件变量。
-
显式的锁定和解锁:
- 使用
lock()
方法获取锁,使用unlock()
方法释放锁。 - 由于需要手动释放锁,因此通常建议使用try-with-resources语句或finally块来确保锁的正确释放。
- 使用
使用
- lock() :加锁,如果获取不到锁就死等
- trylock(超过时间):加锁,如果获取不到锁,等待一定时间之后就放弃加锁
- unlock():解锁
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void processResource() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + ": Processing resource.");
// 执行关键操作
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
Thread thread1 = new Thread(() -> example.processResource(), "Thread 1");
Thread thread2 = new Thread(() -> example.processResource(), "Thread 2");
thread1.start();
thread2.start();
}
}
💊三.对比
下面是ReentrantLock
和synchronized
的对比,以表格形式呈现:
特性/机制 | ReentrantLock | synchronized |
---|---|---|
显式控制 | 需要显式调用lock() 和unlock() 方法 | 自动获取和释放锁 |
异常处理 | 需要显式处理异常,以确保锁正确释放 | 自动释放锁,即使抛出异常 |
灵活性 | 更高的灵活性,支持多种配置选项 | 相对简单,自动处理锁 |
可重入性 | 支持可重入 | 支持可重入 |
公平性 | 支持公平性和非公平性模式 | 默认非公平锁 |
条件变量 | 支持条件变量,通过newCondition() 创建 | 不支持条件变量 |
可中断性 | 支持等待锁时的中断 | 不支持等待锁时的中断 |
性能 | 可以根据需要选择公平性或非公平性以优化性能 | 默认非公平锁,通常提供较好的性能 |
锁的粒度 | 可以显式指定锁对象 | 对于实例方法,锁对象是该对象本身(即this )<br>对于静态方法,锁对象是类的Class对象<br>对于代码块,锁对象是显式指定的对象 |
使用场景 | 需要更精细控制或支持条件变量时使用 | 需要简单易用或避免出错可能性时使用 |
实现方式 | 是标准库中的一个类 | 是一个关键字,是JVM内部实现的(大概率基于C++实现) |
通过这个表格,您可以快速比较ReentrantLock
和synchronized
的不同特点和使用场景,以便在实际开发中做出合适的选择。
- synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待⼀段时间就放弃
- 更强⼤的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是⼀个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便. 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等. 如果需要使用公平锁, 使用 ReentrantLock
🩸四.反思
人生不是一种享乐,而是一桩十分沉重的工作。--列夫·托尔斯泰
-
选择合适的同步机制:
- 在实际应用中,需要根据具体的应用场景和需求来选择合适的同步机制。
- 如果需要更精细的控制和更复杂的同步结构,可以考虑使用ReentrantLock。
- 如果追求简单易用和自动异常处理,可以选择使用synchronized。
-
性能考量:
- 在性能敏感的应用中,需要权衡公平性和性能之间的平衡。
- 非公平锁通常提供更好的性能,但可能导致线程饥饿;公平锁可以避免线程饥饿,但可能会降低性能。
-
锁的粒度:
- 合理选择锁的粒度非常重要,细粒度锁可以提高并发性能,但可能会增加同步逻辑的复杂性。
- 粗粒度锁简化了同步逻辑,但可能会降低并发性能。
-
避免死锁:
- 使用任何类型的锁时都需要小心,以避免死锁的发生。
- 在设计同步逻辑时,需要确保锁的正确获取和释放顺序,避免潜在的死锁风险。
-
异常处理:
- 使用ReentrantLock时,需要格外注意异常处理,确保在任何情况下都能正确释放锁。
- 在使用
synchronized
时,由于自动处理锁的释放,可以减少这方面的担忧。
通过这次学习,我对ReentrantLock和synchronized有了更深的理解,并认识到在设计并发程序时需要综合考虑多种因素。在实际开发中,合理地选择和使用这两种机制可以帮助我们编写更高效、更可靠的并发程序。未来,我将继续深入研究这些知识点,并努力将它们应用到实践中去。
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐
制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸