JUC 三大辅助类和 CompletableFuture、StampedLock

JUC 中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过多时 Lock 锁的频繁操作。这三种辅助类为:
  CountDownLatch: 减少计数
  CyclicBarrier: 循环栅栏
  Semaphore: 信号灯

减少计数 CountDownLatch

CountDownLatch 是一个·同步辅助类,在完成一组正在其他线程中执行的操作值之前,它允许一个或多个线程一直等待。用给定的计数器初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当计数到达零之前,调用await() 方法的线程会被挂起,之后,会释放所有等待的线程,await() 的所有后续调用都将立即返回。这种现象只出现一次——计数器无法被重置,如果需要重置计数器,请考虑使用 CyclicBarrier。

阻塞和挂起是进程两种不同的状态,其描述如下:

阻塞:正在执行的进程由于发生某时间(如I/O请求、申请缓冲区失败等)暂时无法继续执行。此时引起进程调度,OS 把处理机分配给另一个就绪进程,而让受阻进程处于暂停状态,一般将这种状态称为阻塞状态。

挂起:由于系统和用户的需要引入了挂起的操作,进程被挂起意味着该进程处于静止状态。如果进程正在执行,它将暂停执行,若原本处于就绪状态,则该进程此时暂不接受调度。

共同点:
都导致进程暂停执行。
进程都释放CPU,即两个过程都会涉及上下文切换。

不同点:
对系统资源占用不同:虽然都释放了CPU,但阻塞的进程仍处于内存中;而挂起的进程通过“对换”技术被换出到磁盘中。
发生时机不同:阻塞一般在进程等待资源(IO资源、信号量等)时发生;而挂起是由于用户和系统的需要,例如,终端用户需要暂停程序研究其执行情况或对其进行修改、OS为了提高内存利用率需要将暂时不能运行的进程(处于就绪或阻塞队列的进程)调出到磁盘
恢复时机不同:阻塞要在等待的资源得到满足(例如获得了锁)后,才会进入就绪状态,等待被调度而执行;被挂起的进程由将其挂起的对象(如用户、系统)在时机符合时(调试结束、被调度进程选中需要重新执行)将其主动激活。

CountDownLatch 类可以设置一个计数器,然后通过 countDown() 方法来进行减 1 的操作,使用 await() 方法等待计数器不大于0,然后继续执行 await() 方法之后的语句。
CountDownLatch 主要有两个方法,当一个或多个线程调用 await() 方法时,这些线程会阻塞,其它线程调用 countDown() 方法会将计数器减 1(调用 countDown() 方法的线程不会阻塞),当计数器的值变为 0 时,因 await() 方法阻塞的线程会被唤醒,继续执行。

场景:6 个同学陆续离开教室后值班同学才可以关门。

public class CountDownLatchDemo {
    // 6个同学陆续离开教室之后,班长锁门
    public static void main(String[] args) throws InterruptedException {
        // 6个同学陆续离开教室,谁先离开教室不一定
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + " 号同学离开了教室");
            },String.valueOf(i)).start();
        }
        System.out.println(Thread.currentThread().getName() + " 班长锁门走人了");
    }
}

2 号同学离开了教室
1 号同学离开了教室
3 号同学离开了教室
4 号同学离开了教室
main 班长锁门走人了
5 号同学离开了教室
6 号同学离开了教室

这是不对的,5号和6号还没离开教室呢,班长不能锁门

改进:使用 CountDownLatch 辅助

public class CountDownLatchDemo {
    // 6个同学陆续离开教室之后,班长锁门
    public static void main(String[] args) throws InterruptedException {
        //创建CountDownLatch对象,设置初始值
        CountDownLatch countDownLatch = new CountDownLatch(6);
        // 6个同学陆续离开教室,谁先离开教室不一定
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + " 号同学离开了教室");
                //计数  -1
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        //等待
        countDownLatch.await(); // 当 main 线程想要执行的时候,如果计数器没有变为0,那么main 线程会阻塞在这里,当计数器变为0之后,main 线程被唤醒
        System.out.println(Thread.currentThread().getName() + " 班长锁门走人了");
    }
}

1 号同学离开了教室
2 号同学离开了教室
3 号同学离开了教室
5 号同学离开了教室
4 号同学离开了教室
6 号同学离开了教室
main 班长锁门走人了

循环栅栏 CyclicBarrier

