Java-多线程并发-5.锁


🌟 可重入锁 (Reentrant Lock) in Java


📌 定义

  • 可重入锁即是指一个线程可以多次获取同一把锁。在Java中,synchronized 提供的锁就是可重入锁。

📌 为什么需要可重入性

  1. 📜 避免死锁 如果锁不是可重入的,那么调用一个已经获取了锁的方法的其他同步方法时,会导致死锁。
  2. 📜 提高封装性 可重入性允许在一个同步方法中调用另一个同步方法。

📜 示例分析

public class Counter {
    private int count = 0;

    public synchronized void add(int n) {
        if (n < 0) {
            dec(-n);
        } else {
            count += n;
        }
    }

    public synchronized void dec(int n) {
        count += n;
    }
}
  • 在上述代码中,add 方法是同步的,所以当线程进入这个方法时,它会获取this对象的锁。
  • n < 0 时,add 方法调用 dec 方法。由于 dec 方法也是同步的,它也需要this对象的锁。
  • 由于Java中的synchronized锁是可重入的,所以线程可以再次获取this对象的锁,并进入dec方法,而不会发生死锁。

📝 总结

  • Java中的synchronized锁是可重入的,这意味着一个线程可以多次获取同一把锁。
  • 可重入性是为了避免死锁并提高封装性。
  • ⚠️ 注意: 当一个线程获取了锁后,JVM会记录这个锁的重入次数。只有当重入次数减少到0时,锁才会真正被释放,其他线程才能获取该锁。

🌟 死锁 (Deadlock) 和如何避免


📌 死锁的定义

  • 当两个或多个线程互相等待对方释放锁,并永远地等待下去,这种情况称为死锁。

📌 死锁的条件

  1. 📜 互斥条件 一个资源每次只能被一个线程使用。
  2. 📜 请求与保持条件 一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 📜 不剥夺条件 线程已获得的资源在未使用完之前不能被其他线程强行剥夺。
  4. 📜 循环等待条件 若干进程之间形成一种头尾相接的循环等待资源关系。

📜 示例分析

public void add(int m) {
    synchronized(lockA) { ... }
    synchronized(lockB) { ... }
}

public void dec(int m) {
    synchronized(lockB) { ... }
    synchronized(lockA) { ... }
}
  • 在上述代码中,如果两个线程同时执行 add()dec(),可能会发生死锁。
  • 线程1 获得 lockA 并等待 lockB,而 线程2 获得 lockB 并等待 lockA,造成了死锁。

🛠️ 如何避免死锁

  • 避免死锁的基本方法是确保所有线程都以相同的顺序获得锁。
  • 在上述示例中,我们可以通过始终首先获得 lockA,然后获得 lockB 来避免死锁。
public void dec(int m) {
    synchronized(lockA) { ... }
    synchronized(lockB) { ... }
}

📝 总结

  • 死锁是多线程编程中的一个严重问题,一旦发生,只能强制结束进程。
  • 避免死锁的关键是确保所有线程都以相同的顺序获得锁。
  • ⚠️ 注意: 在设计多线程程序时,始终要考虑死锁的可能性,并采取预防措施。

🌟 ReentrantLock in Java


📌 ReentrantLock 的基本概念

  • ReentrantLock 是一个实现了Lock接口的类,与传统的synchronized锁机制相比,提供了更高的加锁和解锁的灵活性。

📜 为什么选择 ReentrantLock

  1. 📜 可重入性synchronized一样,一个线程可以多次获取同一个锁。
  2. 📜 尝试获取锁 提供了tryLock()方法,可以设置等待锁的时间,从而避免无限期的等待。
  3. 📜 中断获取锁 可以中断等待锁的线程。
  4. 📜 公平锁 可以设置为公平锁,这样锁会在等待时间最长的线程中按顺序分配。

📜 如何使用 ReentrantLock

  • 使用ReentrantLock的基本步骤是:
    1. 🔐 创建一个ReentrantLock实例。
    2. 🔐 在逻辑代码前调用lock()方法来获取锁。
    3. 🔐 在finally块中调用unlock()方法来释放锁。
public class Counter {
    private final Lock lock = new ReentrantLock();
    private int count;

    public void add(int n) {
        lock.lock();
        try {
            count += n;
        } finally {
            lock.unlock();
        }
    }
}
  • 在尝试获取锁时,可以使用tryLock()方法,并设定一个最大等待时间。如果在这个时间内没有获取到锁,该方法将返回false
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        ...
    } finally {
        lock.unlock();
    }
}

