跳转至

Lock

好的资料 https://generalthink.github.io/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/

mutex 和monitor的区别

语义不同:

Mutex:Mutex 是一种简单的同步原语,它提供了两种操作:lock(获取锁)和 unlock(释放锁)。当一个线程获得了 Mutex 后,其他线程必须等待该线程释放锁后才能获得锁。Mutex 并不提供其他附加的语义,如条件变量等。 Monitor:Monitor 是一种高级同步机制,它是一种包含了锁、条件变量和一些操作的抽象。除了提供类似 Mutex 的 lock 和 unlock 操作外,Monitor 还提供了等待和通知(wait 和 notify)机制,允许线程在某些条件下等待和唤醒其他线程。 关联性:

Mutex:Mutex 可以与任何数据结构或代码段一起使用,因为它是一种通用的同步原语。 Monitor:Monitor 通常与特定的类或对象关联,它提供了对这些对象的同步访问和控制。在 Java 中,每个对象都有一个关联的监视器,可以使用 synchronized 关键字来获取对象的监视器锁。 灵活性:

Mutex:Mutex 是一种低级同步原语,相对较为灵活,可以根据需要在任何地方使用。 Monitor:Monitor 是一种高级抽象,提供了更方便的等待和通知机制,但其使用受到限制,通常与特定的类或对象关联。 总的来说,Mutex 是一种简单的同步原语,而 Monitor 则是一种更高级的抽象,提供了更多的同步语义和操作,但其使用通常受到限制,适用于特定的类或对象。 Monitor必须和锁一起用 不然会抛出异常 Thread.currency.wait()没有使用场景直接是报错的 Thread.currency不能作为Monitor 每个线程中其实例都不同 Motitor只释放和自己绑定的锁

ReentrantLock唤醒机制

ReentrantLock 在被unlock 的时候会唤醒所有的等待的线程还是唤醒一个 唤醒的规则是怎么样的

ChatGPT ReentrantLock 类的 unlock() 方法会释放锁,并唤醒等待该锁的线程。但是,它并不会像 synchronized 块那样自动唤醒所有等待的线程。具体唤醒的规则是:

公平性设置:如果 ReentrantLock 是公平锁(通过构造函数设置为公平锁),则等待时间最长的线程将会被唤醒,保证线程获取锁的顺序与其等待的顺序一致,实现公平性。

非公平性设置:如果 ReentrantLock 是非公平锁(默认情况下是非公平锁),则等待锁的所有线程中的任何一个都有机会被唤醒,这取决于操作系统的调度和 JVM 的实现。通常情况下,JVM 偏向于唤醒等待时间较长的线程,但并不保证按照等待顺序唤醒。

总的来说,唤醒哪个线程是不确定的,这取决于 JVM 的实现和当前系统的状态。公平性设置影响了唤醒的顺序,但并不是绝对的,而是一个倾向性的特性。

interrupt()方法 InterruptedException

在Java中,interrupt() 方法是用来中断线程的。当你调用一个线程的 interrupt() 方法时,会设置该线程的中断状态为"中断"。但是,这并不意味着线程会立即停止执行。实际上,线程会继续执行,直到它检查到自己的中断状态,并且选择处理中断或者直接退出。

interrupt() 方法的行为会根据线程的不同状态而有所不同: * 程序如果处于NEW 状态 那么调用interrupt() 方法相当于identity()

  • 如果线程处于运行状态(即正在执行任务),那么调用 interrupt() 方法会将线程的中断状态设置为"中断"。但线程不会立即停止执行,而是会继续运行下去,也不会报错。

  • 如果线程处于阻塞状态(例如,调用了 Object.wait()、Thread.sleep()、Thread.join() 等等),那么调用 interrupt() 方法会中断线程的阻塞状态,并抛出 InterruptedException 异常。这时你可以在异常处理中选择继续执行(进入catch段后线程状态设为RUNNIG,线程也进入RUNNING,继续执行catch代码段后的代码)或者退出线程(),没有处理直接抛的话直接结束。

如果线程处于非阻塞、非等待状态,但是检测到自己的中断状态为"中断",那么线程可以选择在适当的时候终止执行,通常通过检查 Thread.currentThread().isInterrupted() 方法来判断中断状态并进行相应的处理。

公平锁和非公平锁

线程切换的开销,其实就是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。(一个线程A释放锁,刚好有另一个线程B来请求;这种情况下,非公平锁:B直接获取锁 公平锁:阻塞B加入队尾,唤醒队首线程来获取锁)

自旋锁

线程协调中,自旋等待通常用于短暂等待,或者在预期等待时间较短且并发负载较轻的情况下。在长时间等待或者高并发负载下,更适合使用线程阻塞等待的方式,以避免资源浪费和性能下降。

CSA乐观锁

用在并发少的情况 * CAS本身是不会并发的,本身是原子的且会锁内存 * 先比较再修改 不一致会返回false 发生冲突 回滚操作或记录日志等 * ABA问题用版本号来两次CAS 每次修改版本号都加1 先执行版本号的CAS再执行值的CAS,只有两次CAS都通过可以修改 减少了锁的获取和释放的开销,以及由于锁竞争导致的线程上下文切换的开销 一些阻塞是不必要的 即虽然锁互斥但是读写实际执行特定过程没有发生读写的并发错误 如读进程已经准备释放锁了但还没执行,这时候写进程来获取锁 再比如读进程互斥区再执行与变量无关的操作等等

  • 乐观锁一般搭配while使用 如 `````` //读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。 public int incrementAndGet(AtomicInteger var) { int prev, next; do { prev = var.get(); next = prev + 1; } while ( ! var.compareAndSet(prev, next)); return next; }

    ``````

