5. JUC并发工具类

116 阅读17分钟

ReentrantLock

ReentrantLock是一种可重入的独占锁,它允许同一个线程多次获取同一个锁而不被阻塞。它的功能类似于synchronized是一种互斥锁,可以保证线程安全。相对于synchronized,ReentrangLock具备以下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量
  • 与synchronized一样,都可支持重入。

ReentrantLock 主要应用于需要更精细控制并发访问、支持可中断锁等待、有超时获取锁需求以及条件变量协调的多线程场景中,以提高程序的并发效率和灵活性。

常用api

Lock接口

ReentrantLock实现了Lock接口规范,常见API如下:

方法名描述
void lock()获取锁,如果锁不可用,线程将阻塞到锁可用为止
void lockInterruptibly()获取锁,但允许线程在等待锁的过程中被中断,抛出InterrupedException
boolean tryLock()尝试非阻塞的获取锁,如果可用返回 true,否则返回false
boolean tryLock(long time, TimeUnit unit)尝试获取锁,如果在指定时间内未获取到则返回false,可相应中断。
void unlock()释放当前线程持有的锁。必须在持有锁的同一线程中调用。
Condition newCondition()创建与锁绑定的条件变量,用于线程间的进一步同步。
int getHoldCount()返回当前线程保持此锁的次数。
boolean isHeldByCurrentThread()查询当前线程是否保持此锁。
boolean isLocked()查询锁是否被任意线程持有。

使用时要注意的问题

  1. 默认情况下ReentrantLock为非公平锁而非公平锁。
  2. 加锁次数和释放锁的次数一定要保持一致,否则会导致现成阻塞或异常。
  3. 加锁操作一定要放在try代码之前,这样可以避免未加锁成功而又释放锁的异常。
  4. 释放锁一定要放在finally中,否则会导致线程阻塞。

ReentrantLock的使用

方法示例

lock()

private static Lock lock = new ReentrantLock();

private static int sum = 0;

public static void main(String[] args) {
    Thread thread = new Thread(() -> {
        //获取锁
        lock.lock();
        try {
            //执行临界区代码
            sum++;
        } catch (Exception e) {

        } finally {
            //释放锁
            lock.unlock();
        }
    });
    thread.start();
}

lock.lockInterruptibly();

