当前位置: 代码迷 >> 综合 >> Java synchronized 和 ReentrantLock
  详细解决方案

Java synchronized 和 ReentrantLock

热度:99   发布时间:2023-12-25 00:33:57.0
  • Java String
  • Java Thread
  • Java ThreadPool
  • Java synchronized 和 ReentrantLock
  • Java 深克隆 和 浅克隆
  • Java Lock
  • Java HashMap

一、synchronizedReentrantLock 实现原理

  • 在 JDK-1.5 之前,共享对象的协调机制只有 synchronizedvolatile
  • 在 JDK-1.5 中增加了新的机制 ReentrantLock
  • 该机制的诞生并不是为了替代 synchronized,而是在 synchronized 不适用的情况下,提供一种可以选择的高级功能。

1. synchronized 悲观锁

  • synchronized 属于独占式 悲观锁
  1. 通过 JVM 隐式实现的,synchronized 只允许同一时刻只有一个线程操作资源。
  2. Java 中每个对象都隐式包含一个 monitor(监视器对象)
  3. 加锁的过程,其实就是竞争 monitor 的过程。
  1. 当线程进入字节码 monitorenter 指令之后,线程将持有 monitor 对象。
  2. 执行 monitorexit 时,释放 monitor 对象。
  3. 当其他线程没有拿到 monitor 对象时,则需要阻塞等待获取该对象。

2. ReentrantLock

  • ReentrantLockLock 的默认实现方式之一。
  1. 它是基于 AQS(Abstract Queued Synchronizer,队列同步器)实现的。
  2. 它默认是通过 非公平锁 实现的,在它的内部有一个 state 的状态字段,用于表示锁是否被占用。
  1. 如果是 0 则表示锁未被占用,此时线程就可以把 state 改为 1,并成功获得锁。
  2. 而其他未获得锁的线程只能去排队等待获取锁资源。
  • synchronizedReentrantLock 都提供了锁的功能。
  1. 具备 互斥性不可见性
  2. 在 JDK-1.5 中 synchronized 的性能远远低于 ReentrantLock
  3. 但在 JDK-1.6 之后,synchronized 的性能略低于 ReentrantLock

二、synchronizedReentrantLock 区别

  1. synchronizedJVM 隐式实现的,而 ReentrantLockJava 语言提供的 API。
  2. ReentrantLock 可设置为公平锁,而 synchronized 却不行。
  3. ReentrantLock 只能修饰代码块,而 synchronized 可以用于修饰方法、修饰代码块等。
  4. ReentrantLock 需要手动加锁和释放锁,如果忘记释放锁,则会造成资源被永久占用,而 synchronized 无需手动释放锁
  5. ReentrantLock 可以知道是否成功获得了锁,而 synchronized 却不行。

三、知识扩展


1. ReentrantLock 源码

  • 无参的构造函数,创建了一个非公平锁。
  • 用户也可以根据第二个构造函数,设置一个 boolean 类型的值,来决定是否使用公平锁来实现线程的调度。
