首页 > 编程语言 >【Java并发】Lock锁详解

【Java并发】Lock锁详解

时间:2024-12-23 12:55:40浏览次数:5  
标签:Java 获取 stamp Lock 写锁 读锁 详解 线程 lock

目录

什么是Lock

Lock和synchronized的区别

Lock的实现类

ReentrantLock锁(Lock的默认实现)

基本使用

ReentrantLock的特点

ReentrantReadWriteLock锁(读写锁)

基本使用

1.初始化锁

2.读操作

3.写操作

ReentrantReadWriteLock的特点 

StampedLock锁(乐观读锁)

基本使用

1.初始化锁

2.写操作

3.悲观读操作

4.乐观读操作 

StampedLock锁的特点

锁模式

时间戳(Stamp)

性能优势

缺点


什么是Lock

Java并发包(java.util.concurrent,简称JUC)提供了比synchronized关键字更灵活、更强大的锁机制,其中Lock接口及其实现类是JUC中锁机制的核心。

Lock和synchronized的区别

  • 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;

  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;

  • synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放 锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;

  • 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1 阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以 不用一直等待就结束了;

  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)

  • Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

Lock的实现类

  • ReentrantLock:可重入锁,是最常用的Lock实现。它支持可重入性,即同一个线程可以多次获取同一把锁。ReentrantLock还支持公平锁(fair),可以按照线程请求锁的顺序来分配锁。

  • ReentrantReadWriteLock:读写锁,允许多个线程同时读取数据,但写操作是互斥的。它包含一对锁:读锁和写锁。读锁是共享的,写锁是排他的。

  • StampedLock:引入于Java 8,是一种读写锁的替代品,提供了更好的性能。它使用“戳”(stamp)来管理锁状态,支持乐观读、写锁和验证操作。

ReentrantLock锁(Lock的默认实现

ReentrantLock是Java并发包java.util.concurrent.locks中提供的一个可重入互斥锁的实现,

基本使用

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();  // 获取锁
        try {
            count++;   // 保护的代码块
        } finally {
            lock.unlock();// 确保释放锁
        }
    }

    public int getCount() {
        return count;
    }
}
  1. 初始化锁new:在 Counter 类中定义了一个 ReentrantLock 类型的成员变量 lock,并通过 new ReentrantLock() 进行初始化。这意味着每个 Counter 实例都拥有自己的锁对象,互不影响。

  2. 获取锁lock:在 increment() 方法中,首先调用 lock.lock() 来获取锁。如果锁已被其他线程持有,则当前线程将被阻塞,直到锁被释放。

  3. 执行业务代码:获取锁之后,进入临界区,执行 count++ 语句。临界区是多线程环境下需要同步的代码段,确保同一时间只有一个线程可以执行这部分代码,从而避免了并发访问导致的数据不一致问题。

  4. 释放锁unlock无论临界区代码是否正常执行完毕,都需要确保锁的释放。这是通过在 finally 块中调用 lock.unlock() 来实现的。使用 finally 块可以保证即使在临界区代码中发生异常,锁也能被正确释放,避免死锁的发生。

ReentrantLock的特点

  • 可重入性:一个线程可以重复获得同一把锁,即如果一个线程持有一个已被锁定的 ReentrantLock 对象,那么这个线程可以再次获取该锁而不会发生死锁,因为锁会维护一个持有计数来追踪同一个线程多次获得该锁。

  • 可中断的锁获取操作:使用 ReentrantLock 时,线程可以响应中断。即当尝试获取一个锁的线程被中断(通过 Thread.interrupt() 方法)时,可以响应这个中断并终止获取锁的操作,而 synchronized 关键字则不具备这个能力。

  • 尝试非阻塞获取:可以尝试获取锁,如果获取不到可以立即返回,而不是无限期等待。

  • 可限时的锁获取操作:在获取锁时可以指定等待的时间,如果在给定的等待时间内无法获取到锁,则可以放弃获取锁的操作。

ReentrantReadWriteLock锁(读写锁)

ReentrantReadWriteLock 是 Java 并发包中的一种读写锁机制,它允许多个读线程同时访问共享资源,但只允许一个写线程访问共享资源。

基本使用

ReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();

// 读操作
readLock.lock();
try {
   // 读取共享资源
} finally {
   readLock.unlock();
}

// 写操作
writeLock.lock();
try {
   // 修改共享资源
} finally {
   writeLock.unlock();
}

下面我们来详细解释上面的代码:

