重入锁(ReentrantLock)、重入读写锁(ReentrantReadWriteLock)、线程通信(Condition)

并发控制与锁机制在多线程编程中的应用
本文详细介绍了使用重入锁、读写锁和读写同步控制方式实现并发控制与锁机制在多线程编程中的应用。通过实例展示了如何使用ReentrantLock和Lock.newCondition()实现生产-消费模型,以及如何使用ReentrantReadWriteLock在并发读写场景下优化性能。此外,还对比了传统同步集合与读写锁控制在效率上的差异。


1.使用重入锁完成的“生产-消费”模型:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Account {
    private static int commonIndex = 5;
    private static Lock indexLock = new ReentrantLock();
    private static Condition canPull = indexLock.newCondition();
    private static Condition needPush = indexLock.newCondition();

    // 多线程索取
    public int pull() {
        int index = 0;
        indexLock.lock();
        try {
            while (commonIndex < 1) {// 判断数据是否符合条件,没有则继续等待并通知补充!
                System.out.println("数量小于1,不够了,请补充...");
                needPush.signalAll();// 数量不够了需要放入,发出需要补充信号
                canPull.await();//等待可以拉取的通知
                System.out.println("收到拉取通知,继续拉取...");
            }
            index = commonIndex;
            Thread.sleep(200); // 用于测试是否会出现取出相同的数字来,(结果:加锁后不会有线程问题)
            commonIndex--;
        } catch (Exception e) {} finally {
            indexLock.unlock();
        }
        return index;
    }

    // 单线成维护
    public void push() {
        indexLock.lock();
        try {
            while (commonIndex <= 0) {
                commonIndex += 3;//以此补充3个
                canPull.signalAll();//通知:可以拉取了...
                System.out.println("报告补充完毕!继续等待补充通知...");
                needPush.await();//等待
            }
        } catch (Exception e) {} finally {
            indexLock.unlock();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(100);
        // 索取
        for (int i = 0; i < 10; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(new Account().pull());
                }
            });
        }
        // 维护,这里也可以用多个线程,但是同步判断条件commonIndex <= 0,
        // 会使得多线程无意义,即只有第一个线程会补充,后续补充线程发现 >0 后不再补充
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                new Account().push();
            }
        });
        // 关闭线程池
        threadPool.shutdown();
    }
}

运行结果:

5
4
3
2
1
数量小于1,不够了,请补充...
数量小于1,不够了,请补充...
数量小于1,不够了,请补充...
数量小于1,不够了,请补充...
数量小于1,不够了,请补充...
报告补充完毕!继续等待补充通知...
收到拉取通知,继续拉取...
3
收到拉取通知,继续拉取...
2
收到拉取通知,继续拉取...
1
收到拉取通知,继续拉取...
数量小于1,不够了,请补充...
收到拉取通知,继续拉取...
数量小于1,不够了,请补充...
报告补充完毕!继续等待补充通知...
收到拉取通知,继续拉取...
3
收到拉取通知,继续拉取...
2

本文为了说明了ReentrantLock和Lock.newCondition()用法,其实上述的 “生产-消费”模型使用 阻塞队列 实现会简单很多。

2.重入读写锁,允许并发读,提高性能,下面是混合读写的锁控制模型:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class Account {
    int data = 0;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    void processCachedData() throws Exception {
        rwl.readLock().lock();
        log("is reading ...");
        if (isOk()) {
            rwl.readLock().unlock();//写锁与读锁互斥,必需释放rwl的读锁后才能上rwl的写锁
            rwl.writeLock().lock();
            try {
                if (isOk()) {// 得到写锁后再判断,防止被其它线程抢先更改
                    log("is writing ...");
                    data += 3;// 进行写操作
                    //Thread.sleep(200000);//可以看到,加上写锁将不可再读
                    Thread.sleep(2);
                } else {
                    log("发现已被抢先更新,无需再更新。");
                }
            } finally {
                log("write over.");
                rwl.readLock().lock();// 在释放写锁之前最好先先获取读锁,以防止使用之前被其他写线程改变了数据状态
                rwl.writeLock().unlock(); // 释放写锁,但上面一句依旧保留了读锁,可防止写锁介入
            }
        }
        try {
            log("读取并使用数据:" + data--);
            Thread.sleep(1000);
        } finally {
            log("read over.");
            rwl.readLock().unlock();
        }
    }
    boolean isOk() {
        return data == 0;
    }
    void log(String msg) {
        System.out.println(Thread.currentThread().getName() + " " + msg);
    }
}
public class Test {
    public static void main(String[] args) {
        final Account account = new Account();
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        account.processCachedData();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        executorService.shutdown();
    }
}
输出:

pool-1-thread-1 is reading ...
pool-1-thread-1 is writing ...
pool-1-thread-1 write over.
pool-1-thread-1 读取并使用数据:3
pool-1-thread-2 is reading ...
pool-1-thread-2 读取并使用数据:2
pool-1-thread-3 is reading ...
pool-1-thread-3 读取并使用数据:1
pool-1-thread-4 is reading ...
pool-1-thread-5 is reading ...
pool-1-thread-1 read over.
pool-1-thread-2 read over.
pool-1-thread-3 read over.
pool-1-thread-4 is writing ...
pool-1-thread-4 write over.
pool-1-thread-4 读取并使用数据:3
pool-1-thread-4 read over.
pool-1-thread-5 发现已被抢先更新,无需再更新。
pool-1-thread-5 write over.
pool-1-thread-5 读取并使用数据:2
pool-1-thread-5 read over.
可以看出:

a.读写锁的优势在于允许同时加读锁以提供性能

b.读锁释放前不能加写锁

c.写锁释放前不能加读锁或写锁


3.一种更加常用的读写同步控制方式

import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * get 和 allKeys 是读操作
 * put 和 clear 是写操作
 * 读操作可以并发进行,但有写锁时会等写锁释放后再加读锁
 * 写操作不允许同时读或写,故必需先释放所有的读锁和写锁
 */
public class RWDictionary {
    private final Map<String, String> m = new TreeMap<String, String>();
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();
    public String get(String key) {
        r.lock();
        try {
            return m.get(key);
        } finally {
            r.unlock();
        }
    }
    public Object[] allKeys() {
        r.lock();
        try {
            return m.keySet().toArray();
        } finally {
            r.unlock();
        }
    }
    public String put(String key, String value) {
        w.lock();
        try {
            return m.put(key, value);
        } finally {
            w.unlock();
        }
    }
    public void clear() {
        w.lock();
        try { m.clear(); } finally { w.unlock(); }
    }
}
上面的例子中,有多个写操作和多个读操作,它们同样遵循原则:并发读、互斥写

上面的例子对非线程安全的集合进行了读写锁控制,比直接使用同步的集合(同步所有访问)效率更高!

