目录
在Java中,JUC(Java Util Concurrent)是一组用于实现多线程应用程序的实用工具类。JUC提供了许多线程安全的数据结构和工具类,其中包括锁。
一、根据不同维度,分类一:
1. 按照锁的粒度分
-
细粒度锁
细粒度锁是指锁定的范围非常小,只锁定共享资源的一小部分。这种锁可以减少线程之间的竞争,从而提高并发性能。Java中的ReentrantLock就是一种细粒度锁。
-
粗粒度锁
粗粒度锁是指锁定的范围比较大,锁住了整个共享资源。这种锁会导致线程之间的竞争增加,从而降低并发性能。Java中的synchronized就是一种粗粒度锁。
2. 按照锁的共享方式分
-
共享锁
共享锁是指多个线程可以同时获取该锁,用于读取共享资源。Java中的ReentrantReadWriteLock就是一种支持共享锁的锁。
-
排他锁
排他锁是指只有一个线程可以获取该锁,用于修改共享资源。Java中的synchronized和ReentrantLock都是一种排他锁。
3. 按照锁的实现方式分
-
自旋锁
自旋锁是指当获取锁失败时,线程不会进入阻塞状态,而是在一个循环中不断尝试获取锁。Java中的AtomicInteger就是一种自旋锁。
-
阻塞锁
阻塞锁是指当获取锁失败时,线程会进入阻塞状态等待锁释放。Java中的synchronized和ReentrantLock都是一种阻塞锁。
二、根据不同维度,分类二:
1. 悲观锁和乐观锁
悲观锁是一种独占锁,在对共享资源进行操作时,悲观锁会认为其他线程会对该资源进行修改,因此会将该资源锁定,直到操作完成后才会释放锁。乐观锁则是一种非独占锁,在对共享资源进行操作时,乐观锁会认为其他线程不会对该资源进行修改,因此不会对该资源进行加锁,而是在操作完成后进行数据版本的比较,如果版本一致则操作成功,否则需要进行重试。
// 悲观锁
synchronized (obj) {
// 对共享资源进行操作
}
// 乐观锁
AtomicInteger value = new AtomicInteger(0);
value.compareAndSet(0, 1);
2. 可重入锁和非可重入锁
可重入锁是一种支持重复加锁的锁,即同一个线程可以多次获取同一个锁,而不会被锁死。非可重入锁则是一种不支持重复加锁的锁,如果同一个线程多次获取同一个锁,则会被锁死。
// 可重入锁
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 对共享资源进行操作
} finally {
lock.unlock();
}
// 非可重入锁
Semaphore semaphore = new Semaphore(1);
semaphore.acquire();
try {
// 对共享资源进行操作
} finally {
semaphore.release();
}
3. 公平锁和非公平锁
公平锁是一种按照线程等待的时间来获取锁的锁,即等待时间最长的线程会最先获取锁。非公平锁则是一种不按照线程等待时间来获取锁的锁,即线程可以在任何时刻获取锁,不考虑等待时间。
// 公平锁
ReentrantLock lock = new ReentrantLock(true);
lock.lock();
try {
// 对共享资源进行操作
} finally {
lock.unlock();
}
// 非公平锁
ReentrantLock lock = new ReentrantLock(false);
lock.lock();
try {
// 对共享资源进行操作
} finally {
lock.unlock();
}
4. 独占锁和共享锁
独占锁是一种只能被一个线程获取的锁,可以用于保护共享资源的独占访问。共享锁则是可以被多个线程同时获取的锁,可以用于保护共享资源的共享访问。
// 独占锁
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.writeLock().lock();
try {
// 对共享资源进行独占访问的操作
} finally {
lock.writeLock().unlock();
}
// 共享锁
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
try {
// 对共享资源进行共享访问的操作
} finally {
lock.readLock().unlock();
}
三、根据不同维度,分类三:
1. 偏向锁(Biased Locking)
-
偏向锁是一种针对单线程执行的优化手段,当一个线程获取了锁后,会在对象头上记录这个线程ID,并设置偏向标志位。当这个线程再次获取同一把锁时,就不需要再次竞争,直接获得锁。只有当其他线程尝试获取锁时,才会撤销偏向锁。偏向锁的目的是减少无竞争情况下的锁操作,提高程序性能。可以使用
-XX:+UseBiasedLocking
参数打开偏向锁机制。
2. 轻量级锁(Lightweight Locking)
- 轻量级锁是针对多个线程交替执行的情况进行的优化。当一个线程获取锁时,会在对象头上记录锁的标志位和线程ID,然后通过CAS操作尝试将对象头中的Mark Word替换为指向线程栈中锁记录的指针。如果操作成功,当前线程就获得了锁。如果CAS操作失败,表示有其他线程竞争锁,当前线程就进入自旋等待,尝试获取锁。当自旋次数超过阈值或者其他线程成功获取了锁,当前线程就会升级为重量级锁。轻量级锁的目的是减少多个线程竞争同一把锁时的锁操作,提高程序性能。
3. 重量级锁(Heavyweight Locking)
- 重量级锁是针对多个线程竞争同一把锁时进行的优化。当一个线程获取锁时,会进入阻塞状态,此时操作系统会将线程挂起,直到其他线程释放锁。重量级锁的目的是确保同一时刻只有一个线程能够访问共享资源,保证数据的正确性。
4. 自旋锁(Spin Locking)
- 自旋锁是一种特殊的锁,当一个线程获取锁时,如果发现其他线程正在使用锁,就会进入自旋等待,不会阻塞线程。当其他线程释放锁时,当前线程就可以立即获取锁,避免了线程的阻塞和唤醒开销。自旋锁的目的是减少线程阻塞和唤醒的开销,提高程序性能。
四、锁源码详解
一、synchronized锁 实现原理、源码详解
synchronized
是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质上又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要由用户态转换到核心态,这个成本非常高,状态之间的切换需要相对比较长的时间,这就是为什么使用 synchronized 会导致线程阻塞的原因。
A.synchronized
监视器的详细过程:
- 在进入同步代码块之前,线程会尝试获取锁。如果锁没有被其他线程持有,则这个线程获取锁并进入同步代码块;否则,这个线程就被阻塞,直到锁被其他线程释放为止。
- 当线程获取到锁并进入同步代码块时,会在对象头中设置锁的标记位,表示这个对象被锁定了。
- 当线程执行完同步代码块时,会释放锁,并清除对象头中的锁标记位,表示这个对象已经被释放了。
- 当线程被阻塞时,它会进入对象的等待队列中,等待被唤醒。当锁被释放时,等待队列中的线程会被唤醒,并竞争锁。
synchronized
的实现涉及到 Java 对象头的结构,具体可以参考 JDK 源码中的 Object
类的头文件定义:
public class Object {
private static native void registerNatives();
static {
registerNatives();
}
/**
* The synchronization status of this object.
*/
private transient volatile int syncStatus;
...
}
其中的 syncStatus
就是用来存储锁状态的字段。在 HotSpot JVM 中,对象头的结构如下:
|--------------|--------------|--------------|--------------|
| Mark Word | Class | Array | Padding |
|--------------|--------------|--------------|--------------|
其中的 Mark Word
中就包含了锁的标记位信息。具体结构可以参考下面的图示:
|--------------------------|--------------------------|
| unused:25-31 | identity_hashcode:1 |
|--------------------------|--------------------------|
| unused:1-3 | age:4 | biased_lock:1 |
|--------------------------|--------------------------|
| lock:2 | GC_state:2 |
|--------------------------|--------------------------|
| ptr_to_lock_record:32 | ptr_to_heavyweight_mon:32|
|--------------------------|--------------------------|
其中的 biased_lock
就是用来表示偏向锁状态的标记位,在没有竞争的情况下可以避免互斥操作从而提高性能。当有其他线程尝试获取锁时,偏向锁就会升级为轻量级锁或重量级锁。
B. 底层的操作系统的 Mutex Lock实现原理
synchronized
是 Java 中最基本的互斥同步手段,它的实现基于对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质上又是依赖于底层的操作系统的 Mutex Lock 来实现的。具体过程如下
1. 在编译阶段,Java 编译器会在代码中的每个同步块(synchronized
块)的前后分别插入 monitorenter
和 monitorexit
指令,这两个指令都需要一个 reference 类型的参数来指明要锁定和解锁的对象。
synchronized (obj) {
// 同步块内容
}
编译后的字节码如下:
// monitorenter 指令,获取锁
// 进入 synchronized 块之前
monitorenter obj
// 同步块内容
// ...
// monitorexit 指令,释放锁
// 离开 synchronized 块之后
monitorexit obj
2. monitorenter
和 monitorexit
指令需要依赖于底层操作系统的 Mutex Lock 来实现对对象的加锁和解锁。在 Java 虚拟机实现中,每个对象都与一个监视器(monitor)关联,当一个线程试图获取对象的监视器时,该线程会进入到对象的监视器队列中等待,直到获取到该对象的监视器。获取到监视器的线程可以执行同步块中的代码,执行完毕后,该线程会释放该对象的监视器,以便其他等待线程可以获取该监视器并执行同步块中的代码。
3. synchronized
的实现还涉及到锁的升级和降级。在 Java 6 及之前的版本中,synchronized
的实现是基于重量级锁(也称为互斥量)来实现的,这种锁的获取和释放都需要进行用户态和内核态之间的切换,因此效率比较低。从 Java 6 开始,synchronized
的实现引入了锁的升级和降级机制,具体来说,当一个线程获取锁时,synchronized
会先尝试使用偏向锁(适用于只有一个线程访问对象的情况),如果偏向锁获取失败,则尝试使用轻量级锁(适用于有多个线程交替访问对象的情况),最后才会使用重量级锁。在锁的释放过程中,synchronized
也会根据情况将锁从重量级锁降级为轻量级锁或偏向锁,以提高程序的执行效率。
二、ReentrantLock锁 实现原理、源码详解
ReentrantLock是Java中的一个独占锁,可以用于实现线程同步。在Java中,synchronized关键字也可以用于线程同步,但是synchronized关键字有很多限制,比如无法中断、无法尝试获取锁等,而ReentrantLock则可以解决这些限制。
A. ReentrantLock实现原理:
ReentrantLock是通过Java中的AQS(AbstractQueuedSynchronizer)实现的,AQS是一个抽象类,提供了一个框架,可以用于实现同步器。ReentrantLock就是通过继承AQS实现的。
ReentrantLock是Java中的一种可重入互斥锁,它具有与synchronized关键字相同的基本行为和语义,但提供了更多的灵活性和可扩展性。ReentrantLock实现了Lock接口,它提供了一组丰富的特性,例如锁投票、定时锁等待和可中断锁等待。与synchronized关键字相比,ReentrantLock提供了更丰富的功能,但使用起来也更复杂一些。
ReentrantLock实现原理的核心是AQS(AbstractQueuedSynchronizer),它是实现锁和相关同步器的关键类。AQS使用一个FIFO队列来管理线程,当线程请求锁的时候,如果锁已经被其他线程占用了,那么该线程就会被加入到等待队列中,等待锁释放。当锁释放的时候,AQS会从等待队列中取出第一个线程,并让它获取到锁。
ReentrantLock内部维护了一个state变量,表示锁的状态。当state为0时,表示锁是未锁定状态,当state大于0时,表示锁已经被某个线程占用了。当一个线程请求锁的时候,如果state为0,那么该线程就可以获取到锁,并将state设置为1;如果state大于0,那么该线程就会被加入到等待队列中,等待锁释放。当线程释放锁的时候,它会将state设置为0,并从等待队列中唤醒一个等待线程。
ReentrantLock中有两种锁,分别是公平锁和非公平锁。公平锁是指线程获取锁的顺序与它们加入等待队列的顺序相同,而非公平锁则不保证这个顺序。在ReentrantLock中,默认使用非公平锁。
B. ReentrantLock源码详解
ReentrantLock中有一个Sync类,它继承了AQS类,并实现了lock、tryAcquire和tryRelease等方法。Sync类是ReentrantLock的内部类,它有两个子类,分别是NonfairSync和FairSync。NonfairSync是非公平锁的实现,FairSync是公平锁的实现。
在ReentrantLock的构造函数中,我们可以选择使用公平锁还是非公平锁。如果我们不传入参数,默认使用非公平锁。
ReentrantLock中的lock方法,就是调用Sync类中的lock方法,它会先尝试获取锁,如果获取失败,就会加入到等待队列中,并被阻塞,直到锁被释放。在Sync类的lock方法中,会调用AQS类的acquire方法。
ReentrantLock中的tryLock方法,就是调用Sync类中的nonfairTryAcquire方法,它会尝试获取锁,如果获取成功,返回true,否则返回false。在Sync类的nonfairTryAcquire方法中,会先判断当前锁是否被占用,如果没有被占用,就尝试获取锁。如果当前锁已经被占用,就返回false。
ReentrantLock中的unlock方法,就是调用Sync类中的release方法,它会释放锁,并唤醒等待队列中的一个线程。在Sync类的release方法中,会调用AQS类的release方法。
ReentrantLock中的newCondition方法,就是调用Sync类中的newCondition方法,它会返回一个Condition对象,用于实现条件变量。在Sync类的newCondition方法中,会创建一个ConditionObject对象,它是AQS类的内部类,用于实现条件变量。
除此之外,ReentrantLock还提供了很多其他方法,比如isHeldByCurrentThread、getHoldCount、isLocked等,用于获取锁的状态信息。
下面是ReentrantLock的源码及其注释说明:
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
// AQS对象,用于实现锁和相关同步器
private final Sync sync;
// 构造函数,默认创建非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 构造函数,根据fair参数创建公平锁或非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 获取锁
public void lock() {
sync.lock();
}
// 获取锁,可中断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
// 尝试获取锁,不会阻塞
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
// 带超时时间的尝试获取锁
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
// 释放锁
public void unlock() {
sync.release(1);
}
// 返回Condition对象
public Condition newCondition() {
return sync.newCondition();
}
// 获取锁的当前持有者线程
public Thread getOwner() {
return sync.getOwner();
}
// 获取当前等待获取锁的线程数
public int getQueueLength() {
return sync.getQueueLength();
}
// 判断当前线程是否持有锁
public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();
}
// 判断锁是否被持有
public boolean isLocked() {
return sync.isLocked();
}
// 非公平锁实现类
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// 尝试获取锁(非公平)
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
// 公平锁实现类
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 尝试获取锁(公平)
final boolean fairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
// Sync是ReentrantLock的核心实现类,它继承了AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
// 获取锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// 尝试获取锁(公平)
abstract boolean fairTryAcquire(int acquires);
// 尝试获取锁(非公平)
abstract boolean nonfairTryAcquire(int acquires);
// 尝试释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
// 返回Condition对象
final ConditionObject newCondition() {
return new ConditionObject();
}
// 获取锁的当前持有者线程
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
// 获取当前等待获取锁的线程数
final int getQueueLength() {
return getQueueLength();
}
// 判断当前线程是否持有锁
final boolean isHeldExclusively() {
return Thread.currentThread() == getExclusiveOwnerThread();
}
}
}
ReentrantLock源码中,Sync是ReentrantLock的核心实现类,它继承了AQS。ReentrantLock分为公平锁和非公平锁两种实现方式,分别对应FairSync和NonfairSync两个内部类。在lock()方法中,如果state为0,那么就将state设置为1,并将当前线程设置为锁的持有者;否则就调用acquire()方法将当前线程加入到等待队列中,等待锁释放。在unlock()方法中,先判断当前线程是否持有锁,如果不是则抛出IllegalMonitorStateException异常;否则就将state减去releases,并将锁的持有者线程设置为null。在非公平锁实现类NonfairSync中,尝试获取锁的方法nonfairTryAcquire()会直接获取锁,而不会判断等待队列中是否有线程在等待。在公平锁实现类FairSync中,尝试获取锁的方法fairTryAcquire()会先判断等待队列中是否有线程在等待,如果有则不会获取锁,而是将当前线程加入到等待队列中。
五、如何使用【锁】事
1. ReentrantLock
:可重入锁,支持公平和非公平两种获取锁的方式。使用tryLock()
方法可以尝试获取锁,避免死锁。
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 获得锁后执行的代码
} finally {
lock.unlock();
}
2. ReentrantReadWriteLock
:可重入读写锁,支持多个线程同时读取,但只能有一个线程写入。读锁和写锁是互斥的。
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
try {
// 获得读锁后执行的代码
} finally {
lock.readLock().unlock();
}
lock.writeLock().lock();
try {
// 获得写锁后执行的代码
} finally {
lock.writeLock().unlock();
}
3. StampedLock
:乐观锁,用于读多写少的场景。读锁和写锁是互斥的,但使用tryOptimisticRead()
方法可以尝试获取一个乐观锁,避免阻塞。
StampedLock lock = new StampedLock();
long stamp = lock.readLock();
try {
// 获得读锁后执行的代码
} finally {
lock.unlock(stamp);
}
stamp = lock.writeLock();
try {
// 获得写锁后执行的代码
} finally {
lock.unlock(stamp);
}
4. Semaphore
:信号量,用于控制同时访问某个资源的线程数量。
Semaphore semaphore = new Semaphore(10); // 允许同时访问的线程数量为10
semaphore.acquire(); // 获取许可
try {
// 执行需要许可的代码
} finally {
semaphore.release(); // 释放许可
}
5. CountDownLatch
:倒计时门闩,用于等待多个线程完成某个任务。
CountDownLatch latch = new CountDownLatch(5); // 需要等待5个线程完成
for (int i = 0; i < 5; i++) {
new Thread(() -> {
// 执行任务
latch.countDown(); // 完成任务后调用countDown()方法
}).start();
}
latch.await(); // 等待所有线程完成任务
6. CyclicBarrier
:循环屏障,用于等待多个线程到达某个状态后再同时执行。
CyclicBarrier barrier = new CyclicBarrier(5); // 需要等待5个线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
// 执行任务
try {
barrier.await(); // 等待所有线程到达屏障
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
// 所有线程到达屏障后执行的代码
}).start();
}
六、总结
JUC(Java.util.concurrent)提供了多种锁机制,包括:
-
ReentrantLock:一个可重入的互斥锁,具有与 synchronized 相同的并发性和内存语义。与 synchronized 不同的是,它具有可中断锁获取、可轮询锁和定时锁等特性。
-
ReentrantReadWriteLock:一个可重入的读写锁,它允许多个线程同时读取共享数据,但对于写操作是互斥的。与 ReentrantLock 相比,它可以提高读操作的并发性能。
-
StampedLock:一个可重入的互斥锁,支持三种模式:写锁、悲观读锁和乐观读锁。与 ReentrantReadWriteLock 相比,它在读操作时不会阻塞写操作,提高了并发性能。
-
Condition:与 Lock 配合使用的条件变量,可以实现等待/通知模式。
-
Semaphore:一个计数信号量,可以用来限制同时访问某个共享资源的线程数。
-
CountDownLatch:一个同步工具类,允许一个或多个线程等待其他线程完成操作后再执行。
-
CyclicBarrier:一个同步工具类,允许多个线程相互等待,直到所有线程都达到某个屏障点后再同时执行。
-
Phaser:一个同步工具类,可以动态地控制线程的阶段,每个阶段完成后进行同步。
以上是 JUC 中常用的锁机制,不同的锁机制适用于不同的场景,开发人员应根据具体情况选择合适的锁机制。