Thread之ReentrantLock和Condition,Semaphore,Atomic

本文介绍了Java中的可重入锁ReentrantLock的使用,包括基础用法、注意事项以及与synchronized的区别。同时,文章还讨论了条件等待变量Condition、信号量Semaphore的概念和应用,以及原子值Atomic的简单使用和AtomicReference的特性,这些都是Java多线程编程中的重要工具。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


在这里插入图片描述

可重入锁 :ReentrantLock

ReentrantLock(再入锁、可重入锁)是 Java 5 提供的锁实现,它的功能和 synchronized 基本相同。ReentrantLock 通过调用 lock 方法来获取锁,通过调用 unlock 来释放锁。

1. ReentrantLock 使用

ReentrantLock 基础使用 ,代码如下:

Lock lock = new ReentrantLock();
lock.lock();    // 加锁
// 业务代码...
lock.unlock();    // 解锁

使用 ReentrantLock 完善本文开头的非线程安全代码:

public class LockTest {

    static int number = 0;

    public static void main(String[] args) 
            throws InterruptedException {
        // ReentrantLock 使用
        Lock lock = new ReentrantLock();
        Thread thread1 = new Thread(() -> {
            try {
                lock.lock();
                addNumber();
            } finally {
                lock.unlock();
            }
        });

        Thread thread2 = new Thread(() -> {
            try {
                    lock.lock();
                    addNumber();
            } finally {
                    lock.unlock();
            }
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("number:" + number);
    }

    public static void addNumber() {
        for (int i = 0; i < 10000; i++) {
            ++number;
        }
    }
}
  • 尝试获取锁

    ReentrantLock 可以无阻塞尝试访问锁,使用 ReentrantLock#tryLock 方法。

    Lock reentrantLock = new ReentrantLock();
    
    // 线程一
    new Thread(() -> {
        try {
            reentrantLock.lock();
            Thread.sleep(2 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }).start();
    
    // 线程二
    new Thread(() -> {
        try {
            Thread.sleep(1 * 1000);
            System.out.println(reentrantLock.tryLock());    // false
            Thread.sleep(2 * 1000);
            System.out.println(reentrantLock.tryLock());    // true
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
    
  • 尝试一段时间内获取锁

    ReentrantLock#lock 默认是以非阻塞方式获取锁,如果获取不到,则立即以 false 返回。

    ReentrantLock#tryLock 有一个重载方法 ReentrantLock#tryLock(long timeout, TimeUnit unit),用于尝试一段时间内获取锁(而非立即返回)

    注意,在此期间,ReentrantLock 是一直尝试,而非等待一段时间后再试。

    Lock reentrantLock = new ReentrantLock();
    
    // 线程一
    new Thread(() -> {
        try {
            reentrantLock.lock();
            System.out.println(LocalDateTime.now());
            Thread.sleep(2 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }).start();
    
    // 线程二
    new Thread(() -> {
        try {
            Thread.sleep(1 * 1000);
            System.out.println(reentrantLock.tryLock(3, TimeUnit.SECONDS));
            System.out.println(LocalDateTime.now());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
    

    以上代码执行结果如下:

    2019-07-05 19:53:51
    true
    2019-07-05 19:53:53
    

    可以看出锁在休眠了 2 秒之后,就被线程二直接获取到了,所以说 tryLock(long timeout, TimeUnit unit) 方法内的 timeout 参数指的是获取锁的最大等待时间。

2. ReentrantLock 注意事项

  • 使用 ReentrantLock 一定要记得释放锁,否则该锁会被永久占用。

  • lock - unlock 应该成对出现,即,lock 了多少次,就要有相应多少次的 unlock 。

    如果有需要,你可以通过 ReentrantLock#getHoldCount 方法查询当前线程执行 lock的次数

3. ReentrantLock 和 synchronized 有什么区别?

synchronized 和 ReentrantLock 都是保证线程安全的,它们的区别如下:

  • ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
  • ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
  • ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等;
  • ReentrantLock 性能略高于 synchronized。

条件等待变量:Condition

Java 中条件变量都实现了 java.util.concurrent.locks.Condition 接口,条件变量的实例化是通过一个 Lock 对象上调用 newCondition() 方法来获取的,这样,条件就和一个锁对象绑定起来了。因此,Java 中的条件变量只能和锁配合使用,来控制并发程序访问竞争资源的安全。

在 Condition 中,用 await() 替换 wait() ,用 signal() 替换 notify() ,用 signalAll() 替换 notifyAll() ,传统线程的通信方式,Condition 都可以实现。

再次提醒
Condition 是被绑定到 Lock 上的,要创建一个 Lock 的 Condition 必须用 newCondition() 方法。 这样看来,Condition 和传统的线程通信没什么区别,Condition 的强大之处在于它可以为多个线程间建立不同的 Condition 。

信号量:Semaphore

Java 通过 Semaphore 类实现了经典的信号量。信号量通过计数器来控制对共享资源的访问。

  • 如果计数器大于 0 , 则允许访问;
  • 如果计数器为 0 ,则不允许访问。

计数器的计数逻辑上代表着当前共享资源的许可证的数量。

Semaphore 类具有如下所示的两个构造函数:

Semaphore(int num)
Semaphore(int num, boolean how)

其中,num 指定了初始的许可证计数大小。因此,num 指定了任意时刻可以访问共享资源的线程数量。如果,num 是 1,那么任意时刻只有一个线程能够访问资源。

默认情况下,等待获取许可证的线程以随机的方式「抢夺」许可证。通过将 how 设置为 true ,可以确保等待的线程以前后顺序获得许可证。即,是否是公平锁。

为了得到许可证,可以调用 Semaphore#acquire 方法,该方法具有以下 2 种形式:

// 获得 1 个许可证
void acquire() throws InterruptedException

// 获得 num 个许可证
void acquire(int num) throws InterruptedException

如果调用时无法获得许可证,就会挂起线程(阻塞),直到许可证可以获得为止。

为了释放许可证,可以调用 Semaphore#release 方法,该方法具有以下 2 种形式:

// 释放 1 个许可证
void release()

// 释放 num 个许可证
void relase(int num)

注意
为了使用信号量控制对资源的访问,在访问资源之前,希望使用资源的每个线程必须先调用 acquire 方法;当线程使用完资源时,必须调用 release 方法。

原子值:Atomic

原子值(atomic)也是 JDK 1.5 的 J.U.C 特性引入的知识点。

如果多个线程更新一个共享计数器,那么你就需要保证更新操作是以线程安全的方式进行的。因为 i++i-- 这样的操作是非原子性的,它们是线程不安全的。

JDK(从 1.5 开始) 在 java.util.concurrent.atomic 包下面为我们准备了很多可以高效、简洁地「对 int、long 和 boolean 值、对象的引用和数组进行原子性操作」的类。

1. 简单使用

以 AtomicLong 为例。

AtomicLong 的 .incrementAndGet 方法可以将 AtomicLong 对象的值加 1 ,并返回增加之后的值。即,实现 ++i 的逻辑,只不过比 ++i 更高级的是整个操作不能被打断,即,它是原子性的。

AtomicLong i = new AtomicLong(0);

System.out.println( i.incrementAndGet() );

AtomicLong 的各个方法的功能都是显而易见的,此处就不一一展示。

不过,需要注意的是,如果你想先读后写 AtomicLong 的值,不要使用 .get 和 .set 方法,因为它两的组合不是原子性的。你要使用的一个 .updateAndGet 方法来替代它们两个。.updateAndGet 方法要求你传入一个 lambda 表达式,在表达式中它会将 AtomicLong 的原值传进来,你在 lambda 表达式中返回新值。

2. AtomicReference

AtomicReference 类提供了一个可以原子读写的对象引用变量。 原子意味着尝试更改相同 AtomicReference 的多个线程(例如,使用比较和交换操作)不会使 AtomicReference 最终达到不一致的状态。 AtomicReference 甚至有一个先进的 .compareAndSet 方法,它可以将引用与预期值(引用)进行比较,如果它们相等,则在 AtomicReference 对象内设置一个新的引用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值