跳转至

Java ReentrantLock:深入理解与高效应用

简介

在多线程编程中,确保共享资源的安全访问至关重要。Java 提供了多种同步机制,ReentrantLock 便是其中强大的一种。ReentrantLock 实现了 Lock 接口,相比传统的 synchronized 关键字,它提供了更灵活、更强大的同步控制能力。本文将详细介绍 ReentrantLock 的基础概念、使用方法、常见实践以及最佳实践,帮助你在多线程编程中更好地运用它。

目录

  1. 基础概念
  2. 使用方法
    • 简单锁定与解锁
    • 尝试锁定
    • 公平性选择
  3. 常见实践
    • 资源竞争控制
    • 线程间通信
  4. 最佳实践
    • 合理使用锁的粒度
    • 避免死锁
  5. 小结
  6. 参考资料

基础概念

ReentrantLock 是一个可重入的互斥锁,这意味着同一个线程可以多次获取该锁。每次获取锁时,锁的持有计数会增加,而每次释放锁时,持有计数会减少。当持有计数为 0 时,锁被完全释放,其他线程可以获取。

synchronized 不同,ReentrantLock 提供了更灵活的锁获取和释放方式,并且支持公平性选择。公平锁会按照线程请求的顺序来分配锁,而非公平锁则允许线程在锁可用时立即尝试获取,这通常会带来更高的性能,但可能导致某些线程长时间等待。

使用方法

简单锁定与解锁

import java.util.concurrent.locks.ReentrantLock;

public class SimpleReentrantLockExample {
    private static ReentrantLock lock = new ReentrantLock();
    private static int sharedResource = 0;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            lock.lock();
            try {
                for (int i = 0; i < 1000; i++) {
                    sharedResource++;
                }
            } finally {
                lock.unlock();
            }
        });

        Thread thread2 = new Thread(() -> {
            lock.lock();
            try {
                for (int i = 0; i < 1000; i++) {
                    sharedResource--;
                }
            } finally {
                lock.unlock();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Shared resource value: " + sharedResource);
    }
}

在这个示例中,我们创建了一个 ReentrantLock 实例,并在两个线程中使用它来保护共享资源 sharedResource。通过 lock.lock() 获取锁,在 try 块中访问共享资源,最后在 finally 块中使用 lock.unlock() 释放锁,以确保无论是否发生异常,锁都会被正确释放。

尝试锁定

tryLock() 方法尝试获取锁,如果锁可用,则立即返回 true,否则返回 false,不会使线程阻塞。

import java.util.concurrent.locks.ReentrantLock;

public class TryLockExample {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            if (lock.tryLock()) {
                try {
                    System.out.println("Thread 1 acquired the lock.");
                    // 访问共享资源
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println("Thread 1 could not acquire the lock.");
            }
        });

        Thread thread2 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("Thread 2 acquired the lock.");
                // 长时间持有锁
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                lock.unlock();
            }
        });

        thread2.start();
        thread1.start();
    }
}

在这个示例中,thread1 使用 tryLock() 尝试获取锁,如果无法获取,它不会等待,而是继续执行其他任务。

公平性选择

创建 ReentrantLock 时,可以通过构造函数指定是否为公平锁。

import java.util.concurrent.locks.ReentrantLock;

public class FairnessExample {
    // 创建公平锁
    private static ReentrantLock fairLock = new ReentrantLock(true);
    // 创建非公平锁
    private static ReentrantLock nonFairLock = new ReentrantLock(false);

    public static void main(String[] args) {
        // 使用公平锁和非公平锁的线程示例
    }
}

默认情况下,ReentrantLock 创建的是非公平锁,因为非公平锁在大多数情况下性能更好。但在某些场景下,公平锁能确保线程按照请求顺序获取锁,避免线程饥饿。

常见实践

资源竞争控制

在多线程环境中,多个线程可能同时访问和修改共享资源,这会导致数据不一致问题。ReentrantLock 可以有效控制资源竞争。

import java.util.concurrent.locks.ReentrantLock;

public class ResourceContentionExample {
    private static ReentrantLock lock = new ReentrantLock();
    private static int sharedCounter = 0;

    public static void incrementCounter() {
        lock.lock();
        try {
            sharedCounter++;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    incrementCounter();
                }
            });
            threads[i].start();
        }

        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("Final counter value: " + sharedCounter);
    }
}

在这个示例中,多个线程调用 incrementCounter() 方法,通过 ReentrantLock 确保 sharedCounter 的操作是线程安全的。

线程间通信

ReentrantLock 可以结合 Condition 实现线程间的通信。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadCommunicationExample {
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();
    private static boolean flag = false;

    public static void main(String[] args) {
        Thread producer = new Thread(() -> {
            lock.lock();
            try {
                while (flag) {
                    condition.await();
                }
                // 生产数据
                flag = true;
                System.out.println("Producer produced data.");
                condition.signalAll();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        Thread consumer = new Thread(() -> {
            lock.lock();
            try {
                while (!flag) {
                    condition.await();
                }
                // 消费数据
                flag = false;
                System.out.println("Consumer consumed data.");
                condition.signalAll();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        producer.start();
        consumer.start();
    }
}

在这个示例中,producer 线程和 consumer 线程通过 ReentrantLockCondition 进行通信,实现了数据的生产和消费同步。

最佳实践

合理使用锁的粒度

锁的粒度指的是锁保护的代码范围大小。过大的锁粒度会导致线程竞争激烈,降低并发性能;过小的锁粒度则可能增加锁的获取和释放开销。应根据具体业务场景,合理确定锁的粒度。

避免死锁

死锁是多线程编程中常见的问题,当两个或多个线程相互等待对方释放锁时,就会发生死锁。为避免死锁,应遵循以下原则: - 尽量减少锁的嵌套使用。 - 确保锁的获取顺序一致。 - 使用 tryLock() 方法设置合理的等待时间,避免无限期等待。

小结

ReentrantLock 是 Java 多线程编程中强大的同步工具,它提供了比 synchronized 更灵活、更强大的功能。通过本文的介绍,你了解了 ReentrantLock 的基础概念、使用方法、常见实践以及最佳实践。在实际开发中,应根据具体需求选择合适的同步机制,合理使用 ReentrantLock,以确保多线程程序的正确性和高效性。

参考资料