CyclicBarrier 是一个同步辅助类,它允许一组线程相互等待,直到达到某个公共屏障点(common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须互相等待,此时 CyclicBarrier 很有用。

CyclicBarrier 看英文单词可以看出大概就是循环阻塞的意思,在使用中 CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await() 之后的语句。可以将 CyclicBarrier 理解为加 1 操作。

场景: 集齐 7 颗龙珠就可以召唤神龙

// 集齐7颗龙珠就可以召唤神龙
public class CyclicBarrierDemo {
    // 创建固定值
    private static final int NUMBER = 7;
    public static void main(String[] args) {
        // 创建 CyclicBarrier
        CyclicBarrier cyclicBarrier =
                new CyclicBarrier(NUMBER,()->{
                    System.out.println("*****集齐7颗龙珠就可以召唤神龙");
                });

        // 集齐七颗龙珠过程
        for (int i = 1; i <=7; i++) {
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName() + " 星龙被收集到了");
                    // await(); 方法告诉 CyclicBarrier 自己已经达到同步点,然后当前线程被阻塞
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

1 星龙被收集到了
2 星龙被收集到了
3 星龙被收集到了
4 星龙被收集到了
5 星龙被收集到了
6 星龙被收集到了
7 星龙被收集到了
*****集齐7颗龙珠就可以召唤神龙

CyclicBarrier 和 CountDownLatch,都是所有线程达到栅栏位置,才能执行。

CyclicBarrier:用来等待其他线程
CountDownLatch:用来等待事件

CyclicBarrier:基于 Condition 来实现的
CountDownLatch:基于 AQS 的共享模式的使用

信号灯 Semaphore

Semaphore 是一个计数信号量,从概念上讲,信号量维护了以一个许可集。如有必要在许可可用前,会阻塞一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。

Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。

Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire() 方法获得许可证,release() 方法释放许可。

场景: 抢车位, 6 部汽车 3 个停车位

// 6辆汽车,停3个车位
public class SemaphoreDemo {
    public static void main(String[] args) {
        // 创建 Semaphore,设置许可数量
        Semaphore semaphore = new Semaphore(3);

        // 模拟6辆汽车
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire(); // 线程通过 acquire(); 方法获得一个许可然后对共享资源进行操作,如果许可集已经分配完了,那么线程进入等待状态,直到其他线程释放许可,才有可能获得许可
                    System.out.println(Thread.currentThread().getName() + " 抢到了车位");
                    // 设置随机停车时间
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5));
                    System.out.println(Thread.currentThread().getName() + " ------离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();// 释放一个许可,许可将被还给 Semaphore
                }
            },String.valueOf(i)).start();
        }
    }
}

2 抢到了车位
1 抢到了车位
3 抢到了车位
3 ------离开了车位
4 抢到了车位
2 ------离开了车位
1 ------离开了车位
6 抢到了车位
5 抢到了车位
4 ------离开了车位
6 ------离开了车位
5 ------离开了车位

题外话,死锁
死锁
线程1和线程2都可以读取和修改,但是可能会造成死锁。
死锁
线程1对上下两个数据写操作,线程2对上下两个数据写操作。也可能会造成上述的死锁。

锁的降级
锁的降级。写的时候可以读,释放了写锁,降级为读锁

CompletableFuture

  • CompletableFuture 简介

CompletableFuture 在 Java 里面被用于异步编程,异步通常意味着非阻塞,可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。
CompletableFuture 实现了 Future,CompletionStage 接口,实现了 Future 接口就可以兼容现在有线程池框架,而 CompletionStage 接口才是异步编程的接口抽象,里面定义多种异步方法,通过这两者集合,从而打造出了强大的 CompletableFuture 类。

  • Future 与 CompletableFuture

Futrue 在 Java 里面,通常用来表示一个异步任务的引用,比如我们将任务提交到线程池里面,然后我们会得到一个 Futrue,在 Future 里面有 isDone() 方法来 判断任务是否处理结束,还有 get 方法可以一直阻塞直到任务结束然后获取结果,但整体来说这种方式,还是同步的,因为需要客户端不断阻塞等待或者不断轮询才能知道任务是否完成。

Future 的主要缺点如下:
(1)不支持手动完成
我提交了一个任务,但是执行太慢了,我通过其他路径已经获取到了任务结果,现在没法把这个任务结果通知到正在执行的线程,所以必须主动取消或者一直等待它执行完成。
(2)不支持进一步的非阻塞调用
通过 Future 的 get() 方法会一直阻塞到任务完成,但是想在获取任务之后执行额外的任务,因为 Future 不支持回调函数,所以无法实现这个功能。
(3)不支持链式调用
对于 Future 的执行结果,我们想继续传到下一个 Future 处理使用,从而形成一个链式的 pipline 调用,这在 Future 中是没法实现的。
(4)不支持多个 Future 合并
比如我们有 10 个 Future 并行执行,我们想在所有的 Future 运行完毕之后,执行某些函数,是没法通过 Future 实现的。
(5)不支持异常处理
Future 的 API 没有任何的异常处理的 api,所以在异步运行时,如果出了问题是不好定位的。

