JUC高级-0608

重新看JUC课程,选择周阳讲的JUC

1.前置知识

lombok插件

Lombok是一个Java库,它通过注解的方式,能够在编译时自动为类生成构造函数、getters、setters、equals、hashCode和toString方法,以及其他常用方法,从而使我们的代码更加简洁,更易于阅读和编写。

  1. @AllArgsConstructor:这个注解会生成一个包含所有字段的构造函数,这个构造函数的参数顺序与字段在类中声明的顺序一致。
  2. @NoArgsConstructor:这个注解会生成一个无参数的默认构造函数。
  3. @Data:这个注解包含了@ToString,@EqualsAndHashCode,@Getter/@Setter和@RequiredArgsConstructor的功能,即:为类提供读写属性,同时生成equals,canEqual,hashCode,toString方法,以及参数为final的构造方法。所以,如果一个类被@Data注解,那么这个类就拥有了以上这些基本的数据操作功能。

方法引用

在Java 8中,方法引用是一种简化Lambda表达式的写法。方法引用可以更简洁、更直观地表示现有的方法、构造方法或者特定类型的任意对象的实例方法。

方法引用有以下四种形式:

  1. 静态方法引用:如果函数签名和定义已存在的静态方法签名一致,就可以使用静态方法引用。
    // Lambda 表达式
    Consumer<String> lambdaConsumer = (String s) -> System.out.println(s);
    
    // 方法引用
    Consumer<String> methodRefConsumer = System.out::println;
    
  2. 特定实例的实例方法引用:如果函数签名和特定实例对象的某个实例方法一致,就可以使用特定实例的实例方法引用。
    String str = "abc";
    Predicate<String> lambdaPredicate = (String s) -> str.startsWith(s);
    Predicate<String> methodRefPredicate = str::startsWith;
    
  3. 任意对象的实例方法引用:如果函数签名和某个类的实例方法一致,就可以使用任意对象的实例方法引用。
    Predicate<String> lambdaPredicate = (String s) -> s.isEmpty();
    Predicate<String> methodRefPredicate = String::isEmpty;
    
  4. 构造方法引用:如果函数签名和构造方法一致,就可以使用构造方法引用。
    Supplier<List<String>> lambdaSupplier = () -> new ArrayList<>();
    Supplier<List<String>> methodRefSupplier = ArrayList::new;
    

总的来说,方法引用是一种让你可以重复使用已有方法的功能。在许多情况下,它们可以使你的代码更简洁、更清晰。

2.线程基础复习

  1. 多线程的优势和劣势:
    1. 优势:
      1. 充分利用多核处理器
      2. 提高程序性能,高并发系统
      3. 提高程序吞吐量,异步+回调等生产需求
    2. 劣势:
      1. 线程安全问题:i++,集合线程安全问题
      2. 线程锁问题:synchronized过重,怎样使用更灵活的锁
      3. 线程性能问题:
  2. 从start一个线程说起:
    1. Thread类中的start是一个同步方法,内部调用了一个start0的本地方法,是用C++写的
    2. C++就是JDK的JDK,Java语言就是C++的简化版。
    3. openjdk的写JNI一般是一一对应的,Thread.java对应的就是Thread.c
    4. 读C++远码:
      1. thread.c : openjdk8\jdk\src\share\native\java\lang
      2. jvm.cpp : openjdk8\hotspot\src\share\vm\prims
        Thread::start(native_ thread);
        
      3. Thread.cpp : openjdk8\hotspot\src\share\vm\runtime
        os::start_ thread(thread);
        //	操作系统提供分配的线程
        
    5. 总结:线程分配是JVM结合操作系统进行分配的
  3. Java多线程相关概念:
    1. 一把锁:synchronized
    2. 两个并:并行和并发
    3. 3个程:进程、线程、管程
      1. 管程:Monitor,监视器,也就是我们说的锁,
        synchronized(Monitor){}	//	monitor就是一个监视器,也就是一个锁	
        
      2. Monitor :是一种同步机制,他的义务是保证同时间只有一个线程可以访问被保护的数据和代码
      3. JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象,
      4. Monitor对象会和Java对象一同创建并销毁,它底层是由C++语言来实现的。
      5. 执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。
  4. 用户线程和守护线程
    1. Java线程分为两种,用户线程和守护线程
      1. 一般而已说的都是用户线程
      2. 守护线程用于在后台完成一些必要的操作,例如垃圾回收线程
      3. 当用户线程运行时,守护线程会一直运行;用户线程结束,守护线程也会随之结束
    2. 线程的daemon属性:
      1. 源码解读:
        public final boolean isDaemon() {
        	return daemon;
        }
        
      2. true就是守护线程,false是用户线程
      3. code演示
        1. main线程也是用户线程,main线程结束了,用户线程并不一定结束
      4. 总结:
        1. 如果用户线程全部结束意味着程序需要完成的业务操作已经结束了,守护线程随着JVM一同结束工作
        2. setDaemon(true)方法必须在start()之前设置,否则报llegalThreadState Exception异常