private final Sync sync;
// 可重入锁
public ReentrantLock() {
    sync = new NonfairSync();// 非公平锁
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

1.1 公平锁 VS 非公平锁

  • 公平锁 的含义是线程需要按照请求的顺序来获得锁。
  • 非公平锁 则允许"插队"的情况存在。
  1. 所谓的 插队 指的是,线程在发送请求的同时,该锁的状态恰好变成了可用。
  2. 那么此线程就可以跳过队列中所有排队的线程直接拥有该锁。
  • 公平锁 由于有挂起和恢复所以存在一定的开销。
  • 因此性能不如 非公平锁
  1. 所以 ReentrantLocksynchronized 默认都是非公平锁的实现方式。

  • ReentrantLock 是通过 lock() 来获取锁,并通过 unlock() 释放锁。

2. lock() 获取锁

  • ReentrantLock 中的 lock() 是通过 sync.lock() 实现的。
  • Sync 类中的 lock() 是一个抽象方法,需要子类 NonfairSyncFairSync 去实现。
@Test
public void test2() {
    Lock lock = new ReentrantLock();try {
    lock.lock();// 加锁// ......业务处理} finally {
    lock.unlock();// 释放锁}
}

2.1 NonfairSync.lock() 非公平锁源码

/*** 执行锁定。尝试立即进行驳船,并在出现故障时备份到正常状态。*/
final void lock() {
    if (compareAndSetState(0, 1))// 将当前线程设置为此锁的持有者setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}

2.2 FairSync.lock() 公平锁源码

final void lock() {
    acquire(1);
}
  • 可以看出 非公平锁公平锁 只是多了一行 compareAndSetState() 方法。
  1. 该方法是尝试将 state 值由 0 置换为 1。
  2. 如果设置成功的话,则说明当前没有其他线程持有该锁,不用再去排队了,可直接占用该锁。
  3. 否则,则需要通过 acquire() 方法去排队。

2.3 acquire() 排队获取锁

// AbstractQueuedSynchronizer
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}

2.4 NonfairSync.tryAcquire() 非公平锁尝试获取锁

  • tryAcquire() 方法尝试获取锁。
  1. 如果获取锁失败,则把它加入到阻塞队列中。
// NonfairSync
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

2.5 FairSync.tryAcquire() 公平锁尝试获取锁

// FairSync
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();int c = getState();if (c == 0) {
    // 公平锁比非公平锁多了一行代码 `!hasQueuedPredecessors()`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);// set state=state+1return true;}return false;
}
  • 对于此方法来说,公平锁非公平锁 只多一行代码 !hasQueuedPredecessors()
  1. 它用来查看队列中,是否有比它等待时间更久的线程。
  2. 如果没有,就尝试一下是否能获取到锁。
  3. 如果获取成功,则标记为已经被占用。
  4. 如果获取锁失败,则调用 addWaiter() 方法,把线程包装成 Node 对象,同时放入到队列中。
  5. addWaiter 方法并不会尝试获取锁,acquireQueued() 方法才会尝试获取锁。
  6. 如果获取失败,则此节点会被挂起。

2.6 acquireQueued() 源码

/*** AbstractQueuedSynchronizer* 队列中的线程尝试获取锁,失败则会被挂起*/
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;// 获取锁是否成功的状态标识try {
    boolean interrupted = false;// 线程是否被中断for (;;) {
    // 获取前一个节点(前驱节点)final Node p = node.predecessor();// 当前节点为头节点的下一个节点时,有权尝试获取锁if (p == head && tryAcquire(arg)) {
    setHead(node);// 获取成功,将当前节点设置为 head 节点p.next = null; // help GC // 原 head 节点出队,等待被 GCfailed = false;// 获取成功return interrupted;}// 判断获取锁失败后是否可以挂起if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())// 线程若被中断,返回 trueinterrupted = true;}} finally {
    if (failed)cancelAcquire(node);}
}
  • 该方法会使用 for(;;) 无限循环的方式来尝试获取锁。
  1. 若获取失败,则调用 shouldParkAfterFailedAcquire() 方法,尝试挂起当前线程。

2.7 shouldParkAfterFailedAcquire() 源码

*** AbstractOwnableSynchronizer* 判断线程是否可以被挂起*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获得前驱节点的状态int ws = pred.waitStatus;// 前驱节点的状态为 SIGNAL,当前线程可以被挂起(阻塞)if (ws == Node.SIGNAL)/** This node has already set status asking a release* to signal it, so it can safely park.*/return true;if (ws > 0) {
    /** Predecessor was cancelled. Skip over predecessors and* indicate retry.*/do {
    // 若前驱节点状态为 CANCELLED,那就一直往前找,直到找到一个正常等待的状态为止node.prev = pred = pred.prev;} while (pred.waitStatus > 0);// 并将当前节点排在它后边pred.next = node;} else {
    /** waitStatus must be 0 or PROPAGATE. Indicate that we* need a signal, but don't park yet. Caller will need to* retry to make sure it cannot acquire before parking.*/// 把前驱节点的状态修改为 SIGNALcompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}
  • 线程入列被挂起的前提条件是。
  1. 前驱节点 的状态为 SIGNAL
  1. SIGNAL 状态的含义是 后继节点 处于 等待 状态,当前节点释放锁后将会唤醒 后继节点
  1. 所以在上面这段代码中,会先判断 前驱节点 的状态。
  1. 如果为 SIGNAL,则当前线程可以被挂起并返回 true
  2. 如果前驱节点 的状态 > 0,则表示 前驱节点 取消了,这时候需要一直往前找,直到找到最近一个正常等待的 前驱节点
  3. 然后把它作为自己的 前驱节点;如果前驱节点正常(未取消),则修改 前驱节点 状态为 SIGNAL
  1. 到这里整个加锁的流程就已经走完了。
  2. 最后的情况是,没有拿到锁的线程会在队列中被挂起,直到拥有锁的线程释放锁之后,才会去唤醒其他的线程去获取锁资源。
  • 整个运行流程如下图所示:
    在这里插入图片描述

3. unlock() 释放锁

// ReentrantLock
public void unlock() {
    sync.release(1);
}
// AbstractQueuedSynchronizer
public final boolean release(int arg) {
    // 尝试释放锁if (tryRelease(arg)) {
    // 释放成功Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;
}
  • 锁的释放流程为:
  1. 先调用 tryRelease() 方法尝试释放锁。
  2. 如果释放成功,则查看头结点的状态是否为 SIGNAL
  3. 如果是,则唤醒头结点的下个节点关联的线程。
  4. 如果释放锁失败,则返回 false

3.1 tryRelease() 源码

/*** ReentrantLock* 尝试释放当前线程占有的锁*/
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;// 释放锁后的状态,0 表示释放锁成功// 如果拥有锁的线程不是当前线程的话抛出异常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {
    // 锁被成功释放free = true;setExclusiveOwnerThread(null);// 清空独占线程}setState(c);// 更新 state 值,0 表示为释放锁成功return free;
}
  • tryRelease() 方法中。
  1. 会先判断当前的线程是不是占用锁的线程。
  1. 如果不是的话,则会抛出异常。
  2. 如果是的话,则先计算锁的状态值 getState() - releases 是否为 0。
  3. 如果为 0,则表示可以正常的释放锁,然后清空独占的线程,最后会更新锁的状态并返回执行结果。

