跳至主要內容

JUC

Yaien Blog原创大约 10 分钟并发线程安全JUC

JUC

JUC 是 Java Util Concurrent(Java 并发工具包)的缩写,是 Java 提供的用于并发编程的工具包。Java 并发工具包位于 java.util.concurrent
包下,提供了一组强大的工具和类,用于简化并发编程、提高性能和可扩展性。

ReentrantLock

ReentrantLock 是 Java 并发工具包中提供的一个可重入锁类。它实现了 Lock 接口,提供了比使用 synchronized 关键字更灵活和可扩展的锁机制。

import java.util.concurrent.locks.ReentrantLock;

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

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            lock.lock(); // 获取锁
            try {
                System.out.println("Thread 1 acquired the lock");
                // 执行需要同步的操作
            } finally {
                lock.unlock(); // 释放锁
            }
        });

        Thread thread2 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("Thread 2 acquired the lock");
                // 执行需要同步的操作
            } finally {
                lock.unlock();
            }
        });

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

在上述示例中,ReentrantLock 被用来保护多个线程对共享资源的访问。通过调用 lock() 方法获取锁,在完成操作后使用 unlock() 方法释放锁,确保多个线程之间的同步和互斥。

特点

可重入性

ReentrantLock 是可重入锁,即同一个线程可以多次获取该锁而不会导致死锁。这意味着线程可以在持有锁的情况下多次进入同步代码块。

独占锁

ReentrantLock 是一种独占锁,同一时间只能有一个线程持有该锁。其他线程需要等待锁释放才能获取锁。

公平性

ReentrantLock 支持公平性和非公平性两种获取锁的策略。在公平模式下,等待时间较长的线程优先获取锁。

条件变量

ReentrantLock 提供了条件变量的支持,可以通过 newCondition() 方法创建一个条件变量,用于线程间的等待和通知。

锁的可中断性

ReentrantLock 支持可中断的获取锁操作。即在等待锁的过程中,可以通过 lockInterruptibly() 方法响应中断。

Lock

ReentrantLock 类实现了 Lock 接口的规范。Lock 接口是 Java 并发工具包中定义的一个接口,用于提供比 synchronized 关键字更灵活和可扩展的锁机制。

ReentrantLock 类实现了这些方法:

void lock()

获取锁,如果锁已被其他线程持有,则当前线程将被阻塞直到获取到锁。

void lockInterruptibly()

可中断地获取锁,如果锁已被其他线程持有,当前线程可以响应中断并退出等待。

boolean tryLock()

尝试获取锁,如果锁当前没有被其他线程持有,则获取锁成功并返回 true,否则立即返回 false。

boolean tryLock(long time, TimeUnit unit)

在指定的时间范围内尝试获取锁,如果在指定时间内获取到锁,则返回 true,否则返回 false。

void unlock()

释放锁,将锁的持有计数减一。如果当前线程是最后一个持有锁的线程,则释放锁并将其返回给其他等待线程。

公平锁和非公平锁

公平锁

    private static ReentrantLock lock = new ReentrantLock(true);

公平锁按照线程请求锁的顺序分配锁。当多个线程等待锁时,会按照线程等待锁的先后顺序来决定锁的获取顺序。

非公平锁

    private static ReentrantLock lock = new ReentrantLock();

非公平锁允许新请求的线程插队获取锁,即使有其他线程正在等待锁。新请求的线程有机会在等待线程之前获取锁。

优缺点

  1. 公平锁保证了锁的公平性,防止某些线程长时间等待锁而无法获取执行机会。非公平锁可能会导致某些线程长时间等待锁的情况,可能产生线程饥饿的问题。
  2. 相比非公平锁,公平锁的性能开销较大,因为需要维护一个等待队列来记录线程的等待顺序,而非公平锁线程可以尝试立即获取锁而不必等待。

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

Java中ReentrantLocksynchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。在实际开发中,可重入锁常常应用于递归操作、调用同一个类中的其他方法、锁嵌套等场景中。