3.CompletableFuture

3.1 Future接口理论复习

Future接口(FutureTask实现类)定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。

比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,忙其它事情或者先执行完,过了一会才去获取子任务的执行结果或变更的行务状态。
JUC高级-0608_第1张图片
Future又称异步任务接口:
一句话:Future接口可以为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务。

3.2 Future接口常用实现类Future Task异步任务

  1. Future接口可以做什么
    1. Future是Java5新加的一个接口,它提供了一种异步并行计算的功能。如果主线程需要执行一个很耗时的计算任务,我们就可以通过future把这个任务放到异步线程中执行。主线程继续处理其他任务或者先行结束,再通过Future获取计算结果。
  2. 本源的Future接口相关架构
    1. 代码说话:
      1. Runnable接口 : 实现run方法,无返回值,不抛出异常
      2. Callable接口 :实现call方法,有返回值,抛出异常
      3. Future接口和FutureTask实现类
      4. 目的:异步多线程任务执行且返回有结果,
      5. 三个特点:多线程/有返回/异步任务
    2. 存在问题:我如果想创建一个线程实现类,需要多线程、有返回值、异步任务,则需要实现callable接口,但是使用Thread创建线程必须传Runnable类型参数,所以我们去找runnable的子类。有没有可以满足的:RunnableFuture,这个接口有一个实现类:FutrueTask,但是FutureTask没有实现Callable接口,不过他支持构造注入。
    3. FutrueTask两种构造:不支持空参构造
      1. FutureTask(Callable callable)
      2. FutureTask( Runnable runnable , V result)
    4. 代码实例:
      //	如何获得一个带有返回值的异步多线程任务,结合Thread类,并且获得处理结果
      public class MyCompletableFutrueDemo {
          
          public static void main(String[] args) throws ExecutionException, InterruptedException {
              
              FutureTask<String> futureTask = new FutureTask<>(new MyThread());
      
              Thread t1 = new Thread(futureTask,"t1");
      
              t1.start();
      				//	获取异步执行结果
              System.out.println(futureTask.get());
          }
      }
      
      
      class MyThread implements Callable<String>{
      
          @Override
          public String call() throws Exception {
              System.out.println("come in callable");
              return "hello world";
          }
      }
      
  3. Future编码实战和优缺点分析
    1. 优点:
      1. future+线程池异步多线程任务配合,能显著提高程序的执行效率。
      2. 代码说话:
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        
            FutureTask<String> futureTask1 = new FutureTask<>(() -> {
            try {TimeUnit.MICROSECONDS.sleep(500); } catch (InterruptedException e) {e.printStackTrace();}
                  return "task1 over";
            });
            threadPool.submit(futureTask1);
            futureTask1.get();
        
      3. 如果3个任务都交给主线程做,需要消耗1100ms,交给3个异步线程,800ms
    2. 缺点:
      1. get因为是异步的,一般建议放在程序最后,不然会阻塞主线程的其他任务执行
      2. 我希望可以过时不侯,可以自动离开:futureTask.get(3,TimeUnit.SECONDS);这个方法会抛出超时异常,不过可能会影响其他程序执行。
      3. isDone()轮询空转,消耗系统资源,而且也不见得能及时获取结果
        1. 如果想要异步获取结果,通常都会以轮询的方式去获取结果,尽量不要阻塞。
    3. 结论:Future对于结果的获取并不友好,只能轮询或者阻塞的方式去获取
  4. 完成一些复杂的任务
    1. 对于简单的业务场景使用Future完全OK
    2. 回调通知
      1. 应对Future的完成时间,完成了可以告诉我,也就是我们的回调通知
      2. 通过轮询的方式去判断任务是否完成这样非常古cPU并且代码也不优雅
    3. 创建异步任务:Future+线程池配合
    4. 多个任务前后依赖可以组合处理(水煮鱼)Future做不到
      1. 想将多个异步任务的计算结果组合起来,后一个异步任务的计算结果需要前一个异步任务的值
      2. 将两个或多个异步计算合成一个异步计算,这几个异步计算互相独立,同时后面这个又依赖前一个处理的结果。
    5. 对计算速度选最快
      1. 当Future集合中某个任务最快结束时,返回结果,返回第一名处理结果。
    6. Future不足以胜任复杂任务:
      1. 使用Future之前提供的那点API就囊中羞涩,处理起来不够优雅,这时候还是让CompletableFuture 以声明式的方式优雅的处理这些需求。
      2. 从i到i++,o(n_n)0哈哈~
      3. Future能干的,CompletableFuture都能干

