# juc **Repository Path**: titaniumes-1/juc ## Basic Information - **Project Name**: juc - **Description**: juc并发编程 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-11-22 - **Last Updated**: 2021-11-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # juc #### 介绍 **juc并发编程 java.util.concurrent** #### Lock 接口 ##### Lock 和synchronized 不同点 1. Lock是一个接口, synchronized是关键字 2. synchronized 发生异常会自动释放锁,Lock不会,需要在finally 中通过unLock()释放锁 3. Lock可以使等待的线程中断,synchronized不行 4. 通过Lock可以知道有没有成功获取到锁,synchronized 做不到 5. Lock可以提高多个线程进行读操作效率,竞争激烈的资源推荐使用Lock ##### 多线程编程步骤 > 上部 第一步 创建资源类,在资源类创建属性和操作方法 > 中部 第二步 在资源类操作方法 1. 判断 2. 干活 3. 通知 第三步 创建多线程,调用资源类的操方法 > 下部 第四步 防止虚假唤醒问题 #### 集合的线程安全 ##### ArrayList 线程不安全解放方案 1. Vectory 2. Collections 3. CopyAndWriteArrayList (推荐使用) ##### HashSet 线程不安全 ​ CopyOnWriteArraySet ##### HashMap 线程不安全 ​ ConcurrentHashMap #### 多线程锁 ##### synchronized 同步基础: java中的每一个对象都可以作为锁 - 对于普通方法,锁是当前实例对象 - 对于静态同步方法,锁是当前类Class对象 - 对于同步方法块,锁是Synchrozied 括号李配置的对象 ##### 公平锁 和非公平锁 - 非公平锁 - 线程饿死 - 效率高 - 公平锁 - 阳光普照,人人都能吃上饭 - 效率相对较低 ReentrantLock 不传递参数就是默认无参构造方法 ,非公平锁 若传递一个true 就是公平锁,看场景选择适合的锁 ``` public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } ``` ##### 可重入锁 (递归锁) - synchronized(隐式) - Lock 显示) ![image-20211123110013691](https://gitee.com/titaniumes-1/typora-server/raw/master/juc-imges/image-20211123110013691.png) ##### 死锁 ![](https://gitee.com/titaniumes-1/typora-server/raw/master/imges/16-死锁.png) ​ #### Callable接口 ##### Callable概述 ![](https://gitee.com/titaniumes-1/typora-server/raw/master/imges/06-futureTask.png) ```java //实现Callable 接口 class MyThread2 implements Callable { @Override public Integer call() throws Exception { System.out.println(Thread.currentThread().getName()+" com in callable"); return 200; } } ``` ```java //labmda 表达式方式 FutureTask futureTask2= new FutureTask<>(() -> { System.out.println(Thread.currentThread().getName()+"com in callable"); return 123; }); //创建一个线程 new Thread(futureTask2,"luck").start(); System.out.println(futureTask2.get()); ``` FutureTask:在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些 作业交给 Future 对象在后台完成 - 当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执 行状态 - 一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去 获取结果 - 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法 - 一旦计算完成,就不能再重新开始或取消计算 - get 方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完 成状态,然后会返回结果或者抛出异常 - get 只计算一次,因此 get 方法放到最后 #### JUC强大的辅助类 ##### CountDownLatch 减少计数 CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行 减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法 之后的语句。 - CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这 些线程会阻塞 - 其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程 不会阻塞) - 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行 场景: 6 个同学陆续离开教室后值班同学才可以关门。 ```java //创建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(); System.out.println(Thread.currentThread().getName()+" 班长锁门走人了"); ``` ##### CyclicBarrier 循环栅栏 CyclicBarrier 看英文单词可以看出大概就是循环阻塞的意思,在使用中 CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一 次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后 的语句。可以将 CyclicBarrier 理解为加 1 操作 场景: 集齐 7 颗龙珠就可以召唤神龙 ```java //定义神龙召唤需要的龙珠总数 int NUMBER = 7; //定义循环栅栏 CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER,()->{ System.out.println("已经集齐7颗龙珠可以召唤神龙了"); }); //定义 7 个线程分别去收集龙珠 for (int i = 1; i <=7; i++) { new Thread(()->{ System.out.println(Thread.currentThread().getName() + "星龙珠被收集到了"); try { cyclicBarrier.await(); }catch (Exception e) { e.printStackTrace(); } },String.valueOf(i)).start(); } ``` ##### Semaphore 信号灯 Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线 程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方 法获得许可证,release 方法释放许可 场景: 抢车位, 6 部汽车 3 个停车位 ```java //创建一个 Semaphore 构造函数 参数1:指定的许可证数量 参数2: 是否公平竞争 默认false 不公平 Semaphore semaphore = new Semaphore(3); //模拟6辆汽车 for (int i = 1; i <= 6; i++) { new Thread(() -> { try { //抢占 semaphore.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(); } }, String.valueOf(i)).start(); } ``` #### ReentrantReadWriteLock读写锁 ![](https://gitee.com/titaniumes-1/typora-server/raw/master/imges/05-乐观锁和悲观锁.png) ![](https://gitee.com/titaniumes-1/typora-server/raw/master/imges/08-读写锁.png) ##### 读写锁介绍 现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那 么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以 应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源, 就不应该允许其他线程对该资源进行读和写的操作了。 针对这种场景,JAVA 的并发包提供了读写锁 ReentrantReadWriteLock, 它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称 为排他锁(独占锁) - 线程进入读锁的前提条件 - 没有其他线程的写锁 - 没有写请求, 但调用的线程和持有锁的线程是同一个 - 线程进入写锁条件 - 没有其他线程的读锁 - 没有其他线程的写锁 - 读写锁的特点 - 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公 平优于公平 - 重进入:读锁和写锁都支持线程重进入 - 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为 读锁 ##### 读写锁案例 场景: 使用 ReentrantReadWriteLock 对一个 hashmap 进行读和写操作 ``` //创建资源类 class MyCache { //创建一个map private volatile Map map = new HashMap<>(); //创建读写锁对象 private ReadWriteLock rwLock = new ReentrantReadWriteLock(); //放暑假 public void put(String key,String value) { //添加写锁 rwLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName()+"正在写操作"); //暂停一会 TimeUnit.MICROSECONDS.sleep(300); //放入暑假 map.put(key,value); System.out.println(Thread.currentThread().getName() +"写完了-->" + key); } catch (Exception e) { e.printStackTrace(); } finally { //释放写锁 rwLock.writeLock().unlock(); } } //取数据 public Object get(String key){ //添加读锁 rwLock.readLock().lock(); Object result = null; try { System.out.println(Thread.currentThread().getName() +"正在读操作" + key ); //暂停一会 TimeUnit.MICROSECONDS.sleep(300); result = map.get(key); System.out.println(Thread.currentThread().getName() +"取完了==>" + key ); } catch (Exception e) { e.printStackTrace(); } finally { //释放读锁 rwLock.readLock().unlock(); } return result; } } //启动类 MyCache myCache = new MyCache(); //创建线程放入数据 for (int i = 1; i <= 5; i++) { final int num = i; new Thread( () ->{ myCache.put(num + "",num+""); },String.valueOf(i)).start(); } //创建线程取出数据 for (int i = 1; i <= 5; i++) { final int num = i; new Thread( () ->{ myCache.get(num +""); },String.valueOf(i)).start(); } ``` ##### 读写锁深入 ![](https://gitee.com/titaniumes-1/typora-server/raw/master/imges/15-读写锁降级.png) ```java //可重入读写锁 ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();//读锁 ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock(); //写锁 //降级锁 //1.获取写锁 writeLock.lock(); System.out.println("titaniume"); //2.获取读锁 readLock.lock(); System.out.println("reader"); //3.释放写锁 writeLock.unlock(); //4.释放读锁 readLock.unlock(); ``` #### BlockingQueue阻塞队列 ##### 阻塞队列概述 先进先出 阻塞队列,顾名思义,首先它是一个队列, 通过一个共享的队列,可以使得数据 由队列的一端输入,从另外一端输出 ![image-20211124162437547](https://gitee.com/titaniumes-1/typora-server/raw/master/imges/image-20211124162437547.png) 当队列是空的,从队列中获取元素的操作将会被阻塞 当队列是满的,从队列中添加元素的操作将会被阻塞 试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素 试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多 个元素或者完全清空,使队列变得空闲起来并后续新增 在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起 的线程又会自动被唤起 ##### 常见的BlockingQueue - ArrayBlockingQueue - 由数组结构组成的有界阻塞队列 - LinkedBlockingQueue - 由链表结构组成的有界(但大小默认值为 integer.MAX_VALUE)阻塞队列 ##### 核心方法 ![image-20211124162717219](https://gitee.com/titaniumes-1/typora-server/raw/master/imges/image-20211124162717219.png) #### ThreadPool线程池 ##### 线程池概述 架构 线程过多会带来调度开销, 进而影响缓存局部性和整体性能 线程池不仅能够保证内核的充分利用,还能防止过分调度 Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors, ExecutorService,ThreadPoolExecutor 这几个类 ![image-20211125100142919](https://gitee.com/titaniumes-1/typora-server/raw/master/imges/image-20211125100142919.png) ##### 线程池说明 - corePoolSize 核心线程数 - maximumPoolSize 最大线程数 - keeAliveTime 空闲线程时间 - Unit 存活时间单位 - workeQueue 阻塞队列 - threadFactory 线程工厂 - hanlder 拒绝策略 三个核心参数影响线程池: 核心线程数(最小线程数量) ,最大线程数量 ,阻塞队列 当提交任务数量大于 核心线程数量时候 会优先放到阻塞队列中,阻塞队列饱后 会扩充线程池的线程数量,直至达到最大线程数量 这时候 在提交任务就会触发线程池的拒绝策略了。 总结起来,也就是一句话,当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。 ![image-20211125104535646](https://gitee.com/titaniumes-1/typora-server/raw/master/imges/image-20211125104535646.png) > 拒绝策略 > > - AbortPolicy (默认) 丢弃任务 抛出拒绝执行 RejectedExecutionException 异常 信息 > > - CallerRunsPolicy 调用者模式 不会抛弃任务有不会抛出异常 ,而是将任务回退到调用者 > > - DiscardOldestPolicy 抛弃队列中等待最久的惹怒,然后把当前任务加入到队列中 > > - DiscardPolicy 直接丢弃,其他啥都没有 > > ##### 线程池使用方式 - 一池N线程: Executors.newFixedThreadPool(int) - 一个任务一个任务执行,一池一线程: Executors.newSingleThreadExecutor() - 线程池根据需求创建线程,可扩容,遇强则强: Executors.newCachedThreadPool() ##### 自定义线程池 项目中创建多线程时,使用常见的三种线程池创建方式,单一、可变、定长都 有一定问题,原因是 FixedThreadPool 和 SingleThreadExecutor 底层都是用 LinkedBlockingQueue 实现的,这个队列最大长度为 Integer.MAX_VALUE, 容易导致 OOM。所以实际生产一般自己通过 ThreadPoolExecutor 的 7 个参 数,自定义线程池 创建线程池 通常不会使用Executors的方式 推荐使用ThreadPoolExecutor 及其 7 个参数手动创建 - corePoolSize 线程核心数量 - maximumPoolSize 最大线程数量 - KeepAliveTime 空闲线程存活世界 - Unit 存货时间单位 - workQueue 存放提交未执行的阻塞队列 - threadFactory 创建线程的工程类 - handler 等待队列满后的拒绝策略 ![image-20211127231941499](https://gitee.com/titaniumes-1/typora-server/raw/master/imges/image-20211127231941499.png) #### Fork/Join分支合并框架 Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子 任务结果合并成最后的计算结果,并进行输出。Fork/Join 框架要完成两件事情: **Fork:把一个复杂任务进行分拆,大事化小** **Join:把分拆任务的结果进行合并** ![image-20211128001511643](https://gitee.com/titaniumes-1/typora-server/raw/master/imges/image-20211128001511643.png) **任务分割**:首先 Fork/Join 框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割 **执行任务并合并结果**:分割的子任务分别放到双端队列里,然后几个启动线程 分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里, 启动一个线程从队列里取数据,然后合并这些数据。 案例 从1加到100 ```java //1~100 拆分想加 类似二分查找 class MyTask extends RecursiveTask{ //拆分差值不能超过10 计算10以内的数量 private static final Integer VALUE =10; private int begin; //拆分开始之 private int end; //查封结束值 private int result; //返回结果 //创建有参数的构造 public MyTask(int begin,int end){ this.begin = begin; this.end = end; } //拆分合并过程 @Override protected Integer compute() { //判断相加两个数值是否大于10 if((end-begin)<=VALUE) { //相加操作 for (int i = begin; i <=end; i++) { result = result+i; } } else {//进一步拆分 //获取中间值 int middle = (begin+end)/2; //拆分左边 MyTask task01 = new MyTask(begin,middle); //拆分右边 MyTask task02 = new MyTask(middle+1,end); //调用方法拆分 task01.fork(); task02.fork(); //合并结果 result = task01.join()+task02.join(); } return result; } } public class ForkJoinDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { //创建MyTask 对象 MyTask task = new MyTask(0, 100); //创建一个分支合并池对象 ForkJoinPool forkJoinPool = new ForkJoinPool(); ForkJoinTask forkJoinTask = forkJoinPool.submit(task); //获取最终合并之后的结果 Integer result = forkJoinTask.get(); System.out.println(result); //关闭池对象 forkJoinPool.shutdown(); } } ``` #### CompletableFuture异步回调 mpletableFuture 在 Java 里面被用于异步编程,异步通常意味着非阻塞, 可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可 以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。 CompletableFuture 实现了 Future, CompletionStage 接口,实现了 Future 接口就可以兼容现在有线程池框架,而 CompletionStage 接口才是异步编程 的接口抽象,里面定义多种异步方法,通过这两者集合,从而打造出了强大的 CompletableFuture 类。