Java 多线程 —— ReentrantLock 与 Condition

本文深入解析ReentrantLock的使用方法,包括公平锁、非公平锁的实现,以及通过Condition实现的生产者消费者模式。探讨了ReentrantLock相较于synchronized的优势与应用场景。

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

引言

ReentrantLock 是 JUC 下的一个功能强劲的锁工具,支持公平锁、非公平锁,以及多等待队列的 Condition 。

也常常被称为“手动锁”。本篇博客主要分析它的使用方法以及 Condition 实现的一个生产者消费者模式。

一、可替代 synchronized 的手动锁

 ReentrantLock是Lock接口的一个实现,可以用于替代synchronized。

使用ReentrantLock可以完成类似synchronized(this)的功能,需要注意的是,就算线程已经执行完毕,Lock也不会自动释放锁,必须要手动释放锁。

与synchronized不同的是:使用synchronized锁如果遇到异常,JVM会自动释放锁,但是Lock必须手动释放锁,因此经常在finally中进行锁的释放。

1.1 模拟 synchronized

public class ReentrantLockDemo {
    Lock lock = new ReentrantLock();
    
    void m1() {
        try {
            lock.lock();
            for (int i = 0; i < 10; i++) {
                TimeUnit.SECONDS.sleep(1);
                System.out.print(i + " ");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();// 比较一下未释放锁前后的执行区别
        }
    }
    
    void m2() {
        lock.lock();
        System.out.println("m2...");
        lock.unlock();
    }
    
    public static void main(String[] args) {
        ReentrantLockDemo r1 = new ReentrantLockDemo();
        new Thread(r1::m1).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(r1::m2).start();
    }
}

执行结果:

1.2 tryLock尝试锁定

将m2()方法修改成如下形式,再次执行,观察结果:

    /**
     * 使用tryLock进行尝试锁定,不管锁定与否,方法都将继续执行, 可以根据tryLock的返回值来判断是否锁定,
     * 也可以指定tryLock的时间,由于tryLock(time)抛出异常,所以注意unLock的处理,必须放在finally中。 <br>
     * 作者: mht<br>
     * 时间:2018年9月15日-下午10:07:42<br>
     */
    void m2() {
//        boolean locked = lock.tryLock();
//        System.out.println("m2..." + locked);
//        if (locked) lock.unlock();

        boolean locked = false;
        try {
            locked = lock.tryLock(3, TimeUnit.SECONDS);
            System.out.print("m2 resume... ");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (locked)
                lock.unlock();
        }
    }

执行结果(修改m2后继续执行第一段代码中的main方法):

Lock 的tryLock()方法支持有参和无参,根据实际需要可以指定具体的等待时间,或不进行等待(如注释掉的代码)。

1.3 允许被打断

在多线程共同请求同一个Lock时,有时会希望某个线程能够被打断,从而终止等待的状态。

Lock.lockInterruptibly()方法同样可以请求一个可用的锁对象,如果锁对象不可用,则进入阻塞状态。但是与Lock.lock()不同的是,当程序中调用Thread.interrupt()方法打断线程时,前者会做出响应,而后者并不会。当然,即便是获得了锁对象,线程依然可以被打断,但是要记住在finally块中释放被打断线程中持有的锁对象

public class InterruptWaitingDemo {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {
            try {
                lock.lock();
                System.out.println("t1 start");
                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
                System.out.println("t1 end");
            } catch (InterruptedException e) {
                System.out.println("interrupted!");
            } finally {
                lock.unlock();
            }
        }, "t1");
        t1.start();

        Thread t2 = new Thread(() -> {
            try {
                lock.lockInterruptibly();
                System.out.println("t2 start");
                TimeUnit.SECONDS.sleep(5);
                System.out.println("t2 end");
            } catch (InterruptedException e) {
                System.out.println("Interrupted!");
            } finally {
                System.out.println("finally:lock = " + lock.tryLock());
                lock.unlock();
            }
        }, "t2");
        t2.start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t2.interrupt();// 打断线程 t2,不论线程是否获得锁。
    }
}

上述代码中,线程t1持有lock对象,并一直锁定,t2无法得到这把锁,如果在线程t2中以原来的lock()方法请求锁的话,在线程外部无法打断该线程,但是使用lockInterruptibly()允许线程在外部被打断。

