并发编程(六)

1.ReentrantLock、Semaphore、CountDownLatch 等常用的并发工具类的用法?

常用的工具类贴图:

常用的并发工具类有很多如上图所示,我们挑最常用的三种工具类来说

ReentrantLock:

ReentrantLock是一种可重入的独占锁,它允许同一个线程多次获取同一个锁而不会被阻塞;它的功能类似于Synchronized 是一种互斥锁,可以保证线程安全

基本语法:
//加锁 阻塞
 lock.lock();
 try {
 ...
 } finally {
 // 解锁
 lock.unlock();
 }

 //尝试加锁 非阻塞
 if (lock.tryLock(1, TimeUnit.SECONDS)) {
 try {
 ...
 } finally {
 lock.unlock();
 }
 }

在使用 ReentrantLock 时要注意以下 4 个问题:

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

代码示例:

import java.util.concurrent.locks.ReentrantLock;

/**
 * 模拟抢票场景
 */
public class ReentrantLockDemo {

    private final ReentrantLock lock = new ReentrantLock();//默认非公平
    private static int tickets = 8; // 总票数

    public void buyTicket() {
        lock.lock(); // 获取锁
        try {
            if (tickets > 0) { // 还有票    读
                try {
                    Thread.sleep(10); // 休眠10ms
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "购买了第" + tickets-- + "张票"); //写
            } else {
                System.out.println("票已经卖完了," + Thread.currentThread().getName() + "抢票失败");
            }

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


    public static void main(String[] args) {
        ReentrantLockDemo ticketSystem = new ReentrantLockDemo();
        for (int i = 1; i <= 10; i++) {
            Thread thread = new Thread(() -> {
                ticketSystem.buyTicket(); // 抢票

            }, "线程" + i);
            // 启动线程
            thread.start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("剩余票数:" + tickets);
    }
}

上述就是很简单的抢票代码的实现,也ReentrantLock的基本用法,对比synchronized 我们可以手动控制加锁和解锁十分可控,这是我们推荐使用ReentrantLock的一个重要原因!

公平锁和非公平锁:

公平锁:线程在获取锁时,按照等待的先后顺序获取锁。

非公平锁:线程在获取锁时,不按照等待的先后顺序获取锁,而是随机获取锁。ReentrantLock默认是非公平锁

 

 我们针对上图所画的抢票场景就可以很好的理解公平锁和非公平锁,D在过来排队时,如果是允许它先插一脚那么就是非公平如果是直接排在C后面那就是公平,其实源码中就是先做一次获取锁的CAS的操作,详情可见后续篇章。

我们利用ReentrantLock 创建也比较简单:

ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁

ReentrantLock lock = new ReentrantLock(true); //公平锁
 可重入锁:

可重入锁是指同一个线程可以多次获得同一个锁,而不会发生死锁。在线程持有锁的情况下,可以再次请求获取同一个锁,而这个请求是允许的,不会被阻塞。这种机制能够避免线程由于自身持有锁而陷入无限等待的情况。

例如,一个线程在执行某个方法时获得了锁,但在这个方法内部又调用了另一个同步方法,而该方法也需要获得同一个锁。这种情况下,由于锁是可重入的,线程不会被阻塞,而是继续执行,因为线程已经拥有了该锁的所有权。

Java中的synchronized关键字和ReentrantLock都是可重入锁。这种特性可以简化代码逻辑,但需要注意在设计时避免造成不必要的锁竞争和死锁。

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo2 {

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter(); // 创建计数器对象

        // 测试递归调用
        counter.recursiveCall(10);
    }


}

class Counter {
    private final ReentrantLock lock = new ReentrantLock(); // 创建 ReentrantLock 对象

    public void recursiveCall(int num) {
        lock.lock(); // 获取锁
        try {
            if (num == 0) {
                return;
            }
            System.out.println("执行递归,num = " + num);
            recursiveCall(num - 1);
        } finally {
            lock.unlock(); // 释放锁
        }
    }
}

上述代码就是利用可重入锁进行的一个递归调用,发现不会出现死锁问题且递减成功

结合condition实现生产者消费者模式:
java.util.concurrent类库中提供Condition类来实现线程之间的协调。调用Condition.await() 方法使
线程等待,其他线程调用Condition.signal() 或 Condition.signalAll() 方法唤醒等待的线程。
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 static final int CAPACITY = 5;
    private final Queue<Integer> queue = new LinkedList<>();
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    class Producer implements Runnable {
        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    while (queue.size() == CAPACITY) {
                        notFull.await(); // 等待队列不满 队列满了就不生产了
                    }
                    int item = (int) (Math.random() * 100);
                    queue.add(item);
                    System.out.println("Produced: " + item);
                    Thread.sleep(2000);
                    notEmpty.signal(); // 通知消费者队列不为空
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    class Consumer implements Runnable {
        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    while (queue.isEmpty()) {
                        notEmpty.await(); // 等待队列不空
                    }
                    int item = queue.poll();
                    System.out.println("Consumed: " + item);
                    Thread.sleep(1000);
                    notFull.signal(); // 通知生产者队列不满
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    public static void main(String[] args) {
        ProducerConsumerExample example = new ProducerConsumerExample();
        Thread producerThread = new Thread(example.new Producer());
        Thread consumerThread = new Thread(example.new Consumer());

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

上面的代码就是生产者消费者的简单实现,运行时可能出现在生产者全部生产之后才会消费,即便已经唤醒了消费者,这是因为线程的唤醒只是让线程处于一个就绪态,并不一定会立刻执行,需要等到真正获取到时间片才会执行,还有一点就是对于lock ,这里的lock是为了防止多线程生产和消费影起的并发问题,上述代码虽然只有一个线程执行生产消费,但是使用锁也可以提供一种一致的编程模型,使得代码更加清晰可读,而且可以为以后的扩展做好准备。

ReentrantLock具体应用场景如下:
  • 解决多线程竞争资源的问题,例如多个线程同时对同一个数据库进行写操作,可以使用 ReentrantLock 保证每次只有一个线程能够写入。
  • 实现多线程任务的顺序执行,例如在一个线程执行完某个任务后,再让另一个线程执行任务。
  • 实现多线程等待/通知机制,例如在某个线程执行完某个任务后,通知其他线程继续执行任务。
Semaphore:

Semaphore(信号量)是一种并发控制机制,用于控制对共享资源的访问。它可以用来限制同时访问某个资源的线程数量,从而避免资源的过度竞争或滥用。Semaphore维护一个许可(permit)的数量,线程可以通过获取许可来访问资源,当许可数量达到上限时,其他线程需要等待,直到有许可被释放。

Semaphore可以被用于多种场景,例如控制线程的并发数量、实现资源池等。它提供了两个主要的操作:acquire()release()acquire() 用于获取一个许可,如果没有可用的许可,则线程会被阻塞,直到有许可可用。release() 用于释放一个许可,使其他等待的线程可以获取许可并继续执行。

Semaphore的构造方法可以指定初始化的许可数量,也可以选择是否使用公平策略(公平策略会按照线程的等待时间来获取许可)。通过调整Semaphore的许可数量,可以动态地控制并发访问资源的线程数量。

构造函数:

permits 表示许可证的数量(资源数),fair 表示公平性,如果这个设为 true 的话,下次执行的线
程会是等待最久的线程
常用的方法:
acquire() 表示阻塞并获取许可;
tryAcquire() 方法在没有许可的情况下会立即返回 false,要获取许可的线程不会阻塞;
release() 表示释放许可
代码示例:
package com.bingfa001;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 模拟限流场景
 */
public class SemaphoreDemo {

    /**
     * 同一时刻最多只允许有两个并发
     */
    private static Semaphore semaphore = new Semaphore(2);

    private static Executor executor = Executors.newFixedThreadPool(10);

    private static ReentrantLock lock =new ReentrantLock();

    public static void main(String[] args) {
        for(int i=0;i<10;i++){
            executor.execute(()->getProductInfo2());
        }
    }

    public static  String getProductInfo() {
        try {
            semaphore.acquire();  //申请许可
            System.out.println("请求服务");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            semaphore.release(); //释放许可
        }
        return "返回商品详情信息";
    }

    public static String getProductInfo2() {
        //lock.lock();
        if(!semaphore.tryAcquire()){
            System.out.println("请求被流控了");
            return "请求被流控了";
        }
        try {
            System.out.println("请求服务");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            semaphore.release();
        //    lock.unlock();
        }
        return "返回商品详情信息";
    }
}

上述代码就是一个限流场景,调用了sleep()方法,让同一时刻获取资源的线程在资源释放前获取失败。如果对方法加锁那么就会等待之前的线程释放资源后再去获取资源,就都可以请求成功了!

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * 实现连接池
 */
public class SemaphoreDemo{

    final static ExecutorService executorService = Executors.newCachedThreadPool();

    public static void main(String[] args) {
        final ConnectPool pool = new ConnectPool(2);

        //5个线程并发来争抢连接资源
        for (int i = 0; i < 5; i++) {
            final int id = i + 1;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    Connect connect = null;
                    try {
                        System.out.println("线程" + id + "等待获取数据库连接");
                        connect = pool.openConnect();
                        System.out.println("线程" + id + "已拿到数据库连接:" + connect);
                        //进行数据库操作2秒...然后释放连接
                        Thread.sleep(2000);
                        System.out.println("线程" + id + "释放数据库连接:" + connect);

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        pool.releaseConnect(connect);
                    }

                }
            });
        }
    }
}

//数据库连接池
class ConnectPool {
    private int size;
    private Connect[] connects;

    //记录对应下标的Connect是否已被使用
    private boolean[] connectFlag;
    //信号量对象
    private Semaphore semaphore;

    /**
     * size:初始化连接池大小
     */
    public ConnectPool(int size) {
        this.size = size;
        semaphore = new Semaphore(size, true);
        connects = new Connect[size];
        connectFlag = new boolean[size];
        initConnects();//初始化连接池
    }

    private void initConnects() {
        for (int i = 0; i < this.size; i++) {
            connects[i] = new Connect();
        }
    }

    /**
     * 获取数据库连接
     *
     * @return
     * @throws InterruptedException
     */
    public Connect openConnect() throws InterruptedException {
        //得先获得使用许可证,如果信号量为0,则拿不到许可证,一直阻塞直到能获得
        semaphore.acquire();
        return getConnect();
    }

    private synchronized Connect getConnect() {
        for (int i = 0; i < connectFlag.length; i++) {
            if (!connectFlag[i]) {
                //标记该连接已被使用
                connectFlag[i] = true;
                return connects[i];
            }
        }
        return null;
    }

    /**
     * 释放某个数据库连接
     */
    public synchronized void releaseConnect(Connect connect) {
        for (int i = 0; i < this.size; i++) {
            if (connect == connects[i]) {
                connectFlag[i] = false;
                semaphore.release();
            }
        }
    }
}

/**
 * 数据库连接
 */
class Connect {

    private static int count = 1;
    private int id = count++;

    public Connect() {
        //假设打开一个连接很耗费资源,需要等待1秒
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("连接#" + id + "#已与数据库建立通道!");
    }

    @Override
    public String toString() {
        return "#" + id + "#";

    }

}
上述代码是用semaphore 实现资源池的示例。
应用场景总结:
限流:Semaphore可以用于限制对共享资源的并发访问数量,以控制系统的流量。
资源池:Semaphore可以用于实现资源池,以维护一组有限的共享资源。
CountDownLatch:
在 Java 中, CountDownLatch是一种同步工具,用于控制一个或多个线程等待一组操作完成后再继续
执行。它的主要作用是让一个或多个线程等待其他线程完成一系列任务后再继续执行。
构造函数:

 常用方法:

CountDownLatch提供了两个主要方法:

  1. countDown(): 该方法会减少内部计数器的值,表示一个操作已经完成。每次调用 countDown() 方法都会使内部计数器减1。
  2. await(): 该方法会使当前线程阻塞,直到内部计数器的值减到零。当计数器为零时,等待的线程会被唤醒,继续执行。
代码示例:

1.实现一个百米赛跑的场景

package com.bingfa001;
import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    // begin 代表裁判 初始为 1
    private static CountDownLatch begin = new CountDownLatch(1);

    // end 代表玩家 初始为 8
    private static CountDownLatch end = new CountDownLatch(8);

    public static void main(String[] args) throws InterruptedException {

        for (int i = 1; i <= 8; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    // 预备状态
                    System.out.println("参赛者" + Thread.currentThread().getName() + "已经准备好了");
                    // 等待裁判吹哨
                    try {
                        begin.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 开始跑步
                    System.out.println("参赛者" + Thread.currentThread().getName() + "开始跑步");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 跑步结束, 跑完了
                    System.out.println("参赛者" + Thread.currentThread().getName() + "到达终点");
                    // 跑到终点, 计数器就减一
                    end.countDown();
                }
            }).start();
        }
        // 等待 5s 就开始吹哨
        Thread.sleep(5000);
        System.out.println("开始比赛");
        // 裁判吹哨, 计数器减一
        begin.countDown();
        // 等待所有玩家到达终点
        end.await();
        System.out.println("比赛结束");

    }

}

2.多任务共同执行后进行汇总

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;

public class CountDownLatchDemo2 {
    public static void main(String[] args) throws Exception {

        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) {
                    e.printStackTrace();
                }
            }).start();
        }

        // 主线程在阻塞,当计数器为0,就唤醒主线程往下执行
        countDownLatch.await();
        System.out.println("主线程:在所有任务运行完成后,进行结果汇总");

    }
}
应用场景总结:
  1. 并行任务同步CountDownLatch 可以用于协调多个并行任务的完成情况,确保所有任务都完成后再继续执行下一步操作。

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值