一天一个 JUC 工具类 Lock 和 Condition

简介: 当谈到Java多线程编程时,我们不可避免地需要处理并发问题。为此Java提供了一个强大的工具包——java.util.concurrent(JUC)

Lock 和 Condition

当谈到Java多线程编程时,我们不可避免地需要处理并发问题。为此Java提供了一个强大的工具包——java.util.concurrent(JUC),其中的Lock和Condition是两个核心组件。这篇博客将详细展开关于Lock和Condition的使用背景、实际案例、注意事项以及底层实现原理,帮助读者更好地掌握这两个工具类,并在多线程编程中灵活应用。

Lock的使用背景:

传统synchronized关键字的局限性:

传统的Java并发编程中,我们常使用synchronized关键字来实现线程同步,确保多个线程对共享资源的访问是安全的。尽管synchronized是Java语言提供的内置锁机制,但它也存在一些局限性:

  1. 性能问题: synchronized关键字在获取锁和释放锁的过程中会涉及到线程的上下文切换,这会导致性能损失,特别是在高并发场景下。当多个线程竞争同一个锁时,其他线程只能等待,无法并发执行,从而降低了系统的吞吐量。
  2. 灵活性不足: synchronized关键字是在代码层面进行加锁的,一旦加锁后,只能等待获取锁的线程释放锁才能继续执行。在某些情况下,我们可能需要更灵活的锁控制,比如可以尝试获取锁并在获取失败时执行其他逻辑,而不是一直等待。

引入Lock的原因:

为了解决synchronized关键字的性能问题和灵活性不足,Java引入了Lock接口,作为对传统同步块的增强。Lock提供了更高级别的并发控制,允许更细粒度的锁控制,并且在某些情况下比synchronized更高效。

Lock的作用和优势:

Lock接口的出现填补了synchronized关键字的局限性,它具有以下作用和优势:

  1. 性能优化: Lock在大部分情况下比synchronized更高效。它采用了更细粒度的锁控制,减少了线程的上下文切换,提高了并发性能。在高并发场景下,使用Lock能够大大减少线程的竞争和等待时间,从而提升系统的吞吐量。
  2. 可中断的获取锁操作: Lock提供了可中断的获取锁操作,即在等待锁的过程中,可以响应中断请求。这样可以避免线程长时间地被阻塞在获取锁的过程中,更好地处理中断逻辑。
  3. 超时获取锁: Lock还支持超时获取锁的操作,即在指定时间内尝试获取锁,如果获取失败,则可以放弃获取锁而执行其他逻辑。这样可以避免线程一直等待锁的释放,增加了灵活性。
  4. 可重入性: Lock可以支持可重入性,即同一个线程在获取了锁之后,可以再次获取锁而不会造成死锁。
  5. 公平性: Lock可以支持公平性,即按照线程的请求顺序来分配锁,避免了某些线程一直无法获取锁的饥饿现象。

综上所述,Lock的出现填补了传统synchronized关键字的不足,提供了更高级别的并发控制机制,让开发人员在多线程编程中能够更灵活地控制锁,提高系统的性能和可靠性。

Lock的使用:

建Lock实例: 首先,我们需要创建一个Lock实例。在大多数情况下,我们会选择使用ReentrantLock类来创建Lock实例。示例代码如下:

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

Lock lock = new ReentrantLock();

获取锁: 在需要同步的代码块中,我们使用Lock实例的lock()方法来获取锁。该方法会尝试获取锁,如果锁当前没有被其他线程占用,则获取成功,线程可以进入临界区执行操作。如果锁已经被其他线程占用,则当前线程会被阻塞,直到锁被释放。示例代码如下:

lock.lock(); // 获取锁
try {
    
    // 执行需要线程同步的操作(临界区)
} finally {
    
    lock.unlock(); // 释放锁,务必在finally块中释放锁,以确保锁一定会被释放
}

释放锁: 在使用完临界区的代码后,我们需要通过unlock()方法来释放锁。确保在获取锁之后,不论临界区代码是否出现异常,都能正确释放锁。示例代码如上所示。

