一天一个 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实现了高级线程同步控制的功能,允许线程在等待某个条件满足时进入等待状态,并在其他线程满足条件后通知被唤醒。这种机制提供了更灵活和高级的线程协作方式,解决了传统线程同步机制无法满足的问题。

相关文章
|
6月前
|
Java
Java中ReentrantLock中 lock.lock(),加锁源码分析
Java中ReentrantLock中 lock.lock(),加锁源码分析
45 0
|
6月前
|
安全 Java API
JavaEE初阶 CAS,JUC的一些简单理解,包含concurrent, ReentrantLock,Semaphore以及ConcurrentHashMap
JavaEE初阶 CAS,JUC的一些简单理解,包含concurrent, ReentrantLock,Semaphore以及ConcurrentHashMap
48 0
|
2月前
|
Java 数据库
JUC工具类: Semaphore详解
信号量Semaphore是并发编程中的一种高级同步机制,它可以在复杂的资源共享场景中发挥重要作用。理解它的工作原理及正确的使用方法对实现高效且健壮的并发控制至关重要。
38 1
|
5月前
|
监控 安全 Java
Java中的锁(Lock、重入锁、读写锁、队列同步器、Condition)
Java中的锁(Lock、重入锁、读写锁、队列同步器、Condition)
29 0
|
6月前
|
存储 安全 算法
掌握Java并发编程:Lock、Condition与并发集合
掌握Java并发编程:Lock、Condition与并发集合
53 0
|
安全 Java
JUC第八讲:Condition源码分析
JUC第八讲:Condition源码分析
JUC并发编程:Condition的简单理解与使用
JUC并发编程:Condition的简单理解与使用
87 0
|
Java
既生 synchronized 何生 JUC 的 显式 locks ?
既生 synchronized 何生 JUC 的 显式 locks ?
110 0
|
Java
Callable,Lock,Condition,ReadWriteLock
Callable,Lock,Condition,ReadWriteLock
55 0