3.3 CompletableFuture对Future的改进

  1. Completable为什么出现?
    1. get()方法在Future 计算完成之前会一直处在阻寨状态下,
    2. isDone()方法容易耗费CPU资源,
    3. 对于真正的异步处理我们希望是可以通过传入回调函数,在Future结束时自动调用该回调函数,这样,我们就不用等待结果。
    4. 阻寨的方式和异步编程的设计理念相违背,而轮询的方式会耗费无谓的CPU资源。因此,JDK8设计出CompletableFuture。
    5. CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。
  2. CompletableFuture 和CompletionStage源码分别介绍
    1. 类架构说明
      public class CompletableFuture<T> implements Future<T>, CompletionStage<T> { 
      
    2. 接口CompletionStage
      1. CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段
      2. 一个阶段的计算执行可以是一个Function,Consumer或者Runnable。
      3. 一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发
      4. 代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段,有些类似Linux系统的管道分隔符传参数。
      5. 在实际操作中,就是.whenComplete 和 .exceptionally 两个方法。
    3. 类CompletableFuture
      1. 在Java8中,CompletableFuture提供了非常强大的Future的扩展 可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合 CompletableFuture 的方法。
      2. 它可能代表一个明确完成的Future,也有可能代表一个完成阶段(CompletionStage),它支持在计算完成以后触发一些西数或执行某些动作。
      3. 它实现了 Future和CompletionStage接口
  3. 核心的四个静态方法,来创建一个异步任务(不推荐使用new CompletableFuture()去获得,创建的是不完备的)
    0. 两组四个方法:
    1. runAsync 无返回值
      1. public static Completable Future runAsync(Runnable runnable)
      2. public static CompletableFuture runAsync(Runnable runnable,Executor executor)
    2. supplyAsync 有返回值(常用)
      1. public static Completable Future supplyAsync(Supplier supplier)
      2. public static CompletableFuture supplyAsync(Supplier supplier, Executor executor)
    3. 上述Executor executor参数说明
      1. 没有指定Executor的方法,直接使用默认的Fork JoinPool.commonPool(),作为它的线程池执行异步代码。
      2. 如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码
    4. code:
      ExecutorService threadPool = Executors.newFixedThreadPool(3);
      
              CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
                  System.out.println(Thread.currentThread().getName());
                  try {
                      TimeUnit.SECONDS.sleep(1);
                  }catch (InterruptedException e){
                      e.printStackTrace();
                  }
              },threadPool);
              System.out.println(completableFuture.get());
      
              CompletableFuture<String> completableFuture2 = CompletableFuture.supplyAsync(() -> {
                  try {
                      TimeUnit.SECONDS.sleep(1);
                  }catch (InterruptedException e){
                      e.printStackTrace();
                  }
                  return "hello world";
              }, threadPool);
      
              System.out.println(completableFuture2.get());
      
              threadPool.shutdown();
      
    5. code通用演示,减少阻塞和轮询:
      1. 从Java8开始引入了CompletableFuture,它是Future的功能增强版,减少阻塞和轮询可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法
      2. 解释下为什么默认线程池关闭,自定义线程池自动关闭
        • 主线程不能立刻结束,否则CompletableFuture就以使用的线程池会立刻关闭:暂停3秒钟线程
        • completable线程类似守护线程,主线程完成之后会关闭
      3. code :
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        
                try {
                    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();
                        }
                        System.out.println("1秒钟之后出结果!");
                        return result;
                    },fixedThreadPool).whenComplete((result,exception)->{
        
                        System.out.println("计算完成!——:"+result);
        
                    }).exceptionally(e->{
                        e.printStackTrace();
                        System.out.println("异常情况:"+e.getCause()+"  "+e.getMessage());
                        return null;
                    });
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    fixedThreadPool.shutdown();
                }
        
                //  主线程不能立刻结束,否则CompletableFuture就以使用的线程池会立刻关闭:暂停3秒钟线程
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        
    6. Completable优点:
      1. 异步任务结束时,会自动回调某个对象的方法:
      2. 主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行
      3. 异步任务出错时,会自动回调某个对象的方法;
      4. 和ajax很像