public static void main(String[] args) {
    Lock lock = new ReentrantLock();
    Thread t = new Thread(() -> {
        try {
            lock.lockInterruptibly();
            try {

            } finally {
                lock.unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            log.debug("等锁的过程中被中断");
        }

    });
    t.start();
}

lock.tryLock()

public static void main(String[] args) {
    Lock lock = new ReentrantLock();
    Thread t = new Thread(() -> {
        try {
            if (!lock.tryLock(1, TimeUnit.SECONDS)) {
               return;
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        try {
            log.debug("t1获得了锁");
        } finally {
            lock.unlock();
        }

    },"t1");
    t.start();
}

公平锁和非公平锁

ReentrantLock支持公平锁和非公平锁两种模式:

  • 公平锁:线程在获取锁时,按照等待的先后顺序获取锁。
  • 非公平锁:线程在获取锁时,不按等待的先后顺序获取锁,而是随机获取锁。ReentrantLock默认是非公平锁
//ReentrantLock lock = new ReentrantLock(true); //公平锁
ReentrantLock lock = new ReentrantLock(); //非公平锁

可重入锁

可重入锁又名递归锁,是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。Java中的ReentrantLock和synchronized都是可重入锁。可重入锁的一个优点是可一定程度避免死锁,在该锁的保护区内再次请求这个锁时,会成功获取并增加锁的计数器,在实际开发中,可重入锁常常用于递归操作、调用同一个类中的其他方法、锁嵌套等场景中

Condition

java.util.concurrent类库中提供的Condition类来实现线程之间的协调。调用Condition.await()方法使线程等待,其他线程调用Condition.signal()或Condition.signalAll()方法唤醒等待的线程。调用Condition的await()方法和signal()方法,都必须在lock保护之内

public class ReentrantLockDemo6 {
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition cigCon = lock.newCondition();
    private static Condition takeCon = lock.newCondition();

    private static boolean hashcig = false;
    private static boolean hastakeout = false;

    //工作
    public void cigratee(){
        lock.lock();
        try {
            while(!hashcig){
                try {
                    log.debug("没有工作,歇一会");
                    //当一个线程调用 condition.await() 方法时,它会首先释放当前持有的锁,然后使该线程进入等待状态,
                    // 直到其他线程调用同一条件变量的 signal() 或 signalAll() 方法来唤醒它。
                    // 此时,被唤醒的线程将尝试重新获取锁(如果可用的话),一旦成功获取锁,将继续执行 await() 方法之后的代码。
                    cigCon.await();

                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            log.debug("干活");
        }finally {
            lock.unlock();
        }
    }

    //送饭
    public void takeout(){
        lock.lock();
        try {
            while(!hastakeout){
                try {
                    log.debug("没有饭,等一会");
                    takeCon.await();

                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            log.debug("有饭了,吃饭");
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo6 test = new ReentrantLockDemo6();
        new Thread(() ->{
            test.cigratee();
        }).start();

        new Thread(() -> {
            test.takeout();
        }).start();

        new Thread(() ->{
            lock.lock();
            try {
                hashcig = true;
                log.debug("唤醒工作的等待线程");
                cigCon.signal();
            }finally {
                lock.unlock();
            }


        },"t1").start();

        new Thread(() ->{
            lock.lock();
            try {
                hastakeout = true;
                log.debug("唤醒送饭的等待线程");
                takeCon.signal();
            }finally {
                lock.unlock();
            }


        },"t2").start();
    }

}

应用场景

  1. 解决多线程竞争资源的问题,例如多个线程同时对一数据进行读写操作,可以使用ReentrantLock保证每次只有一个线程写入。
  2. 实现多线程任务的顺序执行,例如在一个线程执行完某个任务后,再让另一个线程执行任务
  3. 实现多线程等待/通知机制,例如在某个线程执行完某个任务后,通知其他线程继续执行。

既然有了synchronized为什么还要有ReentrangLock

尽管 synchronized 关键字为Java提供了基本的线程同步机制,但 ReentrantLock 类作为 java.util.concurrent.locks 包的一部分,提供了更高级和灵活的功能,以下是几点原因说明为何需要 ReentrantLock

  1. 可中断的等待synchronized 块中的等待是不可中断的,而 ReentrantLock 提供了 lockInterruptibly() 方法,允许等待锁的线程被其他线程中断,提高了响应性和程序的灵活性。
  2. 尝试锁定与超时ReentrantLock 提供了 tryLock() 方法尝试非阻塞地获取锁,以及 tryLock(long time, TimeUnit unit) 方法尝试在一定时间内获取锁,如果无法获取则超时返回,这在某些需要避免长时间阻塞的场景下非常有用。
  3. 公平性选择ReentrantLock 允许开发者选择锁的公平性策略。公平锁会按照线程等待的先后顺序分配锁,而非公平锁则允许插队,可能提高吞吐量但牺牲了一定的公平性。synchronized 默认采用非公平锁,无法调整。
  4. 条件变量ReentrantLock 配合 Condition 对象可以提供更细粒度的线程同步控制,比 synchronized 中的 wait()notify/notifyAll() 更加灵活和强大,允许为不同的条件维护独立的等待集。
  5. 锁的持有计数ReentrantLock 是可重入的,和 synchronized 一样,但它还提供了 getHoldCount() 方法来检查锁被当前线程持有的次数,这对于调试和复杂的同步逻辑是有帮助的。
  6. 更好的性能: 在某些场景下,尤其是在高竞争环境下,ReentrantLock 可能提供比 synchronized 更好的性能,因为JVM对锁的优化和实现细节不断进步,尤其是在Java 6及以后的版本中。

总的来说,虽然 synchronized 更简洁易用,但在需要更高级特性时,ReentrantLock 提供了更多选择和控制权,以适应更复杂多变的并发需求。

既然有了wait()notify()为什么还要有Condition

尽管 wait(), notify(), 和 notifyAll() 方法可以实现线程之间的基本通信,但是它们与特定的对象监视器(锁)紧密绑定,使用时相对局限,且不易于管理复杂的线程同步逻辑。相比之下,Condition 接口(通过 ReentrantLock 或其它锁类的 newCondition() 方法获得)提供了更加灵活和强大的线程协调机制,以下是几个关键点说明为什么需要 Condition

  1. 多个等待条件: 在复杂应用中,一个对象可能需要基于不同条件来通知不同的等待线程。Condition 允许多个等待集,每个条件有自己的等待和通知机制,而 wait()notify() 只能为对象维护单一的等待集。
  2. 精确通知notify() 方法随机唤醒一个等待线程,而 notifyAll() 唤醒所有等待线程,它们都不够精确。使用 Condition,你可以精确控制哪些线程被唤醒,通过 signal() 唤醒单个线程或 signalAll() 唤醒所有等待该条件的线程。
  3. 避免虚假唤醒: 使用 wait()notify() 时,需要在循环中检查条件以防止虚假唤醒(spurious wakeups)。而 Conditionawait() 方法在内部已经处理了虚假唤醒的问题,使得代码更简洁、易于理解和维护。
  4. 与锁分离Condition 对象与锁(如 ReentrantLock)解耦,可以在不同的锁上创建多个条件变量,增加了设计的灵活性和模块化程度,使得锁和条件逻辑可以独立管理。
  5. 更清晰的API设计Condition 的 API 设计(如 await(), signal(), signalAll())更明确地表达了等待和通知的意图,相比 wait()notify() 更符合面向对象的设计原则,提高了代码的可读性和可维护性。

综上所述,虽然 wait()notify()notifyAll() 在简单的同步场景下足够使用,但当涉及到更复杂的线程间协调和通信时,Condition 接口提供的精细控制和高级功能使其成为更优的选择。

Semaphore

Semaphore 是 Java 并发编程中一个重要的同步工具类,位于 java.util.concurrent 包中,它基于计数信号量的概念,用于控制同时访问特定资源或执行某个操作的线程数量。Semaphore 可以看作是一种资源计数器,它维护了一个许可集合,线程通过调用 acquire() 方法获取许可(减小许可数量),执行完毕后通过 release() 方法归还许可(增加许可数量)。这种机制有效地实现了对并发访问的控制,特别是在资源有限的场景下,比如数据库连接池、线程池的大小限制、文件读写等。

常用api

方法名描述
Semaphore(int permits)构造函数,创建具有指定许可数量的 Semaphore,默认非公平策略。
Semaphore(int permits, boolean fair)构造函数,创建具有指定许可数量和公平性的 Semaphore。true 表示公平策略。
void acquire()获取一个许可,如果无可用许可则阻塞当前线程。
boolean tryAcquire()尝试非阻塞地获取一个许可,立即返回是否成功。
boolean tryAcquire(long timeout, TimeUnit unit)尝试获取许可,等待指定时间后返回是否成功。
int availablePermits()返回当前可用的许可数量。
int drainPermits()立即减少并返回当前可用的所有许可数量。

Semaphore的使用

模拟停车场

public class ParkingLotDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(5); // 停车场有5个车位

        for (int i = 0; i < 10; i++) { // 10辆车尝试停车
            new Thread(() -> {
                try {
                    semaphore.acquire(); // 尝试获取车位
                    System.out.println(Thread.currentThread().getName() + " 停车成功");
                    TimeUnit.SECONDS.sleep(2); // 模拟停车时间
                    System.out.println(Thread.currentThread().getName() + " 离开");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release(); // 释放车位
                }
            }, "Car " + i).start();
        }
    }
}

模拟数据库连接池

public class DatabaseConnectionPool {
    /**
     * 控制并发访问的信号量
     */
    private final Semaphore semaphore;

    /**
     * 连接池中初始连接数
     */
    private int connectionCount;

    public DatabaseConnectionPool(int maxConnections) {
        this.semaphore = new Semaphore(maxConnections);
        this.connectionCount = maxConnections;
    }

    public void getConnection() {
        try {
            //尝试获取许可
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + "获取数据库连接");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取连接时被中断", e);
        }
    }

    public void releaseConnection() {
        //释放许可
        semaphore.release();
        System.out.println(Thread.currentThread().getName() + "释放数据库连接");
    }

    public static void main(String[] args) {
        //包含5个连接的池
        DatabaseConnectionPool databaseConnectionPool = new DatabaseConnectionPool(5);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            int taskId = i;
            executorService.execute(() -> {
                //请求连接
                databaseConnectionPool.getConnection();
                try {
                    //模拟数据库耗时
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName() + " 完成数据库操作" + "TaskId" + taskId);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    databaseConnectionPool.releaseConnection();
                    System.out.println(Thread.currentThread().getName() + " 释放连接" + "TaskId" + taskId);
                }
            });
        }
    }
}

应用场景

  • 资源池管理:如数据库连接池、线程池,限制同时访问资源的最大数量。
  • 限流:在高并发系统中,作为限流器,控制同时访问服务的请求量,防止系统过载。
  • 生产者消费者模型:控制生产和消费的速度,保证队列的稳定。
  • 互斥访问控制:虽然 synchronized 和 ReentrantLock 更常用作互斥锁,但在某些特定场景下,Semaphore 也可以用于控制对共享资源的并发访问,尤其是当需要控制并发度而不是简单的互斥时。

CountDownLatch

CountDownLatch(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集。 CountDownLatch使用给定的计数值(count)初始化。await方法会阻塞直到当前的计数值(count),由于countDown方法的调用达到0,count为0之后所有等待的线程都会被释放,并且随后对await方法的调用都会立即返回。count不会被重置。

常用api

方法名描述
CountDownLatch(int count)构造方法,初始化一个CountDownLatch实例,计数器设为count
void await()使当前线程等待,直到计数器达到零。若当前计数不为零,则阻塞。
boolean await(long timeout, TimeUnit unit)类似于await(),但增加超时限制,超时后返回false
void countDown()递减计数器的值。如果新的计数为零,则释放所有等待的线程。
long getCount()返回当前计数器的值。

CountDownLatch的使用

模拟多任务完成后合并汇总

public static void main(String[] args) {
    CountDownLatch countDownLatch = new CountDownLatch(5);

    for (int i = 0; i < 5; i++) {
        final int index = i;
        new Thread(() -> {
            try {
                Thread.sleep(1000+ ThreadLocalRandom.current().nextInt(2000));
                System.out.println("任务"+index+"执行完成");
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
    }

    //主线程阻塞,当计数器为0,唤醒主线程往下执行
    try {
        countDownLatch.await();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    System.out.println("主线程执行完毕");
}

应用场景

  1. 并行任务同步:CountDownLatch可以用于协调多个并行任务的完成情况,确保所有任务都完成后再执行下一步操作。
  2. 多任务汇总:CountDownLatch可以用于统计多个线程的完成情况,以确定所有线程都已完成工作。
  3. 资源初始化:CountDownLatch可以用于等待资源的初始化完成,以便在资源初始化完成后开始使用

CycliBarrier

CycliBarrier(回环栅栏或循环屏障),是java并发库中的一种同步工具,通过它可以实现让一组线程等待至某个状态(屏障点)之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,Cyclibarrier可以被重用

常用api

方法名描述
CyclicBarrier(int parties)构造方法,创建一个新的 CyclicBarrier 实例,parties 表示需要等待的线程数。
CyclicBarrier(int parties, Runnable barrierAction)构造方法,额外接受一个 Runnable,在每次所有线程到达屏障后执行。
int getParties()返回需要等待的参与者数目。
int getNumberWaiting()返回当前在屏障处等待的参与者数目。
boolean isBroken()查询屏障是否处于损坏状态。
void reset()重置屏障至初始状态。如果当前有线程在等待,则抛出异常。
void await()使当前线程在屏障处等待,直到所有参与者都到达屏障或超时/被中断。
long await(long timeout, TimeUnit unit)类似于 await(),但增加超时限制。如果在指定时间内未达到屏障,则返回false

CyclicBarrier的使用

public static void main(String[] args) {

    CyclicBarrier cyclicBarrier = new CyclicBarrier(3);

    for (int i = 0; i < 5; i++) {
        new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()
                            + "开始等待其他线程");
                    // 阻塞直到指定的线程都调用此方法,继续执行
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread().getName() + "开始执行");
                    //TODO 模拟业务处理
                    Thread.sleep(5000);
                    System.out.println(Thread.currentThread().getName() + "执行完毕");

                } catch (Exception e) {
                    e.printStackTrace();
                }
        }).start();

    }

}

应用场景

  1. 多线程任务:Cyclibarrier可以用于将复杂的任务分配给多个线程执行,并在所有线程完成工作后触发后续操作。
  2. 数据处理:Cyclibarrier可以用于协调多个线程间的数据处理,在所有线程处理完数据后触发后续操作。

Cyclibarrier与CountDownLatch区别

  • CountDownLatch是一次性的,Cyclibarrier是循环可用的
  • CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。

Exchanger

Exchanger 是 Java 并发工具类之一,用于两个线程之间交换数据。它提供了一个同步点,在这个点上两个线程可以相遇并交换它们携带的对象。这对于需要两个线程协作完成任务,尤其是在交替处理数据或结果的场景中非常有用。

常用api

方法名描述
Exchanger构造方法,创建一个新的 Exchanger 实例。
V exchange(V x)交换数据的方法。线程调用此方法时会被阻塞,直到另一个线程也调用了此方法,然后两个线程交换它们提供的对象(x)。
V exchange(V x, long timeout, TimeUnit unit)带超时的交换数据方法。如果在指定时间内没有其他线程来进行交换,则该调用将返回 null(如果当前线程被中断,则抛出 InterruptedException)。

Exchanger的使用

   
private static final Exchanger<String> exgr = new Exchanger<String>();
private static ExecutorService threadPool = Executors.newFixedThreadPool(2);

public static void main(String[] args) {
    threadPool.execute(new Runnable() {
        @Override
        public void run() {
            try {
                String A = "银行流水A";// A录入银行流水数据
                exgr.exchange(A);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    threadPool.execute(new Runnable() {

        @Override
        public void run() {
            try {
                String B = "银行流水B";// B录入银行流水数据
                String A = exgr.exchange(B);
                System.out.println("A和B数据是否一致:" + A.equals(B) +
                        ",A录入的是:"+ A + ",B录入是:" + B);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    threadPool.shutdown();
}

应用场景

Exchanger可以用于各种应用场景,具体取决于Exchanger的实现。常见的场景包括:
1. 数据交换:在多线程环境中,两个线程可以通过Exchanger进行数据交换。
2. 数据采集:在数据采集系统中,可以使用Exchanger在采集线程和处理线程间进行数据交换。

Phaser

Phaser(阶段协同器)是一个Java实现的并发工具类,用于协调多个线程的执行。它提供了一些方便的方法来管理多个阶段的执行,可以让程序员灵活地控制线程的执行顺序和阶段性的执行。Phaser可以被视为CyclicBarrier和CountDownLatch的进化版,它能够自适应地调整并发线程数,可以动态地增加或减少参与线程的数量。所以Phaser特别适合使用在重复执行或者重用的情况.

常用api

方法名描述
Phaser()构造一个新的 Phaser 实例,没有注册任何参与者,没有父节点,初始化阶段值为 0。
Phaser(int parties)构造一个新的 Phaser 实例,初始注册指定数量的参与者。
Phaser(Phaser parent)构造一个新的 Phaser 实例,并指定其父节点,形成层次化同步结构。
int register()注册一个新参与者到此 Phaser,返回此参与者注册后的阶段编号。
int bulkRegister(int parties)批量注册多个参与者,返回最后注册的参与者的阶段编号。
boolean arriveAndAwaitAdvance()表示当前参与者到达屏障,如果所有参与者都到达,则推进到下一个阶段并返回 true;否则返回 false
boolean arrive()表示当前参与者到达屏障,但不等待其他参与者,仅返回当前阶段号。
int arriveAndDeregister()表示当前参与者到达屏障,等待其他参与者,阶段推进后注销当前参与者,并返回新阶段号。
int getPhase()返回当前阶段号。
int getRegisteredParties()返回当前注册的参与者数量。
boolean isTerminated()检查此 Phaser 是否终止,即没有活动参与者且阶段已结束。
void advance()强制推进到下一个阶段,即使没有参与者到达。

Phaser的使用

// 创建一个Phaser实例,初始parties为3,表示初始时有3个参与者
   Phaser phaser = new Phaser(3);

   // 注册额外的参与者
   phaser.register();

   // 使用Lambda表达式创建并启动线程
   IntStream.range(0, 4).forEach(i -> {
       new Thread(() -> {
           String threadName = Thread.currentThread().getName();
           System.out.println(threadName + "到达阶段1");
           // 到达阶段1,等待其他线程
           phaser.arriveAndAwaitAdvance();

           // 模拟执行任务
           int taskDuration = ThreadLocalRandom.current().nextInt(1000);
           try {
               Thread.sleep(taskDuration);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }

           System.out.println(threadName + "完成任务,到达阶段2");
           // 完成任务后,到达阶段2,再次等待其他线程
           phaser.arriveAndAwaitAdvance();

           // 最后阶段,到达阶段3
           System.out.println(threadName + "到达最终阶段");
           phaser.arriveAndDeregister(); // 到达并注销自己
       }, "Thread-" + i).start();
   });

   // 主线程等待所有参与者完成最后阶段
   while (phaser.getRegisteredParties() > 0) {
       phaser.arriveAndAwaitAdvance(); // 主线程也需要推进阶段
   }

   System.out.println("所有任务完成,程序结束。");
}

应用场景

  1. 多线程任务分配:用于将复杂的任务分配给多个线程执行,并协调线程间的合作。
  2. 多级任务分配:用于实现多级任务流程,在每一级任务完成后触发下一级任务的开始
  3. 模拟并行计算:用于模拟并行运算,协调多个线程间的工作。
  4. 可以用于实现阶段性任务,在每一阶段任务完成后触发下一阶段任务的开始。