CompletableFuture 的使用案例

public class CompletableFutureUseDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "---come in");
            int result = ThreadLocalRandom.current().nextInt(10);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (result > 5) { // 跟据生产的随机数,模拟产生异常情况
                int i = 10 / 0;
            }
            System.out.println("----------1秒钟后出结果" + result);
            return result;
        }, executorService).whenComplete((v, e) -> {
            System.out.println("进入--whenComplete--");
            if (e == null) {
                System.out.println("计算完成 更新系统" + v);
            }
        }).exceptionally(e -> {
            e.printStackTrace();
            System.out.println("异常情况:" + e.getCause() + " " + e.getMessage());
            return null;
        });
        System.out.println(Thread.currentThread().getName() + "先去完成其他任务");
        executorService.shutdown();
    }
}

main先去完成其他任务
pool-1-thread-1---come in
----------1秒钟后出结果4
进入--whenComplete--
计算完成 更新系统4

----------------------------分割线-----------------------------

pool-1-thread-1---come in
main先去完成其他任务
进入--whenComplete--
异常情况:java.lang.ArithmeticException: / by zero java.lang.ArithmeticException: / by zero
java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
	at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:315)
	at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:320)
	at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1770)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.base/java.lang.Thread.run(Thread.java:842)
Caused by: java.lang.ArithmeticException: / by zero
	at com.juc.CompletableFutureUseDemo.lambda$main$0(CompletableFutureUseDemo.java:23)
	at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768)
	... 3 more

需求:
电商网站比价需求分析:

  1. 需求说明:
    a. 同一款产品,同时搜索出同款产品在各大电商平台的售价
  2. 输出返回:
    a. 出来结果希望是同款产品的在不同地方的价格清单列表,返回一个List
    例如:《Mysql》 in jd price is 88.05 《Mysql》 in taobao price is 90.43
  3. 解决方案,对比同一个产品在各个平台上的价格,要求获得一个清单列表
    a. step by step,按部就班,查完淘宝查京东,查完京东查天猫…
    b. all in,万箭齐发,一口气多线程异步任务同时查询
public class CompletableFutureMallDemo {
    static List<NetMall> list = Arrays.asList(new NetMall("京东"), new NetMall("淘宝"), new NetMall("当当"));

    /**
     * 一个一个的查
     * @param list
     * @param productName
     * @return
     */
    public static List<String> getPrice(List<NetMall> list, String productName) {
        //《Mysql》 in jd price is 88.05
        return list
                .stream()
                .map(netMall ->
                        String.format("《" + productName + "》" + "in %s price is %.2f",
                                netMall.getNetMallName(),
                                netMall.calcPrice(productName)))
                .collect(Collectors.toList());
    }

    /**
     * 使用 CompletableFuture 异步查,不指定线程池,就使用默认的线程池
     * 把list里面的内容映射给CompletableFuture()
     * @param list
     * @param productName
     * @return
     */
    public static List<String> getPriceByCompletableFuture(List<NetMall> list, String productName) {
        return list.stream().map(netMall ->
                        CompletableFuture.supplyAsync(() ->
                                String.format("《" + productName + "》" + "in %s price is %.2f",
                                        netMall.getNetMallName(),
                                        netMall.calcPrice(productName)))) // Stream<CompletableFuture<String>>
                .collect(Collectors.toList()) // List<CompletableFuture<String>>
                .stream() // Stream<String>
                .map(s -> s.join()).collect(Collectors.toList()); // List<String>
    }

    public static void main(String[] args) {
        /**
         * 采用“一个一个的查”方式查询
         * 《masql》in 京东 price is 110.11
         * 《masql》in 淘宝 price is 109.32
         * 《masql》in 当当 price is 109.24
         * ------costTime: 3094 毫秒
         */
        long StartTime = System.currentTimeMillis();
        List<String> list1 = getPrice(list, "masql");
        for (String element : list1) {
            System.out.println(element);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("------costTime: " + (endTime - StartTime) + " 毫秒");

        /**
         * 采用“使用 CompletableFuture 异步查”三个异步线程方式查询
         * 《mysql》in 京东 price is 109.71
         * 《mysql》in 淘宝 price is 110.69
         * 《mysql》in 当当 price is 109.28
         * ------costTime1009 毫秒
         */
        long StartTime2 = System.currentTimeMillis();
        List<String> list2 = getPriceByCompletableFuture(list, "mysql");
        for (String element : list2) {
            System.out.println(element);
        }
        long endTime2 = System.currentTimeMillis();
        System.out.println("------costTime" + (endTime2 - StartTime2) + " 毫秒");

    }
}

class NetMall {
    private String netMallName;