3.4 函数式编程

复习:

  1. Runnable,无参数,无返回值
    @FunctionalInterface
    public interface Runnable {
        public abstract void run();
    }
    
  2. Function:功能性函数接口:接收一个参数,并且有返回值
    @FunctionalInterface
    public interface Function<T, R> {
    
        R apply(T t);
    }   
    
  3. Consumer : 消费者型接口:接收一个参数,无返回值
    @FunctionalInterface
    public interface Consumer<T> {
    
        void accept(T t);
    }
    
  4. Supplier:供给者型接口,无参数,有返回值
    @FunctionalInterface
    public interface Supplier<T> {
    
        T get();
    }
    
  5. BiConsumer : 双参数消费者,两个参数,无返回值
    @FunctionalInterface
    public interface BiConsumer<T, U> {
    
        void accept(T t, U u);
    }
    
  6. 小总结
函数式接口名称 方法名称 参数 返回值
Runnable run 无参数 无返回值
Function apply 1个参数 有返回值
Consumer accept 1个参数 无返回值
Supplier get 无参数 有返回值
BiComsumer accept 2个参数 无返回值
  1. Chain链式调用
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Accessors(chain = true)
    class Student{
    
        private Integer id;
        private String studentName;
        private String major;
        
    }
    public static void main(String[] args) {
    
            Student student = new Student();
    
      			//	链式调用
            student.setId(12).setStudentName("mike").setMajor("CS");
        }
    
  2. join和get对比:都可以从conpletable里面取值:
    1. 作用几乎一样,只是join不抛出异常