📝 总结

  • ReentrantLock 提供了比 synchronized 更强大和灵活的锁定机制。
  • 通过tryLock,可以避免线程的死锁。
  • 使用 ReentrantLock 时,要确保始终在 finally 块中释放锁,以避免出现死锁。
  • ⚠️ 注意: 虽然 ReentrantLock 提供了更多的功能,但在某些情况下,简单的synchronized可能更适合。选择哪种锁取决于具体的用例。

🌟 使用 ReentrantLockCondition 替代 synchronized, waitnotify


📌 基本概念

  • 在多线程编程中,当某个条件不满足时,线程可能需要等待,直到该条件变为真。这是通过synchronized关键字配合wait()notify()/notifyAll()方法来实现的。
  • ReentrantLock配合Condition提供了一种更加灵活的方式来实现线程的等待和唤醒。

📜 如何使用 Condition

  1. 📜 创建 Condition 通过 Lock 对象的 newCondition() 方法来创建。
  2. 📜 等待 使用 Conditionawait() 方法来使线程等待。
  3. 📜 唤醒 使用 Conditionsignal()signalAll() 来唤醒等待的线程。

🔍 示例代码

class TaskQueue {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private Queue<String> queue = new LinkedList<>();

    public void addTask(String s) {
        lock.lock();
        try {
            queue.add(s);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public String getTask() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                condition.await();
            }
            return queue.remove();
        } finally {
            lock.unlock();
        }
    }
}

📜 特点和优势

  1. 📜 灵活性 Condition 提供了比 waitnotify 更灵活的线程等待/唤醒机制。
  2. 📜 多条件 一个 Lock 可以绑定多个 Condition,这让你可以在不同的情况下进行不同的等待。
  3. 📜 可定时的等待tryLock 类似,await 可以设置一个最大等待时间。

📝 总结

  • 使用 ReentrantLockCondition 可以提供比 synchronized, wait, 和 notify 更强大和灵活的线程同步机制。
  • synchronized 关键字不同,当使用 ReentrantLockCondition 时,你有责任手动锁定和解锁。
  • ⚠️ 注意: 在实际应用中,选择合适的同步机制取决于特定的需求和场景。

🌟 使用 ReadWriteLock 提高并发读效率


📌 概念简介

  • 在多线程并发读写场景下,很多时候读操作的频率远远大于写操作。使用传统的锁,如synchronizedReentrantLock,会导致所有的读和写操作都是串行的,从而降低了系统的整体性能。

  • ReadWriteLock 是一种特殊的锁,它区分了读操作和写操作,允许多个线程同时进行读操作,但在写操作时,所有的读和写操作都会被阻塞,直到写操作完成。

📜 使用方法

  1. 📜 创建 ReadWriteLock 使用 ReentrantReadWriteLock 类来创建。
  2. 📜 获取读锁 使用 readLock() 方法。
  3. 📜 获取写锁 使用 writeLock() 方法。

🔍 示例代码

public class Counter {
    private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
    private final Lock rlock = rwlock.readLock();
    private final Lock wlock = rwlock.writeLock();
    private int[] counts = new int[10];

    public void inc(int index) {
        wlock.lock(); // 加写锁
        try {
            counts[index] += 1;
        } finally {
            wlock.unlock(); // 释放写锁
        }
    }

    public int[] get() {
        rlock.lock(); // 加读锁
        try {
            return Arrays.copyOf(counts, counts.length);
        } finally {
            rlock.unlock(); // 释放读锁
        }
    }
}

📜 特点和优势

  1. 📜 并发读 允许多个线程同时进行读操作。
  2. 📜 独占写 当有线程进行写操作时,不允许其他线程进行任何读或写操作。
  3. 📜 性能优化 在读多写少的场景中,ReadWriteLock 可以提供比传统锁更好的性能。

📝 总结

  • ReadWriteLock 是一种特殊的锁机制,用于提高在读多写少的场景中的性能。
  • 使用 ReadWriteLock 时,要确保正确地获取和释放读锁和写锁。
  • 读锁允许多线程并发读,但在有线程持有写锁时不允许任何其他的读和写操作。
  • 写锁保证了写操作的独占性。
  • 使用 ReadWriteLock 可以提高并发性,但也可能增加编程的复杂性,因此,在使用时要特别小心。

🌟 使用 StampedLock 提高并发读效率