<think>好的,我现在需要解释“可重入锁”这个概念。首先,用户之前问了关于Java多线程的问题,现在继续深入锁机制,应该保持技术连贯性。 可重入锁,英文是ReentrantLock,属于Java并发包里的一个类。那它的核心特点是什么?我记得是可重入性,也就是同一个线程可以多次获取同一把锁,而不会自己阻塞自己。这点和synchronized关键字类似,但ReentrantLock功能更强大。 用户可能想知道为什么需要可重入。比如,一个同步方法调用另一个同步方法,如果锁不可重入线程就会死锁。这时候需要锁支持重入,每次获取锁计数器加一,释放时减一,直到零才真正释放。 接下来需要比较ReentrantLock和synchronized的区别。比如ReentrantLock可以设置公平性,避免线程饥饿;支持中断等待,能尝试获取锁;还有条件变量等高级功能。但使用时必须手动释放锁,容易出错,通常配合try-finally使用。 另外,可重入锁的实现原理,比如通过AQS(AbstractQueuedSynchronizer)维护状态和等待队列。每个锁关联一个持有线程和计数器,记录重入次数。这部分可能用户不太需要深入,但了解原理有助于正确使用。 还要注意适用场景。虽然ReentrantLock灵活,但synchronized在简单情况下更简洁,JVM也会优化。所以需要根据需求选择,比如需要超时功能或公平锁时选ReentrantLock。 可能用户遇到的常见问题,比如忘记释放锁导致死锁,或者错误地使用非公平锁引起性能问题。这时候需要强调正确使用模式,比如在finally块中释放锁。 最后,给出代码示例,展示基本用法,比如lock()和unlock(),以及条件变量的使用。同时提醒在Java 5之后ReentrantLock的性能已经和synchronized接近,但编码复杂度更高。 总结时,要明确可重入锁的作用、优势、使用注意事项,以及与synchronized的对比,帮助用户根据实际情况做出选择。</think>可重入锁(Reentrant Lock)是并发编程中的重要概念,下面通过分层解析帮助您深入理解: --- ### **一、核心特征** 1. **重入特性** - 同一线程可重复获取已持有的锁 - 示例场景: ```java public synchronized void methodA() { methodB(); // 调用另一个同步方法 } public synchronized void methodB() { // 若锁不可重入,此处将导致死锁 } ``` 2. **锁计数器** - 每次获取锁计数器+1,释放时-1 - 计数器归零时完全释放锁资源 --- ### **二、Java实现对比** | 特性 | `synchronized`关键字 | `ReentrantLock`类 | |--------------------|--------------------------|--------------------------| | 锁获取方式 | 自动获取/释放 | 手动`lock()`/`unlock()` | | 公平性 | 非公平(无法指定) | 可构造公平/非公平锁 | | 锁中断 | 不支持 | `lockInterruptibly()`支持 | | 条件变量 | 单一`wait()`/`notify()` | 可创建多个`Condition`对象 | | 超时机制 | 无 | `tryLock(timeout)`支持 | | 性能 | JVM优化较好 | Java 5+优化后接近 | --- ### **三、底层实现原理** ```java // 简化的锁状态管理逻辑 public class ReentrantLock { private volatile int state; // 锁状态计数器 private Thread owner; // 当前持有线程 public void lock() { if (owner == Thread.currentThread()) { state++; // 重入计数 } else { // 通过CAS竞争锁... } } public void unlock() { if (--state == 0) { owner = null; // 完全释放 } } } ``` - 基于AQS(AbstractQueuedSynchronizer)实现 - 通过CLH队列管理等待线程 --- ### **四、最佳实践示例** ```java ReentrantLock lock = new ReentrantLock(true); // 创建公平锁 void safeOperation() { lock.lock(); try { // 临界区操作 nestedLock(); // 嵌套调用 } finally { lock.unlock(); // 必须确保释放 } } void nestedLock() { lock.lock(); // 可重入获取 try { // 嵌套操作 } finally { lock.unlock(); } } ``` --- ### **五、典型应用场景** 1. **递归调用**:方法递归时的线程安全 2. **锁粒度细化**:需要细粒度控制锁获取/释放顺序 3. **复杂同步**:使用多个Condition实现精准唤醒 ```java Condition notFull = lock.newCondition(); Condition notEmpty = lock.newCondition(); ``` 4. **分布式锁基础**:可重入特性在分布式锁设计中同样重要 --- ### **六、注意事项** 1. **解锁对称性**:必须确保`lock()`与`unlock()`成对出现 2. **死锁风险**:虽然可重入,但跨锁嵌套仍可能死锁 3. **性能权衡**:公平锁会降低吞吐量(上下文切换增加约10-100倍) --- ### **七、扩展思考** - 可重入读写锁(`ReentrantReadWriteLock`)如何提升并发度? - `StampedLock`如何通过乐观优化性能? - 如何在分布式系统中实现可重入特性?(如Redis+Lua脚本实现原子计数) 理解可重入锁是掌握Java并发编程的重要基石,合理选择同步机制能显著提升系统性能和可靠性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值