    public NetMall() {
    }

    public NetMall(String netMallName) {
        this.netMallName = netMallName;
    }

    public String getNetMallName() {
        return netMallName;
    }

    public void setNetMallName(String netMallName) {
        this.netMallName = netMallName;
    }

    public double calcPrice(String productName) {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return ThreadLocalRandom.current().nextDouble() * 2 + productName.charAt(0);
    }
}

StampedLock 是 jdk8 中新增的一个读写锁,也是对 JDK5 中的读写锁 ReentrantReadWriteLock 的优化。
stamp 代表了锁的状态。当 stamp 返回零时,表示线程获取锁失败,并且当释放锁或者转换锁的时候,都要传入最初获取的 stamp 值。
它是由饥饿问题引出:
ReentrantReadWriteLock 实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,因此当前有可能会一直存在读锁,而无法获得写锁。

如何解决锁饥饿问题:
1、使用”公平“策略可以一定程度上缓解这个问题,但使用“公平”策略是以牺牲系统吞吐量为代价的。
2、使用 StampedLock 类的乐观读锁方式 —> 采取乐观获取锁,其他线程尝试获取写锁时不会被阻塞,在获取乐观读锁后,还需要对结果进行校验。

两个例子,一个是传统模式:

public class StampedLockDemo {
    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write() {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
        try {
            number = number + 13;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程结束修改");
    }

    public void read() {
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName() + "\t" + " come in readLock codeBlock");
        for (int i = 0; i < 4; i++) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + " 正在读取中");
        }
        try {
            int result = number;
            System.out.println(Thread.currentThread().getName() + "\t" + "获得成员变量值result: " + result);
            System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥");
        } finally {
            stampedLock.unlockRead(stamp);
        }

    }

    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();
        new Thread(() -> {
            resource.read();
        }, "readThread").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t"+" come in");
            resource.write();
        }, "writeThread").start();
    }
}

readThread	 come in readLock codeBlock
readThread	 正在读取中
writeThread	 come in
readThread	 正在读取中
readThread	 正在读取中
readThread	 正在读取中
readThread	获得成员变量值result: 37
写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥
writeThread	写线程准备修改
writeThread	写线程结束修改

使用乐观读锁优化:乐观读模式 ----> 读的过程中也允许其他线程的写锁介入但不会被阻塞,在获取乐观读锁后,还需要对结果进行校验。

public class StampedLockDemo {
    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write() {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
        try {
            number = number + 13;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程结束修改");
    }

    public void read() {
        long stamp = stampedLock.tryOptimisticRead();

        int result = number;

        System.out.println("4秒前 stampedLock.validate方法值(true 无修改 false有修改)" + "\t" + stampedLock.validate(stamp));
        for (int i = 0; i < 4; i++) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + " 正在读取...." + i + "秒后" + "stampedLock.validate方法值(true 无修改 false有修改)" + "\t" + stampedLock.validate(stamp));
        }
         // 在获取乐观读锁后,还需要对结果进行校验
        if (!stampedLock.validate(stamp)) {
            System.out.println("有人修改----------有写操作");
            stamp = stampedLock.readLock();
            try {
                System.out.println("从乐观读升级为悲观读");
                result = number;
                System.out.println("重新悲观读后result:" + result);
            } finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "finally value: " + result);

    }


    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();
        new Thread(() -> {
            resource.read();
        }, "readThread").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + " come in");
            resource.write();
        }, "writeThread").start();
    }
}

4秒前 stampedLock.validate方法值(true 无修改 false有修改)	true
readThread	 正在读取....0秒后stampedLock.validate方法值(true 无修改 false有修改)	true
writeThread	 come in
writeThread	写线程准备修改
writeThread	写线程结束修改
readThread	 正在读取....1秒后stampedLock.validate方法值(true 无修改 false有修改)	false
readThread	 正在读取....2秒后stampedLock.validate方法值(true 无修改 false有修改)	false
readThread	 正在读取....3秒后stampedLock.validate方法值(true 无修改 false有修改)	false
有人修改----------有写操作
从乐观读升级为悲观读
重新悲观读后result:50
readThread	finally value: 50

StampedLock的缺点:
1、StampedLock 不支持重入,没有Re开头
2、StampedLock 的悲观读锁和写锁都不支持条件变量,这个也需要注意
3、使用 StampedLock 一定不要调用中断操作,即不要调用 interrupt() 方法

  • 29
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值