📌 概念简介

  • StampedLock 是 Java 8 中引入的一种新的锁机制,它主要解决了 ReadWriteLock 的一些局限性。与 ReadWriteLock 不同的是,StampedLock 支持 “乐观读”,这种读取方式允许一个线程在读取数据时,其他线程可以进行写入。

  • 乐观读意味着在大部分情况下,读操作不需要等待写操作完成,从而大大提高了并发效率。但是,这也带来了数据一致性的问题,因为在乐观读的过程中,可能会遇到其他线程正在写入的情况。为了处理这种情况,StampedLock 提供了一个 validate() 方法来检查在读取过程中是否发生了写操作。

📜 使用方法

  1. 📜 创建 StampedLock 使用 StampedLock 类来创建。
  2. 📜 获取写锁 使用 writeLock() 方法。
  3. 📜 乐观读 使用 tryOptimisticRead() 方法。
  4. 📜 验证乐观读 使用 validate() 方法检查在读取过程中是否发生了写操作。
  5. 📜 获取悲观读锁 如果乐观读失败,使用 readLock() 方法。

🔍 示例代码

public class Point {
    private final StampedLock stampedLock = new StampedLock();
    private double x;
    private double y;

    public void move(double deltaX, double deltaY) {
        long stamp = stampedLock.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
    }

    public double distanceFromOrigin() {
        long stamp = stampedLock.tryOptimisticRead();
        double currentX = x;
        double currentY = y;
        if (!stampedLock.validate(stamp)) {
            stamp = stampedLock.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                stampedLock.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

📜 特点和优势

  1. 📜 并发读 允许多个线程同时进行读操作。
  2. 📜 独占写 当有线程进行写操作时,不允许其他线程进行任何读或写操作。
  3. 📜 乐观读 允许一个线程在读取数据时,其他线程可以进行写入。
  4. 📜 数据一致性 通过 validate() 方法确保数据在读取过程中的一致性。

📝 总结

  • StampedLock 是一种特殊的锁机制,用于提高在读多写少的场景中的性能。
  • 使用 StampedLock 时,要确保正确地获取和释放读锁和写锁。
  • 通过乐观读和悲观读的结合,StampedLock 能够实现高效的并发读取,同时保证数据的一致性。
  • 但是,使用 StampedLock 会增加编程的复杂性,并且 StampedLock 是不可重入的,所以使用时需要特别小心。

🌟 使用 Semaphore 进行线程并发限制


📌 概念简介

  • Semaphore 是一个用于管理许可的类,它可以限制对受限资源的并发访问数量。

  • 通过控制许可的数量,可以实现对资源的并发访问限制。例如,如果 Semaphore 初始化为3,则最多只允许3个线程同时访问受限资源。

  • 它常常被用于资源池,如数据库连接池,线程池等。

📜 使用方法

  1. 📜 创建 Semaphore 使用 Semaphore 构造函数并指定许可数量。

  2. 📜 获取许可 使用 acquire() 方法。如果所有许可都被其他线程占用,则当前线程会阻塞直到获取到许可。

  3. 📜 释放许可 使用 release() 方法。释放许可后,其他正在等待的线程可能会获得许可并继续执行。

  4. 📜 尝试获取许可 使用 tryAcquire() 方法。它可以指定等待时间,如果在指定时间内无法获取许可,它将返回 false

🔍 示例代码

public class DatabaseConnections {
    // 假设最多允许100个数据库连接
    private final Semaphore semaphore = new Semaphore(100);

    public Connection getConnection() throws InterruptedException {
        semaphore.acquire();
        try {
            // 获取数据库连接
            return createNewConnection();
        } finally {
            semaphore.release();
        }
    }

    private Connection createNewConnection() {
        // 创建新的数据库连接...
        return new Connection();
    }
}

📜 特点和优势

  1. 📜 灵活的资源访问控制 通过调整许可的数量,可以轻松地控制对资源的并发访问。

  2. 📜 资源管理 对于资源有限的场景,例如数据库连接,线程,文件句柄等,Semaphore 提供了有效的管理机制。

  3. 📜 提高系统稳定性 防止过多的并发请求压垮系统。

📝 总结

  • Semaphore 是一个用于管理许可的并发工具,它允许多个线程访问一个或多个受限资源。

  • 通过控制许可的数量,可以限制对资源的并发访问,从而有效地管理和控制资源。

  • 使用 Semaphore 时,需要注意合理设置许可的数量,并确保在使用资源后正确释放许可,以便其他线程可以获取并使用资源。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yueerba126

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值