在Java中,锁是用于多线程同步的关键机制,可以通过不同的锁来实现对共享资源的互斥访问。以下是一些常见的Java锁的深入理解:
1. Synchronized锁:
Synchronized是Java中的关键字,用于实现线程之间的互斥访问,确保在同一时刻只有一个线程可以执行被Synchronized修饰的代码块或方法。Java对象在内存中的布局包括对象头和实例数据两部分。对象头中的Mark Word用于存储对象的元数据信息,其中的一部分被用来存储锁相关的信息。以下是Synchronized锁的深入理解:
1.0.monitor
在Java字节码层面,synchronized
关键字的实现会涉及到monitorenter
和monitorexit
两个字节码指令。以下是一个简单的Java代码示例和对应的字节码:
Java代码:
public class SynchronizedExample {
private static final Object lock = new Object();
public void synchronizedMethod() {
synchronized (lock) {
// 临界区域
// ...
}
}
}
对应的字节码:
public class SynchronizedExample {
private static final java.lang.Object lock;
public SynchronizedExample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void synchronizedMethod();
Code:
0: getstatic #2 // Field lock:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter
6: aload_1
7: monitorexit
8: goto 16
11: astore_2
12: aload_1
13: monitorexit
14: aload_2
15: athrow
16: return
Exception table:
from to target type
6 8 11 any
11 14 11 any
}
解释:
synchronizedMethod
方法中,通过monitorenter
指令获取锁,monitorexit
指令释放锁。getstatic #2
:将lock
字段(即指向lock
对象的引用)推送到栈顶。dup
:复制栈顶数值并将复制值压入栈顶。这是为了在monitorexit
指令之后还能使用lock
。monitorenter
:获取锁。如果当前线程获取了锁,则计数器加一;如果锁被其他线程持有,则当前线程阻塞等待。monitorexit
:释放锁。当锁的计数器减为零时,锁被完全释放。
这就是在字节码层面对synchronized
关键字的基本实现。字节码中的monitorenter
和monitorexit
指令是实现Synchronized
锁机制的核心。
1.1. 内置锁(Intrinsic Lock):
-
Synchronized原理: Synchronized使用的是对象监视器(Object Monitor)作为内置锁,每个Java对象都与一个Object Monitor相关联。一个线程在进入Synchronized块之前会尝试获取对象的锁,如果锁被其他线程持有,则当前线程会被阻塞。
-
锁的粒度: Synchronized支持对对象的方法或代码块进行锁定,可以在方法级别或代码块级别使用。
1.2. 锁的可重入性(Reentrant):
-
概念: Synchronized是可重入锁,同一线程可以多次获得同一个锁,而不会被阻塞。这允许线程在持有锁的情况下调用自己类中的其他Synchronized方法。
-
实现: 内置锁通过一个计数器来实现可重入性,每次成功获取锁,计数器加1,释放锁时计数器减1,只有当计数器为0时才真正释放锁。
1.3. 锁的互斥性:
- 原子性: Synchronized确保了其修饰的代码块或方法的原子性执行,一个线程持有锁时,其他线程无法同时进入被锁定的代码块或方法。
1.4. 锁的等待与唤醒:
-
等待: 当一个线程尝试获取一个被其他线程持有的锁时,它将被阻塞,进入等待状态。
-
唤醒: 当一个线程释放了锁,等待在该锁上的线程将被唤醒,有机会争夺锁。
1.5. 对象监视器和锁的关系:
-
每个对象一个锁: 对象监视器是与对象相关联的,每个对象都有一个用于同步的锁。
-
Class锁: 对于静态Synchronized方法,锁是与类相关联的,称为Class锁。
1.6. 性能考虑:
-
重量级锁: Synchronized是重量级锁,存在竞争时可能涉及用户态到内核态的切换,因此在高并发情况下性能可能受到影响。
-
轻量级锁和偏向锁: 为了提高性能,Java引入了轻量级锁和偏向锁,用于减小锁的开销。在适当的场景下,虚拟机会自动优化锁的实现。
1.7. Synchronized优化(锁升级):
Mark Word中的一些位被用于表示锁的状态。主要有以下几个标志位:
- 无锁状态: 表示当前对象没有被线程持有。如果对象的锁状态为无锁状态,则线程尝试使用CAS(Compare And Swap)将锁状态设置为偏向锁,并将持有锁的线程ID记录在Mark Word中。
-
偏向锁(Biased Locking): 当一个线程获得了锁,并且没有竞争时,JVM会在对象头的标记位上设置偏向锁。这样,在之后该线程再次获取锁时,无需进行CAS等操作,提高性能。
-
轻量级锁(Lightweight Locking): 在没有竞争的情况下,JVM会将对象头中的一部分标记为“偏向锁”(biased lock),表示该对象是被一个线程所拥有的。持有锁的线程ID与当前线程ID不同,则尝试使用CAS将锁状态设置为轻量级锁,并在对象的Mark Word中创建锁记录,避免进入重量级锁的状态。
-
重量级锁(Heavyweight Locking): 当有多个线程竞争锁时,线程尝试使用CAS将锁状态设置为重量级锁,并在操作系统层面使用互斥量来实现锁。
1.8. 锁的并发性:
-
细粒度锁: 使用Synchronized时,尽量使用细粒度锁,避免过大的锁范围,提高并发性。
-
避免长时间持有锁: 尽量减小在Synchronized块中的执行时间,避免长时间持有锁。
1.9. 可见性与内存语义:
-
Synchronized保证可见性: 进入Synchronized块会清空工作内存,从主内存中读取共享变量,退出Synchronized块时会将修改刷新回主内存,保证了可见性。
-
内存语义: Synchronized不仅保证互斥性,还具有一定的内存语义,保证了操作的有序性。
2. ReentrantLock锁:
ReentrantLock
是Java中的可重入锁,它提供了与synchronized
相似的同步机制,但相对于synchronized
,ReentrantLock
提供了更多的灵活性和控制。以下是ReentrantLock
锁的深入理解:
2.0.怎么实现的:
ReentrantLock
是Java中可重入锁的一种实现,它使用了底层的AbstractQueuedSynchronizer (AQS)
来实现锁的机制。AQS
是一个提供了基本的同步原语的抽象框架,它可以用来实现各种形式的同步器。AQS
维护了一个状态和一个等待队列。状态表示被同步的资源的状态,可以是任意的int
值。等待队列用来存放等待获取锁的线程
2.1. 可重入性:
ReentrantLock
是可重入锁,同一线程可以多次获得同一个锁而不会被阻塞。
2.2. 锁的状态:
ReentrantLock
的锁状态包含了持有锁的线程和重入次数。
2.3. 锁的基本用法:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void lockedMethod() {
lock.lock(); // 获取锁
try {
// 临界区域
// ...
} finally {
lock.unlock(); // 释放锁
}
}
}
-
ReentrantLock
的lock()
方法首先会调用Sync
内部类的lock
方法。在Sync
内部类中,lock
方法通过AQS
的acquire
方法来尝试获取锁。 -
ReentrantLock
的unlock()
方法则会调用Sync
内部类的unlock
方法,通过AQS
的release
方法来释放锁。
2.4. 公平锁与非公平锁:
ReentrantLock
有两个内部类:NonfairSync
和FairSync
,分别用于实现非公平锁和公平锁。默认情况下,ReentrantLock
创建的是非公平锁。- 公平锁是按照线程请求锁的顺序来分配的,而非公平锁是不考虑线程的等待时间,有可能导致某些线程一直获取不到锁。
2.5. Condition条件:
ReentrantLock
提供了Condition
接口的实现,通过newCondition()
方法创建。可以使用Condition
实现更灵活的线程通信,例如等待某个条件满足后再继续执行。
2.6. 锁的中断响应:
ReentrantLock
允许在等待锁的过程中响应中断,这与synchronized
不同,可以通过lockInterruptibly()
方法实现。
2.7. 锁的超时获取:
ReentrantLock
支持在尝试获取锁时设置超时时间,通过tryLock(long time, TimeUnit unit)
实现。
2.8. 锁的升级与降级:
ReentrantLock
允许锁的升级和降级。在持有锁的情况下,可以调用lock.newCondition()
创建一个新的Condition
,然后释放主锁,等待某个条件满足时再获取锁。
2.9. 锁的可见性和内存语义:
- 与
synchronized
一样,ReentrantLock
同样保证了锁的可见性和一定的内存语义。
2.10. 锁的性能优化:
ReentrantLock
相对于synchronized
在高并发的情况下性能更好。在JDK6之后,JVM对synchronized
进行了优化,但在某些场景下,ReentrantLock
的性能仍有优势。
3. ReadWriteLock读写锁:
ReadWriteLock
是Java中用于支持读写分离的锁,它允许多个线程同时读取共享资源,但在写操作时必须互斥。ReadWriteLock
的核心思想是提高并发性,对于读取操作,多个线程可以同时进行,而写入操作则需要独占锁。
以下是ReadWriteLock
的深入理解:
3.1. 读写锁接口:
ReadWriteLock
接口有两个核心方法:readLock()
用于获取读锁,writeLock()
用于获取写锁。
3.2. ReentrantReadWriteLock实现:
- Java提供了
ReentrantReadWriteLock
类来实现ReadWriteLock
接口,它是ReentrantLock
的扩展。
3.3. 读锁和写锁的关系:
- 多个线程可以同时持有读锁,但在写锁被持有时,所有的读锁和其他写锁都会被阻塞。
3.4. 读锁共享性:
- 读锁是共享的,多个线程可以同时获取读锁,以实现对共享资源的并发读取。
3.5. 写锁的独占性:
- 写锁是独占的,一旦有线程持有写锁,其他线程无法同时获取写锁或读锁。
3.6. Reentrant特性:
ReentrantReadWriteLock
是可重入的,同一个线程可以多次获取同一种锁。
3.7. 公平性和非公平性:
- 与
ReentrantLock
类似,ReentrantReadWriteLock
可以在构造函数中选择是公平锁还是非公平锁。
3.8. Condition的支持:
ReentrantReadWriteLock
提供了Condition
接口的实现,通过readLock.newCondition()
和writeLock.newCondition()
来创建读锁和写锁的条件对象。
3.9. 降级和升级:
- 读写锁支持锁的降级(从写锁降级为读锁)和升级(从读锁升级为写锁)操作,这使得在某些场景下可以更灵活地管理锁。
3.10. 性能考虑:
- 读写锁适用于读操作频繁、写操作相对较少的场景,能够提高系统的并发性。
4. StampedLock锁:
StampedLock
是Java 8引入的一种读写锁的实现,相比于传统的ReentrantReadWriteLock
,StampedLock
提供了更多的功能和更高的性能。主要特点包括乐观读、悲观读和写锁,并支持锁的升级和降级。以下是对StampedLock
的深入理解:
4.1. 三种锁模式:
-
悲观读锁(readLock): 类似于
ReentrantReadWriteLock
的读锁,当线程获取悲观读锁时,其他线程无法获取写锁。 -
写锁(writeLock): 与悲观读锁互斥,即悲观读锁和写锁不能同时存在。
-
乐观读锁(tryOptimisticRead): 是一种乐观的、不阻塞的读锁。在使用乐观读锁时,其他线程可能同时进行写操作,但使用乐观读锁的线程随时可能发现数据不一致而重新获取悲观读锁。
4.2. 戳记(stamp):
- 每次锁的状态变更都会导致一个戳记的生成。戳记是一个long类型的数值,可以用来判断锁的状态是否发生过变化。
4.3. 乐观读锁的使用:
-
通过
tryOptimisticRead()
获取乐观读锁,并返回一个戳记。此时,其他线程可能进行写操作,但戳记可以用来验证在当前线程获取戳记后,锁的状态是否发生了变化。 -
使用乐观读锁后,可以使用
validate(stamp)
方法来验证锁的状态是否发生了变化。如果验证通过,可以继续使用获取悲观读锁的方式来读取数据。
4.4. 锁的升级和降级:
StampedLock
允许在不释放锁的情况下,从乐观读锁升级为悲观读锁,或者从悲观读锁降级为乐观读锁。
4.5. 非阻塞算法:
StampedLock
采用非阻塞算法,大多数情况下不会阻塞线程。
4.6. 适用场景:
StampedLock
适用于读操作远远多于写操作的场景,因为悲观读锁和写锁之间是互斥的。
4.7. 性能优化:
- 在某些情况下,
StampedLock
的性能比ReentrantReadWriteLock
更好,尤其是在读多写少的情况下。
5. LockSupport锁:
LockSupport
是Java并发包中的工具类,提供了一种基于线程的阻塞和唤醒的机制。与Object
类的wait()
和notify()
方法相比,LockSupport
更灵活,可以在线程之间传递许可,而不需要依赖对象的监视器。以下是对LockSupport
的深入理解:
5.1. 阻塞和唤醒:
LockSupport
提供了park()
和unpark(Thread thread)
方法,用于阻塞当前线程和唤醒指定线程。
5.2. 许可的获取和释放:
- 每个线程都有一个许可(permit)与之关联,许可的初始状态是可用的。
park()
方法会消耗许可,而unpark(Thread thread)
方法会释放许可。
5.3. park和unpark的配对使用:
park()
和unpark(Thread thread)
方法是成对使用的,即park()
调用的线程被阻塞后,只有unpark(Thread thread)
方法才能唤醒它。
5.4. 线程中断:
- 当线程被
park()
阻塞时,如果线程被中断,park()
方法会返回,并且不会消耗许可。可以通过Thread.interrupted()
来判断线程是否被中断。
5.5. 阻塞和唤醒顺序:
park()
和unpark(Thread thread)
的执行顺序可以是任意的,不一定是先阻塞后唤醒或先唤醒后阻塞。
5.6. 公平性:
LockSupport
不保证公平性,即不保证等待时间最长的线程优先获得许可。
5.7. park和interrupt:
- 当线程被中断后,
park()
方法不会抛出InterruptedException
,但会返回,此时可以通过Thread.interrupted()
来检查中断状态。
5.8. 实际应用:
LockSupport
通常用于实现其他同步工具类,如ReentrantLock
和AQS
中就用到了LockSupport
来阻塞和唤醒线程。
5.9. 性能优势:
- 相比于
Object
的wait()
和notify()
方法,LockSupport
的实现更加轻量,性能更好。
5.10. 底层实现:
LockSupport
的实现依赖于底层的Unsafe类,提供了一种基于CAS操作的无锁的线程阻塞和唤醒的机制。
这些锁机制在不同场景下有各自的优劣,选择合适的锁要根据具体的应用需求和性能要求。