import java.util.concurrent.locks.ReentrantLock;

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

    public static void main(String[] args) {
        try {
            lock.lock(); // 第一次获取锁
            System.out.println("First lock acquisition");

            // 在持有锁的情况下再次获取锁
            lock.lock(); // 第二次获取锁
            System.out.println("Second lock acquisition");
        } finally {
            lock.unlock(); // 释放锁
            System.out.println("First lock released");

            lock.unlock(); // 释放锁
            System.out.println("Second lock released");
        }
    }
}

在上述示例中,线程首次获取锁后,可以再次获取同一把锁而不会被阻塞。通过调用两次 lock.lock() 获取锁,然后通过 lock.unlock() 释放锁,实现了可重入锁的使用。

Condition

Condition 是 Java 并发工具包中的一个接口,它与 ReentrantLock 结合使用,提供了对线程等待和通知的支持。通过 Condition,可以实现更精细的线程间通信和同步。

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

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

    public static void main(String[] args) {
        Thread producerThread = new Thread(() -> {
            lock.lock();
            try {
                // 生产数据的过程
                while (!isDataReady) {
                    condition.await();
                }
                System.out.println("Producer: Data is consumed");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        Thread consumerThread = new Thread(() -> {
            lock.lock();
            try {
                // 消费数据的过程
                System.out.println("Consumer: Data is produced");
                isDataReady = true;
                condition.signal();
            } finally {
                lock.unlock();
            }
        });

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

在上述示例中,producerThread 线程负责生产数据,consumerThread 线程负责消费数据。通过 lock 和 condition 实现了线程间的等待和通知机制。当数据还未准备好时,生产者线程调用 condition.await() 进入等待状态,直到被消费者线程唤醒。当数据准备好后,消费者线程调用 condition.signal() 唤醒等待的生产者线程。

Condition 接口定义了以下几个主要方法:

void await()

当前线程释放锁,并等待直到被其他线程调用该 Condition 的 signal() 或 signalAll() 方法唤醒。在等待期间,当前线程会被阻塞。

void awaitUninterruptibly()

与 await() 方法类似,但是在等待期间不会响应中断。

long awaitNanos(long nanosTimeout)

当前线程等待一段时间,直到被唤醒或等待时间超时。超时时间以纳秒为单位,返回值表示剩余的等待时间。

boolean await(long time, TimeUnit unit)

当前线程等待一段时间,直到被唤醒、等待时间超时或被中断。返回值表示是否在等待时间内被唤醒。

boolean awaitUntil(Date deadline)

当前线程等待直到指定的时间点,直到被唤醒、等待时间超时或被中断。返回值表示是否在等待时间内被唤醒。

void signal()

唤醒一个等待在该 Condition 上的线程,使其从等待状态返回。

void signalAll()

唤醒所有等待在该 Condition 上的线程,使它们从等待状态返回。

需注意的问题

正确释放锁

使用 ReentrantLock 获取锁后,必须确保在适当的时候释放锁,否则会导致死锁的问题。通常使用 try-finally 块来确保在获取锁后一定会释放锁。

lock.lock();
try {
    // 执行需要同步的操作
} finally {
    lock.unlock();
}

避免重复获取锁

ReentrantLock 是可重入锁,同一个线程可以多次获取同一把锁。但要注意避免重复获取锁,确保每次获取锁都会有相应的释放操作。否则,如果在获取锁后没有正确释放锁,可能导致其他线程无法获取锁而发生死锁。

公平性和非公平性选择

ReentrantLock 支持公平性和非公平性的获取锁策略。默认情况下,它是非公平的。如果需要公平性,可以在创建 ReentrantLock 实例时指定 true,即 new ReentrantLock(true)
。公平锁会按照线程的请求顺序分配锁,而非公平锁则允许新请求的线程插队获取锁。根据实际情况选择合适的获取锁策略。

使用条件变量

ReentrantLock 提供了条件变量(Condition)的支持,可以通过 newCondition() 方法创建条件变量。在使用条件变量时,需要注意正确地使用 await()、signal() 和 signalAll()
方法,避免出现线程永久等待或信号丢失的问题。

性能和开销

相比于使用 synchronized 关键字,ReentrantLock 可能会带来额外的性能开销。因此,只有在需要更高级的特性时才使用 ReentrantLock,对于简单的同步需求,使用 synchronized 可能更加方便和高效。

Semaphore

Semaphore(信号量)是 Java 并发工具包中的一个类,用于控制同时访问某个资源的线程数量。它可以用来限制同时执行某个任务的线程数量,或者限制同时访问某个共享资源的线程数量。

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    private static final int THREAD_COUNT = 5;
    private static Semaphore semaphore = new Semaphore(2); // 设置信号量数量为2

    public static void main(String[] args) {
        for (int i = 0; i < THREAD_COUNT; i++) {
            Thread thread = new Thread(() -> {
                try {
                    semaphore.acquire(); // 获取许可
                    System.out.println("Thread acquired a permit");
                    // 执行需要同步的操作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release(); // 释放许可
                    System.out.println("Thread released a permit");
                }
            });
            thread.start();
        }
    }
}

在上述示例中,有5个线程同时尝试获取信号量的许可。由于信号量的数量限制为2,所以只有前两个线程能够成功获取许可,而其他线程需要等待。获取许可后,线程执行需要同步的操作,并在完成后释放许可。

应用场景

  • 限流控制

Semaphore 可以用于限制对某个资源的并发访问数量,例如限制同时访问某个接口或数据库连接的线程数量。

  • 资源池管理

Semaphore 可以用于管理资源池,例如连接池、对象池等。通过设置信号量的数量,可以限制对资源的同时访问数量,避免资源过度占用。

CountDownLatch

CountDownLatch用于控制线程等待其他线程完成一组操作后再继续执行。

CountDownLatch 维护了一个计数器,通过指定计数器的初始值,可以控制需要等待的操作数量。当一个或多个线程调用 countDown() 方法时,计数器的值减少;当计数器的值变为零时,所有等待的线程都会被释放,可以继续执行。

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    private static final int THREAD_COUNT = 5;
    private static CountDownLatch latch = new CountDownLatch(THREAD_COUNT);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < THREAD_COUNT; i++) {
            Thread thread = new Thread(() -> {
                try {
                    // 模拟线程执行任务
                    Thread.sleep(1000);
                    System.out.println("Thread completed its task");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown(); // 操作完成,计数器减一
                }
            });
            thread.start();
        }

        latch.await(); // 等待计数器变为零
        System.out.println("All threads have completed their tasks");
    }
}

在上述示例中,有5个线程执行任务,并在任务完成后调用 countDown() 方法减少计数器的值。主线程通过调用 await() 方法等待计数器的值变为零,以确保所有线程的任务都完成后再继续执行。

CountDownLatch 的主要方法包括:

CountDownLatch(int count)

创建一个指定初始计数值的 CountDownLatch 对象。

void countDown()

计数器减一,表示一个操作已完成。

void await()

线程等待,直到计数器的值变为零。如果计数器的值已经是零,那么该方法会立即返回。

boolean await(long timeout, TimeUnit unit)

在指定的时间范围内等待,直到计数器的值变为零,或等待时间超时。返回值表示是否在等待时间内计数器变为零。

使用场景

  • 并行任务同步

CountDownLatch可以用于协调多个并行任务的完成情况,确保所有任务都完成后再继续执行下一步操作。

  • 多任务汇总

CountDownLatch可以用于统计多个线程的完成情况,以确定所有线程都已完成工作。

  • 资源初始化

CountDownLatch可以用于等待资源的初始化完成,以便在资源初始化完成后开始使用。

上次编辑于:
贡献者: yanggl