Lock的特点在代码中的体现:

相较于synchronized关键字,Lock的特点主要体现在以下几个方面:

  1. 手动获取和释放锁: 使用Lock需要手动获取和释放锁,而synchronized关键字在进入和退出代码块时会自动获取和释放锁。
  2. 可中断获取锁: Lock提供了可中断的获取锁操作,即lock()方法可以响应中断请求。
  3. 可超时获取锁: Lock还支持超时获取锁的操作,即tryLock()方法可以在指定的时间内尝试获取锁。
  4. 公平性: Lock可以支持公平性,即在等待获取锁的队列中按照线程请求的顺序来分配锁。

使用Lock解决资源竞争问题和死锁问题:

解决资源竞争问题:

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

public class ResourceRaceDemo {
    
    private Lock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
    
        lock.lock();
        try {
    
            count++;
        } finally {
    
            lock.unlock();
        }
    }
}

在上面的代码中,我们使用ReentrantLock来保护count变量的访问,避免多个线程同时修改count而导致的竞争问题。

解决死锁问题:

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

public class DeadlockDemo {
    
    private Lock lock1 = new ReentrantLock();
    private Lock lock2 = new ReentrantLock();

    public void method1() {
    
        lock1.lock();
        try {
    
            // 获取lock1后继续尝试获取lock2
            lock2.lock();
            try {
    
                // 执行操作
            } finally {
    
                lock2.unlock();
            }
        } finally {
    
            lock1.unlock();
        }
    }

    public void method2() {
    
        lock2.lock();
        try {
    
            // 获取lock2后继续尝试获取lock1
            lock1.lock();
            try {
    
                // 执行操作
            } finally {
    
                lock1.unlock();
            }
        } finally {
    
            lock2.unlock();
        }
    }
}

在上面的代码中,我们使用两个ReentrantLock实例lock1和lock2来保护method1和method2方法,避免了死锁问题的发生。

实现线程同步保障数据一致性和安全性:

在使用Lock进行线程同步时,我们可以确保共享数据的一致性和安全性。通过合理地获取和释放锁,保护共享资源,我们可以避免多个线程同时对共享资源进行修改,从而确保数据的正确性和一致性。在上面的代码示例中,我们使用Lock锁来保护共享变量count的访问,从而避免了多个线程同时修改count导致的数据不一致问题。

Lock的使用注意事项:

1. 避免死锁: 死锁是多个线程相互等待对方释放锁,导致所有线程都无法继续执行的情况。为了避免死锁,需要按照固定的顺序获取锁,即线程按照相同的顺序获取锁资源。另外,可以设置超时时间来尝试获取锁,如果超过一定时间仍未获取到锁,则放弃获取并执行其他逻辑,避免线程长时间等待。

2. 正确释放锁: 在使用Lock时,必须在合适的地方释放锁,否则可能导致锁泄漏或产生死锁。为了确保锁一定会被释放,通常在finally块中释放锁。这样即使临界区代码出现异常,锁也能被正确释放。

Lock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区代码
} finally {
    lock.unlock(); // 确保在任何情况下都能正确释放锁
}

3. 可中断获取锁: Lock提供了可中断获取锁的操作,即lock()方法可以响应中断请求。在使用lock()方法获取锁时,如果当前线程被中断,它会立即响应中断并抛出InterruptedException异常。可以在catch块中处理中断逻辑。

4. 可超时获取锁: Lock支持超时获取锁的操作,即tryLock()方法可以在指定的时间内尝试获取锁。如果在指定时间内未能获取到锁,则可以放弃获取锁并执行其他逻辑。

Lock lock = new ReentrantLock();
if (lock.tryLock(5, TimeUnit.SECONDS)) {
    
    try {
    
        // 获取锁成功,执行临界区代码
    } finally {
    
        lock.unlock();
    }
} else {
    
    // 获取锁失败,执行其他逻辑
}