AQS相关

AQS 核心概念及实现

AQS(AbstractQueuedSynchronizer)是 Java 中实现锁和同步器的基础框架,提供了独占锁和共享锁的管理机制。它的核心部分包括以下几个关键概念和方法:

AQS 核心概念

1. setExclusiveOwnerThread(Thread thread)
  • 这个方法用于设置当前持有锁的线程。在 独占模式 下(如 ReentrantLock),锁只能被一个线程持有,这个方法帮助确定哪个线程持有锁。
  • 对于 共享锁模式(如 SemaphoreReentrantReadWriteLock 的读锁),不需要调用 setExclusiveOwnerThread,因为共享锁可以被多个线程持有,且不需要记录哪个线程持有锁。
2. compareAndSetState(int expectedState, int newState)
  • 这是 AQS 的一个核心方法,用于原子地更新同步器的状态。它使用 CAS(Compare-And-Swap) 来确保状态的更新是线程安全的。
  • 在锁的实现中,state 通常表示锁的资源状态(如可用资源的数量,或锁是否被占用)。通过 compareAndSetState,可以在不使用传统锁的情况下实现高效的并发控制。
3. 模板方法

AQS 提供了以下四个模板方法,这些方法定义了锁和同步器的核心行为,但没有实现具体的逻辑。实现类需要根据具体的需求来实现这些方法: - tryAcquire(int arg):尝试获取锁(独占模式)。 - tryRelease(int arg):尝试释放锁(独占模式)。 - tryAcquireShared(int arg):尝试获取共享锁。 - tryReleaseShared(int arg):尝试释放共享锁。

这些方法的实现取决于具体的同步器(如 ReentrantLockSemaphore 等),而 AQS 通过这些模板方法提供了一套通用的框架来管理锁的获取、释放和线程的排队。


独占模式和共享模式

1. 独占模式(Exclusive Mode)
  • 独占模式 下(如 ReentrantLock),只有一个线程可以持有锁。当一个线程获取锁后,其他线程必须等待锁被释放才能获取。独占模式通过 tryAcquire(int arg)tryRelease(int arg) 方法来控制锁的获取和释放。
2. 共享模式(Shared Mode)
  • 共享模式 下(如 SemaphoreReentrantReadWriteLock 的读锁),多个线程可以共享访问资源。当一个线程释放资源时,其他线程可以获取资源。共享模式通过 tryAcquireShared(int arg)tryReleaseShared(int arg) 方法来控制锁的获取和释放。

示例代码

ReentrantLock 使用独占模式

```java import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class ReentrantLockExample { private static class Sync extends AbstractQueuedSynchronizer { // 尝试获取锁(独占模式) @Override protected boolean tryAcquire(int acquires) { if (compareAndSetState(0, 1)) { // 如果状态是0(未被占用),则获取锁 setExclusiveOwnerThread(Thread.currentThread()); // 设置当前线程为锁持有者 return true; } return false; }

    // 尝试释放锁(独占模式)
    @Override
    protected boolean tryRelease(int releases) {
        if (Thread.currentThread() != getExclusiveOwnerThread()) {
            throw new IllegalMonitorStateException();  // 非持有锁的线程不能释放锁
        }
        setExclusiveOwnerThread(null);  // 释放锁
        setState(0);  // 设置状态为0(未占用)
        return true;
    }

    // 判断当前线程是否持有锁
    @Override
    protected boolean isHeldExclusively() {
        return getState() != 0;
    }
}

private final Sync sync = new Sync();

public void lock() {
    sync.acquire(1);
}

public void unlock() {
    sync.release(1);
}

}

三类锁

锁的优化机制

偏向锁(Biased Locking)

  • 适用场景: 无竞争的情况下,只有一个线程进入临界区。
  • 特点:
  • 偏向锁会“偏向”第一个访问锁的线程,后续无需同步操作,性能最优。
  • 通过记录线程ID来避免不必要的同步开销。
  • 潜在问题:
  • 当有其他线程尝试获取锁时,偏向锁会撤销并升级为轻量级锁。
  • 频繁的撤销和升级操作会导致性能损耗(STW,Stop-The-World)。

轻量级锁(Lightweight Locking)

  • 适用场景: 多个线程可以交替进入临界区,竞争程度较低。
  • 特点:
  • 使用CAS(Compare-And-Swap)自旋机制,尝试通过忙等待(Busy-Waiting)获取锁。
  • 避免直接进入内核态,减少系统调用开销。
  • 潜在问题:
  • 自旋操作会消耗CPU资源,尤其是在高竞争场景下。
  • 如果自旋时间过长,会升级为重量级锁。

重量级锁(Heavyweight Locking)

  • 适用场景: 多线程同时进入临界区,竞争程度较高。
  • 特点:
  • 依赖于操作系统的互斥量(Mutex Lock)来实现同步。
  • 涉及系统调用和内核态切换,确保线程阻塞和唤醒的正确性。
  • 潜在问题:
  • 系统调用和内核态切换会带来较大的性能开销。
  • 适用于高竞争场景,但性能较差。

总结

  • 偏向锁: 在无竞争时性能最佳,但有竞争时升级开销较大。
  • 轻量级锁: 适用于低竞争场景,自旋消耗CPU,但避免了内核态切换。
  • 重量级锁: 适用于高竞争场景,系统调用开销大,但能确保线程安全。

选择合适的锁机制需要根据具体的竞争场景和性能需求进行权衡。

参考文章

回到页面顶部