1.初始化锁
ReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();
  • 创建一个 ReentrantReadWriteLock 实例 lock,它内部维护了读锁和写锁。

  • 通过调用 lock.readLock() 获取读锁 readLock,该锁允许多个线程同时持有,用于保护读操作。

  • 通过调用 lock.writeLock() 获取写锁 writeLock,该锁在同一时间只能由一个线程持有,用于保护写操作。

2.读操作
// 读操作
readLock.lock();
try {
   // 读取共享资源
} finally {
   readLock.unlock();
}
  • 在读取共享资源之前,先调用 readLock.lock() 获取读锁。如果读锁可用,则立即获得;如果已有其他线程持有写锁或正在等待写锁,则当前线程将被阻塞,直到读锁可用。

  • 进入 try 块,执行读取共享资源的代码。由于读锁允许多个线程同时持有,因此可以有多个线程并发执行这部分代码。

  • 在 finally 块中调用 readLock.unlock() 释放读锁。确保即使在读取过程中发生异常,读锁也能被正确释放,避免死锁。

3.写操作
// 写操作
writeLock.lock();
try {
   // 修改共享资源
} finally {
   writeLock.unlock();
}
  • 在修改共享资源之前,先调用 writeLock.lock() 获取写锁。如果写锁可用,则立即获得;如果已有其他线程持有读锁或写锁,则当前线程将被阻塞,直到写锁可用。

  • 进入 try 块,执行修改共享资源的代码。由于写锁在同一时间只能由一个线程持有,因此这部分代码是独占执行的,确保了写操作的原子性和一致性。

  • 在 finally 块中调用 writeLock.unlock() 释放写锁。确保即使在修改过程中发生异常,写锁也能被正确释放,避免死锁。

ReentrantReadWriteLock的特点 

  • 读写分离:内部维护了两个锁,一个读锁(共享锁)和一个写锁(独占锁)。读锁允许多个线程同时持有,而写锁在任何时候只能由一个线程持有。

  • 互斥关系:读锁和写锁之间是互斥的,即当有线程持有写锁时,其他线程无法获取读锁或写锁;而读锁之间是共享的,多个线程可以同时持有读锁。

  • 可重入性:支持可重入,即同一个线程可以多次获取同一把锁。如果一个线程已经持有了读锁,它可以再次获取读锁;如果持有了写锁,它可以再次获取写锁或读锁。

  • 锁降级:支持从写锁降级为读锁,但不支持从读锁升级为写锁。降级过程是先获取写锁,然后获取读锁,最后释放写锁。

  • 公平性:可以设置为公平锁或非公平锁。公平锁会按照线程请求锁的顺序来分配锁,而非公平锁可能会允许在队列外的线程抢到锁。