3.5 案例精讲-电商网站比价需求

  1. 需求说明:
    1. 同一款产品,同时搜索出同款产品在各大电商平台的售价:
    2. 同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少
  2. 输出返回:
    1. 出来结果希望是同款产品的在不同地方的价格清单列表,返回一个List
    2. 《mysql》 in jd price is 88.05
    3. 《mysql》 in dangdang price is 86.11
    4. 《mysql》 in taobao price is 90.43
  3. 解决方案,比对同一个商品在各个平台上的价格,要求获得一个清单列表,
    1. step by step,按部就班,查完京东查淘宝,查完淘宝查天猫……
    2. all in. 万箭齐发,一口气多线程异步任务同时查询。。。。。
  4. 核心逻辑写法:
     public static List<String> getPriceByASync(List<NetMall> list,String productName) {
            return list
                    .stream()
                    .map(netMall -> 
                            CompletableFuture.supplyAsync(() -> 
                                    String.format(productName + " is %s price is %.2f",
                                            netMall.getMallName(),
                                            netMall.calcPrice(productName))))	//Stream>
                    .collect(Collectors.toList())	// List>
                    .stream()	//	Stream>
                    .map(CompletableFuture::join)	//Stream
                    .collect(Collectors.toList());	//	List
        }
    
    
    以上是周阳代码,不知道为什么不这么写,减少一次来回转换
      
      public static List<String> getPriceByASync(List<NetMall> list,String productName) {
            return list
                    .stream()
                    .map(netMall -> CompletableFuture.supplyAsync(()->
                            String.format(String.format(productName + " is %s price is %.2f",
                            netMall.getMallName(),
                            netMall.calcPrice(productName)))).join())
                    .collect(Collectors.toList());
        }
    
    然后自己去测试了一下:
    后面的写法看似简单,实则失去了异步队列的优势,而是一个一个去执行,并且等待join,然后映射成新的值,相当于异步任务退化成串行化执行,耗时差距非常大!!
    