5. 公平性: Lock可以支持公平性,即在等待获取锁的队列中按照线程请求的顺序来分配锁。通过在创建Lock实例时传入true来实现公平性,默认情况下为非公平锁。

Lock lock = new ReentrantLock(true); // 公平锁

与synchronized关键字进行比较:

Lock和synchronized关键字都可以用于实现线程同步,但在何时选择Lock而非synchronized需要根据具体场景来考虑:

  1. 灵活性: Lock提供了更高级别的线程同步控制,比synchronized更灵活,例如可中断获取锁、可超时获取锁等。如果需要更细粒度的锁控制或更灵活的线程同步机制,可以选择使用Lock。
  2. 性能: 在高并发场景下,Lock通常比synchronized更高效。synchronized是Java语言提供的内置锁,虽然Java对其进行了优化,但在某些情况下,Lock的性能更优。如果性能是一个关键考虑因素,可以考虑使用Lock。
  3. 可中断性: Lock提供了可中断获取锁的功能,这在某些情况下是非常有用的。如果需要线程在等待锁的过程中可以响应中断请求,那么Lock是一个更好的选择。
  4. 公平性: Lock可以支持公平性,而synchronized是非公平锁。如果需要在等待获取锁的队列中按照线程请求的顺序分配锁,可以选择使用Lock,并传入true实现公平性。

综上所述,Lock提供了更高级别的线程同步控制,拥有更多的特性和灵活性,而synchronized是Java内置的基本锁机制。在实际应用中,可以根据具体需求来选择合适的线程同步方式,以确保程序的正确性和性能。

Condition的使用背景:

Condition是Java JUC(并发工具包)中的一个重要组件,它是Lock接口的一个补充,用于解决传统线程同步机制无法满足的问题,允许线程间进行协作和通信。

引入Condition的作用和用途:

  1. 线程协作和通信: Condition允许线程在特定条件下进行协作和通信。在传统线程同步机制中,线程只能通过竞争锁来实现同步,但无法进行线程间的有效通信。Condition的出现使得线程可以等待特定条件满足时再进行操作,从而更好地协调多个线程的执行顺序和互动。
  2. 避免忙等待: 在某些场景下,如果线程需要等待某个条件满足后再继续执行,传统的忙等待(busy-waiting)方式会导致CPU资源的浪费。而Condition可以让线程在等待条件时进入等待状态,直到其他线程发出特定的信号来唤醒它。
  3. 精确唤醒: 使用Condition,我们可以更加精确地控制哪些线程被唤醒。传统的notify()和notifyAll()方法会随机地唤醒等待的线程,而Condition提供了更细粒度的唤醒控制,可以选择性地唤醒特定条件下等待的线程。
  4. 多个条件的等待和唤醒: Condition可以创建多个等待队列,每个队列可以根据不同的条件进行等待和唤醒。这使得线程可以根据不同的条件选择性地等待和唤醒,从而提高了线程间的灵活性和协作能力。

Condition是Lock的一个重要补充:

在Java中,Lock接口提供了更灵活和高级的线程同步控制机制,但是Lock本身并没有提供等待/通知机制。而Condition的出现填补了这个缺陷,它为Lock提供了等待/通知的功能,允许线程在等待某个条件满足时进入等待状态,并在其他线程满足条件后通知被唤醒。

在Lock接口中,我们可以通过newCondition()方法来创建一个Condition实例,每个Condition实例都与一个Lock相关联。通过Condition,线程可以在等待某个条件时调用await()方法进入等待状态,而其他线程在满足条件时调用signal()或signalAll()方法来唤醒等待的线程。

Condition的出现使得Lock接口更加强大和灵活,允许线程间进行更细粒度的通信和协作,解决了传统线程同步机制无法满足的问题。在复杂的多线程编程中,Condition为我们提供了一种更高级别的线程同步和协作方式,从而让我们能够更好地控制线程的执行顺序和互动。

Condition的使用案例:

Condition是通过Lock接口的newCondition()方法创建的,每个Condition实例都与一个Lock相关联。Condition允许线程在等待某个条件满足时进入等待状态,而其他线程在满足条件时可以通知等待的线程。下面是Condition的主要方法:

  1. await(): 当线程调用await()方法时,它会释放当前持有的锁,并进入等待状态,直到其他线程调用signal()或signalAll()方法唤醒它。注意,调用await()方法前必须先获取锁。
  2. signal(): 当某个线程满足了某个条件,它可以调用signal()方法来通知等待该条件的线程中的一个线程,唤醒其中一个等待的线程。
  3. signalAll(): 与signal()类似,但signalAll()会唤醒等待该条件的所有线程。

生产者消费者模式示例:

下面通过一个生产者消费者模式的例子来演示如何使用Condition实现线程间的有效协作。在这个示例中,我们将使用ReentrantLock和Condition来实现生产者消费者问题。

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ProducerConsumerExample {
    
    private Lock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmpty = lock.newCondition();
    private Queue<Integer> queue = new LinkedList<>();
    private int maxSize = 5;

    public void produce() throws InterruptedException {
    
        lock.lock();
        try {
    
            while (queue.size() == maxSize) {
    
                notFull.await(); // 队列已满,等待非满条件
            }
            int num = (int) (Math.random() * 100);
            queue.offer(num);
            System.out.println("Produced: " + num);
            notEmpty.signal(); // 生产后通知非空条件
        } finally {
    
            lock.unlock();
        }
    }

    public void consume() throws InterruptedException {
    
        lock.lock();
        try {
    
            while (queue.isEmpty()) {
    
                notEmpty.await(); // 队列为空,等待非空条件
            }
            int num = queue.poll();
            System.out.println("Consumed: " + num);
            notFull.signal(); // 消费后通知非满条件
        } finally {
    
            lock.unlock();
        }
    }

    public static void main(String[] args) {
    
        ProducerConsumerExample example = new ProducerConsumerExample();

        Thread producerThread = new Thread(() -> {
    
            try {
    
                while (true) {
    
                    example.produce();
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
    
                e.printStackTrace();
            }
        });

        Thread consumerThread = new Thread(() -> {
    
            try {
    
                while (true) {
    
                    example.consume();
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
    
                e.printStackTrace();
            }
        });

        producerThread.start();
        consumerThread.start();
    }
}

在上面的代码中,我们使用ReentrantLock和Condition来创建一个生产者消费者模型。生产者负责向队列中添加元素,消费者负责从队列中取出元素。当队列满时,生产者会等待队列非满条件(notFull),而当队列为空时,消费者会等待队列非空条件(notEmpty)。每次生产者生产一个元素后,会通知消费者队列非空;每次消费者消费一个元素后,会通知生产者队列非满。

处理线程间的依赖关系:

Condition的引入使得我们可以更好地处理线程间的依赖关系。在生产者消费者模式中,生产者必须等待队列非满才能生产,而消费者必须等待队列非空才能消费。Condition允许我们将这些依赖关系明确地表达出来,使得线程在等待条件时进入等待状态,而不是忙等待,从而节省了CPU资源。同时,当满足条件时,线程可以被唤醒,继续执行相关的操作,实现线程间的有效协作。通过使用Condition,我们可以更加清晰地控制线程的执行顺序,避免了隐式的依赖关系导致的线程执行问题。

Condition的实现原理:

Condition的底层实现原理:

在Java JUC工具包中,Condition是通过AbstractQueuedSynchronizer(AQS)实现的。AQS是实现Lock接口的抽象基类,它提供了一种用于构建锁和同步器的框架,是ReentrantLock和Condition的底层基础。

Condition内部维护了一个条件队列,用于存放因等待某个条件而被阻塞的线程。每个Condition对象与一个条件队列相关联。当一个线程调用Condition的await()方法时,它会释放锁,并将自己加入到条件队列中等待。当其他线程调用Condition的signal()或signalAll()方法时,会从条件队列中唤醒等待的线程,使得这些线程可以继续执行。

Condition与Lock之间的关联:

Condition是Lock接口的一个补充,每个Condition对象都是通过Lock接口的newCondition()方法创建的。因此,每个Condition对象都与一个Lock相关联,用于等待和唤醒线程。

在使用Condition时,首先需要通过Lock接口的实现类(如ReentrantLock)创建一个Lock实例。然后,通过Lock实例调用newCondition()方法来创建一个Condition对象。这样就可以在多个线程之间实现条件等待和唤醒。

Java JUC工具包在实现Condition时的关键设计:

在Java JUC工具包实现Condition时,关键设计主要体现在AbstractQueuedSynchronizer(AQS)和ConditionObject类中。AQS是Condition的底层基础,而ConditionObject则是Condition的实际实现。

关键设计包括:

  1. 条件队列: Condition维护了一个条件队列,用于存放因等待某个条件而被阻塞的线程。条件队列的实现是基于AQS的等待队列。
  2. 等待和唤醒机制: 调用Condition的await()方法时,线程会释放锁,并进入条件队列等待。当其他线程调用Condition的signal()或signalAll()方法时,会从条件队列中唤醒等待的线程,使得这些线程可以继续执行。
  3. ConditionObject类: ConditionObject是Condition的具体实现类,它继承自AbstractQueuedSynchronizer。在ConditionObject类中,重写了await()、signal()和signalAll()等方法,实现了等待和唤醒的具体逻辑。
  4. 条件的状态管理: Condition在内部维护了等待条件是否满足的状态信息,以及哪些线程在等待条件。这样,当其他线程满足条件时,就可以唤醒等待的线程。

总的来说,Java JUC工具包在实现Condition时,利用了AQS的等待队列来实现条件等待和唤醒的机制,ConditionObject作为具体实现类,提供了等待和唤醒的具体逻辑。通过这样的设计,Condition实现了高级线程同步控制的功能,允许线程在等待某个条件满足时进入等待状态,并在其他线程满足条件后通知被唤醒。这种机制提供了更灵活和高级的线程协作方式,解决了传统线程同步机制无法满足的问题。

相关文章
|
7月前
|
Java
Java中ReentrantLock中 lock.lock(),加锁源码分析
Java中ReentrantLock中 lock.lock(),加锁源码分析
53 0
|
7月前
|
安全 Java API
JavaEE初阶 CAS,JUC的一些简单理解,包含concurrent, ReentrantLock,Semaphore以及ConcurrentHashMap
JavaEE初阶 CAS,JUC的一些简单理解,包含concurrent, ReentrantLock,Semaphore以及ConcurrentHashMap
52 0
|
6月前
|
Java
Java中的内置锁synchronized关键字和wait()、notifyAll()方法
【6月更文挑战第17天】Java的synchronized和wait/notify实现顺序打印ALI:共享volatile变量`count`,三个线程分别检查`count`值,匹配时打印并减1,未匹配时等待。每个`print`方法加锁,确保互斥访问。代码示例展示了线程同步机制。考虑异常处理及实际场景的扩展需求。
93 3
|
6月前
|
监控 安全 Java
Java中的锁(Lock、重入锁、读写锁、队列同步器、Condition)
Java中的锁(Lock、重入锁、读写锁、队列同步器、Condition)
37 0
|
安全 Java
JUC第八讲:Condition源码分析
JUC第八讲:Condition源码分析
|
7月前
|
存储 安全 算法
掌握Java并发编程:Lock、Condition与并发集合
掌握Java并发编程:Lock、Condition与并发集合
57 0
JUC并发编程:Condition的简单理解与使用
JUC并发编程:Condition的简单理解与使用
97 0
Juc并发编程08——Condition实现源码分析
看看ReentrantLock中的newCondition方法
Juc并发编程08——Condition实现源码分析
|
Java
既生 synchronized 何生 JUC 的 显式 locks ?
既生 synchronized 何生 JUC 的 显式 locks ?
116 0