StampedLock锁(乐观读锁

StampedLock 是 Java 8 引入的一种新型锁机制,它在传统的 ReentrantLockReentrantReadWriteLock 的基础上进行了优化和增强,通过引入乐观读锁和时间戳(stamp)的概念,提升了读写性能,尤其是在读多写少的场景下。

基本使用

import java.util.concurrent.locks.StampedLock;

public class StampedLockExample {
    private double x, y;
    private final StampedLock lock = new StampedLock();

    // 写操作:移动坐标
    public void move(double deltaX, double deltaY) {
        long stamp = lock.writeLock();  // 获取写锁
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            lock.unlockWrite(stamp);  // 释放写锁
        }
    }

    // 悲观读操作:获取坐标
    public double distanceFromOrigin() {
        long stamp = lock.readLock();  // 获取读锁
        try {
            return Math.sqrt(x * x + y * y);
        } finally {
            lock.unlockRead(stamp);  // 释放读锁
        }
    }

    // 乐观读操作:获取坐标
    public double optimisticDistanceFromOrigin() {
        long stamp = lock.tryOptimisticRead();  // 获取乐观读锁
        double currentX = x, currentY = y;
        if (!lock.validate(stamp)) {  // 检查乐观读锁是否被其他写操作干扰
            stamp = lock.readLock();  // 回退到悲观读锁
            try {
                currentX = x;
                currentY = y;
            } finally {
                lock.unlockRead(stamp);  // 释放读锁
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

代码较为复杂,下面我们来详细解析:

1.初始化锁
    private final StampedLock lock = new StampedLock();
  • 创建一个 StampedLock 实例 lock,它是 java.util.concurrent.locks 包中的一个锁机制,用于控制对共享资源的并发访问。

2.写操作
public void move(double deltaX, double deltaY) {
    long stamp = lock.writeLock();  // 获取写锁
    try {
        x += deltaX;
        y += deltaY;
    } finally {
        lock.unlockWrite(stamp);  // 释放写锁
    }
}
  • 获取写锁:调用 lock.writeLock() 方法获取写锁,返回一个 long 类型的 stamp 值。写锁是独占模式,同一时间只能有一个线程持有写锁,用于保护写操作。

  • 执行写操作:在获取写锁后,进入 try 块,执行修改共享资源的操作,即将坐标点 (x, y) 按照给定的增量 (deltaX, deltaY) 进行移动。

  • 释放写锁:在 finally 块中调用 lock.unlockWrite(stamp) 方法释放写锁,传入之前获得的 stamp 值。确保即使在写操作过程中发生异常,写锁也能被正确释放,避免死锁。

3.悲观读操作
public double distanceFromOrigin() {
    long stamp = lock.readLock();  // 获取读锁
    try {
        return Math.sqrt(x * x + y * y);
    } finally {
        lock.unlockRead(stamp);  // 释放读锁
    }
}
  • 获取读锁:调用 lock.readLock() 方法获取读锁,返回一个 stamp 值。读锁是共享模式,允许多个线程同时持有读锁,用于保护读操作。

  • 执行读操作:在获取读锁后,进入 try 块,执行读取共享资源的操作,计算坐标点 (x, y) 到原点的距离。

  • 释放读锁:在 finally 块中调用 lock.unlockRead(stamp) 方法释放读锁,传入之前获得的 stamp 值。确保即使在读操作过程中发生异常,读锁也能被正确释放,避免死锁。

4.乐观读操作 
public double optimisticDistanceFromOrigin() {
    long stamp = lock.tryOptimisticRead();  // 获取乐观读锁
    double currentX = x, currentY = y;
    if (!lock.validate(stamp)) {  // 检查乐观读锁是否被其他写操作干扰
        stamp = lock.readLock();  // 回退到悲观读锁
        try {
            currentX = x;
            currentY = y;
        } finally {
            lock.unlockRead(stamp);  // 释放读锁
        }
    }
    return Math.sqrt(currentX * currentX + currentY * currentY);
}
  • 获取乐观读锁:调用 lock.tryOptimisticRead() 方法获取乐观读锁,返回一个 stamp 值。乐观读锁是一种无锁的数据访问方式,允许读取时不加锁,适用于读多写少的场景,可以显著提高并发性能。

  • 读取数据:在获取乐观读锁后,读取共享资源的当前值 currentX 和 currentY

  • 验证 stamp:调用 lock.validate(stamp) 方法检查在读取数据期间是否有写操作发生。如果验证失败,表示数据可能已被修改。

  • 回退到悲观读锁:如果验证失败,则调用 lock.readLock() 方法获取悲观读锁,重新读取共享资源的值。然后在 finally 块中释放悲观读锁。

  • 返回结果:根据读取的坐标值计算距离并返回

StampedLock锁的特点

锁模式
  • 三种锁模式:提供了写锁、悲观读锁和乐观读锁三种模式,以满足不同的并发场景需求。

  • 写锁(write lock):独占模式的锁,和ReentrantLock类似,保证写操作的排他性。

  • 悲观读锁(read lock):共享模式的锁,多个线程可以同时持有读锁,但写锁需要等待。

  • 乐观读锁(Optimistic Read Lock):无锁机制,基于乐观假设,在读取数据时不加锁,适用于读多写少的场景。

时间戳(Stamp)
  • 锁状态表示:每个锁操作都会返回一个 long 类型的 stamp,代表锁的状态和版本信息。后续操作需要根据这个stamp来验证锁是否有效。

  • 锁转换依据:根据 stamp 进行锁的转换,如乐观读锁验证失败时,可以获取悲观读锁或写锁。

性能优势
  • 高并发性能:在读多写少的场景下,性能优于传统的 ReentrantReadWriteLock,因为乐观读锁减少了线程阻塞和上下文切换。

  • 减少阻塞:乐观读锁允许读操作在没有竞争的情况下快速进行,只有在检测到写操作发生时才会回退到悲观读锁或重试。

缺点
  • 不可重入:StampedLock是非重入锁,这意味着同一个线程不能在持有锁时再次获取同类型的锁,否则会造成死锁。

  • 读锁饥饿:在高写入负载的场景下,悲观读锁可能会被长期阻塞,导致读操作饥饿。

  • CPU飙升风险:如果线程使用writeLock()或者readLock()获得锁之后,线程还没执行完就被interrupt()的话。

标签:Java,获取,stamp,Lock,写锁,读锁,详解,线程,lock
From: https://blog.csdn.net/hrh1234h/article/details/144649097

相关文章