执行结果:

线程被打断后捕捉到 InterruptedException 异常,而上图是以输出“Interrupted!”字符串的形式提现,图中的异常并不是被打断的异常,而是未获取锁的情况下执行lock.unlock()抛出的异常。

1.4 实现公平锁

公平锁指的是,哪个线程等待时间长就优先获得锁。synchronized属于非公平锁,这意味着锁被释放后其他线程会随机获得synchronized锁。

ReentrantLock可以实现公平锁的需求,例如下面代码所示:

public class FairLockDemo extends Thread {
    /** 参数为true为公平锁 */
    private static ReentrantLock lock = new ReentrantLock(true);

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "获得锁");
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        FairLockDemo demo = new FairLockDemo();
        Thread th1 = new Thread(demo);
        Thread th2 = new Thread(demo);
        th1.start();
        th2.start();
    }
}

执行结果:

二、Condition 条件队列

Lock.newCondition 可以得到一个条件队列对象 —— Condition。

Condition 意为“条件”,它是为满足不同前提条件才能执行操作的线程提供的条件队列。

事实上,synchronized 内置锁也有一个等待队列,但内置的条件队列存在着一些缺陷:每个内置锁只能有一个条件队列,对于一些不同的同步方法,如 put、take,它们虽然都需要锁,但对于一些有界缓存(如ArrayBlockingQueue 等),它们执行操作的前提条件一定是不同的。

与内置条件队列不同的是,对于每个Lock,可以有任意数量的 Condition 对象,同时,Condition 对象继承了相关 Lock 对象的公平性,对于公平的锁,线程会依照 FIFO 的顺序从 Condition.await 中释放。

在 Condition 对象中,与 wait、notify 和 notifyAll 方法对应的分别是 await、signal 和 signalAll。

《Java 并发编程实战》

与内置锁和条件队列一样,当使用显式的 Lock 和 Condition 时,也必须满足锁、条件谓词和条件变量之间的三元关系。在条件谓词中包含的变量必须由 Lock 来保护,并且在检查条件谓词以及调用 await 和 signal 时,必须持有 Lock 对象。

signal 比 signalAll 更高效,它能极大地减少在每次缓存操作中发生的上下文切换与锁请求的次数。

以下程序实现了一个生产者消费者的情景,两个生产者线程,和10个消费者线程:

public class T_Condition<T> {
    
    final private LinkedList<T> list = new LinkedList<>();
    final private int MAX;
    private int size = 0;
    
    public T_Condition(int max) {
        this.MAX = max;
    }
    
    private Lock lock = new ReentrantLock();
    private Condition producer = lock.newCondition();
    private Condition consumer = lock.newCondition();
    
    public void put(T t) {
        try {
            lock.lock();
            while (list.size() == MAX) {
                producer.await();
            }
            list.addFirst(t);
            size++;
            System.out.println(Thread.currentThread().getName() + " put 了1个元素,当前容量:" + size + "/" + MAX);
            // 通知消费者线程进行消费
            consumer.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    
    public T get() {
        T t = null;
        try {
            lock.lock();
            while (list.size() == 0) {
                consumer.await();
            }
            t = list.pollLast();
            size--;
            System.out.println(Thread.currentThread().getName() + " get 了1个元素,当前容量:" + size + "/" + MAX);
            producer.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return t;
    }
    
    public static void main(String[] args) {
        T_Condition<String> container = new T_Condition<String>(10);
        
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                while (true) {
                    container.put(new String());
                }
            }, "producer" + i).start();
        }
        
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                while (true) {
                    container.get();
                }
            }, "consumer" + i).start();
        }
    }
}

总结

1、ReentrantLock是Lock接口经常用到的实现类,它可以代替synchronized实现相同的功能,但是需要手动调用 lock.unlock()释放锁。这也是与synchronized的区别最大的区别。

2、手动锁在使用上更加灵活,操作性更强一些。可以尝试使用 tryLock() lockInterruptibly() 等操作自定义锁的操作。

3、ReentrantLock可以实现公平锁,但是在性能上会有些折扣。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值