4. JDK-1.6 锁优化


4.1 自适应自旋锁

  • JDK-1.5 在升级为 JDK-1.6 时,HotSpot 虚拟机团队在锁的优化上下了很大功夫。
  • 比如实现了 自适应式自旋锁锁升级 等。
  • JDK-1.6 引入了自适应式自旋锁,意味着自旋的时间不再是固定的时间了。
  1. 比如在同一个锁对象上,如果通过自旋等待成功获取了锁。
  2. 那么虚拟机就会认为,它下一次很有可能也会成功(通过自旋获取到锁)
  3. 因此允许自旋等待的时间会相对的比较长。
  4. 而当某个锁通过自旋很少成功获得过锁,那么以后在获取该锁时,可能会直接忽略掉自旋的过程,以避免浪费 CPU 的资源,这就是 自适应自旋锁 的功能。

4.2 锁升级

  • 锁升级 其实就是从 偏向锁轻量级锁 再到 重量级锁 升级的过程。
  • 这是 JDK-1.6 提供的优化功能,也称之为 锁膨胀

4.3 偏向锁

  • 偏向锁 是指在无竞争的情况下设置的一种锁状态。
  • 偏向锁 的意思是,它会偏向于第一个获取它的线程。
  1. 当锁对象第一次被获取到之后,会在此对象头中设置标示为 “01”,表示偏向锁的模式,并且在对象头中记录此线程的 ID。
  2. 这种情况下,如果是持有偏向锁的线程每次在进入的话,不再进行任何同步操作。
  3. LockingUnlocking 等,直到另一个线程尝试获取此锁的时候,偏向锁模式才会结束。
  • 偏向锁 可以提高带有同步,但无竞争的程序性能。
  1. 但如果在多数锁总会被不同的线程访问时,偏向锁模式就比较多余了。
  2. 此时可以通过 -XX:-UseBiasedLocking禁用偏向锁 以提高性能。

4.4 轻量锁 和 重量锁

  • 轻量锁 是相对于 重量锁 而言的。
  1. 在 JDK-1.6 之前,synchronized 是通过操作系统的互斥量(mutex lock)来实现的。
  2. 这种实现方式,需要在用户态和核心态之间做转换,有很大的性能消耗。
  3. 这种传统实现锁的方式被称之为 重量锁
  • 轻量锁 是通过比较并交换(CAS,Compare and Swap)来实现的。
  1. 它对比的是线程和对象的 Mark Word(对象头中的一个区域)
  1. 如果更新成功,则表示当前线程成功拥有此锁。
  2. 如果失败,虚拟机会先检查对象的 Mark Word 是否指向当前线程的栈帧。
  3. 如果是,则说明当前线程已经拥有此锁,否则,则说明此锁已经被其他线程占用了。
  • 当两个以上的线程争抢此锁时,轻量级锁 就膨胀为 重量级锁,这就是 锁升级 的过程,也是 JDK 1.6 锁优化的内容。

四、小结

  • 首先是 synchronizedReentrantLock 的实现过程。
  • 然后是 synchronizedReentrantLock 的区别。
  • 最后通过源码的方式,看了 ReentrantLock 加锁和解锁的执行流程。
  • 接着又看了 JDK-1.6 中的锁优化,包括自适应式自旋锁的实现过程,以及 synchronized 的三种锁状态和锁升级的执行流程。
  • synchronized 刚开始为偏向锁,随着锁竞争越来越激烈,会升级为轻量级锁和重量级锁。
  • 如果大多数锁被不同的线程所争抢就不建议使用偏向锁了。

1. ReentrantLock 的具体实现细节是什么?

2. JDK-1.6 时锁做了哪些优化?

  相关解决方案