3.6 CompletableFuture常用方法

  1. Completable实现了Future和CompletionStage,前者方法少功能弱,后者方法多。现以分类的方式介绍Completable的方法。
  2. 获得结果和触发计算
    1. 获取结果
      1. public T get() 一定要拿到结果,容易阻塞
      2. public T get(long timeout, TimeUnit unit) 过时不候
      3. public T join() 和get一样,只是没有异常
      4. public T getNow (T valuelfAbsent) 现在取值,如果没完成给一个替代结果,不阻塞
    2. 主动触发计算
      1. public boolean complete(T value):返回是否打断了get阻塞,打断的话用替代值,没打断用计算值
      2. //	code
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
                    //暂停几秒钟线程
                    //暂停几秒钟线程
                    try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
                    return 1;
                },threadPoolExecutor);
        
                System.out.println(future.get());
                System.out.println(future.get(2L,TimeUnit.SECONDS));
         				System.out.println(future.getNow(9999));
        				System.out.println(future.complete(-44)+"\t"+future.get());
        
  3. 对计算结果进行处理
    1. thenApply:(一般常用
      1. 计算结果存在依赖关系,这两个线程串行化
      2. completable默认使用forkJoinPool线程池,如果主线程结束,他也会自动消失
      3. 异常:由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停。
      4. System.out.println(CompletableFuture.supplyAsync(() -> {
                    return 1;
                }).thenApply(f -> {
                    return f + 2;
                }).thenApply(f -> {
                    return f + 3;
                }).whenComplete((v, e) -> {
                    if (e == null) {
                        System.out.println("0-------result: " + v);
                    }
                }).exceptionally(e -> {
                    e.printStackTrace();
                    System.out.println(e.getMessage());
                    return null;
                }).join());
        
    2. handle
      1. 计算结果存在依赖关系,这两个线程串行化,这一点和上面一样
      2. 区别:有异常也可以往下一步走,根据带的异常参数可以进一步处理,只能走一步。
      3. System.out.println(CompletableFuture.supplyAsync(() ->  {
                    return 1;
                }).handle((f,e) -> {
                    System.out.println("-----1");
                    return f + 2;
                }).handle((f,e) -> {
                    System.out.println("-----2");
                    return f + 3;
                }).handle((f,e) -> {
                    System.out.println("-----3");
                    return f + 4;
                }).whenComplete((v, e) -> {
                    if (e == null) {
                        System.out.println("----result: " + v);
                    }
                }).exceptionally(e -> {
                    e.printStackTrace();
                    return null;
                }).join());
        
    3. 总结:
      1. Exceptionally : try/catch
      2. whenComplete / handle -> try/finally
  4. 对计算结果进行消费
    1. 接收任务的处理结果,并消费处理,无返回结果
    2. thenAccept
      CompletableFuture.supplyAsync(() -> {
                  return 1;
              }).thenApply(f -> {
                  return f+2;
              }).thenApply(f -> {
                  return f+3;
              }).thenAccept(System.out::println);
      
    3. 对比补充:code之间执行顺序问题:
      1. thenRun
        1. thenRun(Runnable runnable)
        2. 任务A 执行完执行 B,并且R不需要A 的结果
        3. System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenRun(() -> {}).join());	//	A忙完了忙B,两者没有依赖,只是有顺序关系
          
      2. thenAccept
        1. thenAccept(Consumer action)
        2. 任务A执行完执行 B,B需要A的结果,但是任务B无返回值
        3. System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenAccept(resultA -> {}).join());	//	A忙完了,B需要A的结果,有顺序也有依赖关系,但是B无返回值
          
      3. thenApply
        1. thenApply(Function fn)
        2. 任务 A执行完执行 B,B需要A的结果,同时任务B有返回值
        3. System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenApply(resultA -> resultA + " resultB").join());	//	B需要A的结果,并且还有返回值
          
    4. CompletableFuture和线程池说明
      1. 以thenRun和thenRunAsync为例,有什么区别?
      2. 小总结:
        1. 没有传入自定义线程池,都用默认线程池ForkJoinPool
        2. 传入了一个自定义线程池,如果你执行第一个任务的时候,传入了一个自定义线程池:
          1. 调用thenRun方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池。
          2. 调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池
        3. 备注:有可能处理太快,系统优化切换原则,直接使用main线程处理
        4. 其它如: thenAccept和thenAcceptAsync, thenApply和thenApplyAsync等,它们之间的区别也是同理
      3. 源码分析:判断用户cpu核数,一般都大于1,所以用了Async默认就是用forkJoinPool
  5. 对计算速度进行选用
    1. 谁快用谁:
    2. applyToEither
    3. System.out.println(CompletableFuture.supplyAsync(() -> {
                  //暂停几秒钟线程
                  try {
                      TimeUnit.SECONDS.sleep(1);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  return 1;
              }).applyToEither(CompletableFuture.supplyAsync(() -> {
                  try {
                      TimeUnit.SECONDS.sleep(2);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  return 2;
              }), r -> r + " is winner ").join());
      
  6. 对计算结果进行合并
    1. 两个CompletionStage任务都完成后,最终能把两个任务的结果一起交给thenCombine来处理先完成的先等着,等待其它分支任务
    2. thenCombine
    3. System.out.println(CompletableFuture.supplyAsync(() -> {
                  return 10;
              }).thenCombine(CompletableFuture.supplyAsync(() -> {
                  return 20;
              }), (r1, r2) -> {
                  return r1 + r2;
              }).join());
      
    4. 二者的结果合并之后,依然可以继续合并。

4. Java多线程锁

4.1 乐观锁和悲观锁

  1. 悲观锁
    1. 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
    2. synchronized关键字和Lock的实现类都是悲观锁
    3. 适合写操作多的场景,先加锁可以保证写操作时数据正确。显式的锁定之后再操作同步资源
    4. 一句话:狼性锁
  2. 乐观锁
    1. 认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁。
    2. 在Java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。
    3. 如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
    4. 如果这个数据己经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等
    5. 判断规则
      1. 版本号机制Version
      2. 最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的

4.2 8锁案例展示锁的原理

  1. 锁相关的8种案例演示code,和JUC基础里面的8锁案例一样,不再次记录。
  2. 本质上,锁的范围;是否同一把锁

4.3 synchronized三种方式及底层原理

  1. synchronized三种应用方式
    1. 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
    2. 作用于代码块,对括号里配置的对象加锁。
    3. 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
  2. synchronized底层原理:
    1. 同步代码块:实现使用的是monitorenter和monitorexit指令
      1. 现象:会有一个enter和两个exit:原因:为了避免异常导致锁无法退出,自动进行退出
      2. JUC高级-0608_第2张图片
      3. 相比synchronized,lock就必须写在finally里面保证必然释放
      4. 一定是一个enter对应两个exit吗?
        1. 默认情况是的
        2. 如果一个同步块必然会抛出异常,那么第一个exit就省略掉了
    2. 普通同步方法:ACC_SYNCHRONIZED
      1. 调用指念将会检查方法的ACCSYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitgr锁,然后再执行方法,最后在方法完成(无论是正常完成还是非馆常完成)时释放monitor
    3. 静态同步方法ACC_PUBLIC, ACC_ SYNCHRONIZED
  3. synchronized锁的是什么?
    0. 管程:
    1. 管程(英语:Monitors,也称为监视器)是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般定硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
    2. 同步指令:Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor,更常见的是直接将它称为“锁”)来实现的。
      方法级的同步是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟
      机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为
      同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如
      果设置了,执行线程就要求先成功持有管程,然后才能热行方法,最后当方法完成(无论是正當完成
      还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同
      步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。
    3. 为什么任何一个对象都能成为锁
      1. 每个对象天生都带着一个对象监视器
      2. 每一个被锁住的对象都会和Monitor关联起来
    4. 什么是管程monitor,C++源码解读
      1. ObjectMonitor java 一> ObjectMonitor.cpp 一> objectMonitor.hpp
      2. objectMonitor.hpp:属性及作用
        1. _owner:指向持有ObjectMoniter对象的线程
        2. _WaitSet :存放处于wait状态的线程队列
        3. _EntryList :存放处于等待锁block状态的线程队列
        4. _recursions:锁的重入次数
        5. _count:用来记录该线程获取锁的次数
    5. 在Hotspot虚拟机中,monitor采用ObjectMonitor实现
      1. synchronized必须作用于某个对象中,所以Java在对象的头文件存储了锁的相关信息。锁升级功能主要依赖于 MarkWord 中的锁标志。位和释放偏向锁标志位,后续讲解锁升级时候我们再加深,
      2. JUC高级-0608_第3张图片

4.4 公平锁和非公平锁

  1. 从ReentrantLock卖票编码演示公平和非公平现象
  2. 何为公平锁/非公平锁
    1. 公平:是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的
      Lock lock = new ReentrantLock(true);/true 表示公平锁,先来先得
    2. 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)
      Lock lock = new ReentrantLock(false);/false 表示非公平锁,后来的也可能先获得锁
      Lock lock = new ReentrantLock(/默认非公平锁
  3. 为什么会有公平锁/非公平锁的设计?为什么默认非公平?
    1. 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间养存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
    2. 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
  4. 为什么非公平锁效率更高
    1. 非公平锁在锁被释放时不保证任何特定的线程获取锁。如果一个线程刚好在锁被释放时请求锁,那么这个线程可以立即获取锁,而不需要经过上下文切换。这可以大大减少上下文切换的开销,提高效率。
  5. 什么时候用公平?什么时候用非公平
    1. 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。
  6. 底层:AbstractQueuedSynchronizer简称AQS

4.5 可重入锁(递归锁)

  1. 指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
    简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
    如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚
    所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
  2. 可重入锁种类:
    1. 隐式锁(即synchronized关键字使用的锁)默认是可重入锁:由于底层设计原理,天生自带可重入属性
      1. 同步方法、同步块
    2. Synchronized的重入的实现机理:
      1. 每个锁对象拥有一个锁计数器(_count)和一个指向持有该锁的线程的指针(_owner)
      2. 当执行monitorenter,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
      3. 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程择放该锁。
      4. 当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁己被释放。
    3. 显式锁(即Lock)也有ReentrantLock这样的可重入锁:
      1. 必须lock和unlock一一匹配,每一层嵌套都要匹配起来,虽然自己的线程依然可以进入,但是由于锁有一个线程监视器,所以不进行unlock的话,其他线程会被卡住,自己不会被卡住。

4.6 死锁及排查

  1. 定义:死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进租的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
  2. 哪些会导致死锁:原因
    1. 系统资源不足
    2. 进程运行推进的顺序不合适
    3. 资源分配不当
  3. 如何排查死锁
    1. jps -l 查看进程号
    2. jstack pid -> Found deadlock
    3. 或图形化jconsole

你可能感兴趣的:(JUC,java,开发语言)