重新看JUC课程,选择周阳讲的JUC
1.前置知识
lombok插件
Lombok是一个Java库,它通过注解的方式,能够在编译时自动为类生成构造函数、getters、setters、equals、hashCode和toString方法,以及其他常用方法,从而使我们的代码更加简洁,更易于阅读和编写。
@AllArgsConstructor
:这个注解会生成一个包含所有字段的构造函数,这个构造函数的参数顺序与字段在类中声明的顺序一致。@NoArgsConstructor
:这个注解会生成一个无参数的默认构造函数。@Data
:这个注解包含了@ToString,@EqualsAndHashCode,@Getter/@Setter和@RequiredArgsConstructor的功能,即:为类提供读写属性,同时生成equals,canEqual,hashCode,toString方法,以及参数为final的构造方法。所以,如果一个类被@Data注解,那么这个类就拥有了以上这些基本的数据操作功能。
方法引用
在Java 8中,方法引用是一种简化Lambda表达式的写法。方法引用可以更简洁、更直观地表示现有的方法、构造方法或者特定类型的任意对象的实例方法。
方法引用有以下四种形式:
- 静态方法引用:如果函数签名和定义已存在的静态方法签名一致,就可以使用静态方法引用。
// Lambda 表达式 Consumer<String> lambdaConsumer = (String s) -> System.out.println(s); // 方法引用 Consumer<String> methodRefConsumer = System.out::println;
- 特定实例的实例方法引用:如果函数签名和特定实例对象的某个实例方法一致,就可以使用特定实例的实例方法引用。
String str = "abc"; Predicate<String> lambdaPredicate = (String s) -> str.startsWith(s); Predicate<String> methodRefPredicate = str::startsWith;
- 任意对象的实例方法引用:如果函数签名和某个类的实例方法一致,就可以使用任意对象的实例方法引用。
Predicate<String> lambdaPredicate = (String s) -> s.isEmpty(); Predicate<String> methodRefPredicate = String::isEmpty;
- 构造方法引用:如果函数签名和构造方法一致,就可以使用构造方法引用。
Supplier<List<String>> lambdaSupplier = () -> new ArrayList<>(); Supplier<List<String>> methodRefSupplier = ArrayList::new;
总的来说,方法引用是一种让你可以重复使用已有方法的功能。在许多情况下,它们可以使你的代码更简洁、更清晰。
2.线程基础复习
- 多线程的优势和劣势:
- 优势:
- 充分利用多核处理器
- 提高程序性能,高并发系统
- 提高程序吞吐量,异步+回调等生产需求
- 劣势:
- 线程安全问题:i++,集合线程安全问题
- 线程锁问题:synchronized过重,怎样使用更灵活的锁
- 线程性能问题:
- 优势:
- 从start一个线程说起:
- Thread类中的start是一个同步方法,内部调用了一个start0的本地方法,是用C++写的
- C++就是JDK的JDK,Java语言就是C++的简化版。
- openjdk的写JNI一般是一一对应的,Thread.java对应的就是Thread.c
- 读C++远码:
- thread.c : openjdk8\jdk\src\share\native\java\lang
- jvm.cpp : openjdk8\hotspot\src\share\vm\prims
Thread::start(native_ thread);
- Thread.cpp : openjdk8\hotspot\src\share\vm\runtime
os::start_ thread(thread); // 操作系统提供分配的线程
- 总结:线程分配是JVM结合操作系统进行分配的
- Java多线程相关概念:
- 一把锁:synchronized
- 两个并:并行和并发
- 3个程:进程、线程、管程
- 管程:Monitor,监视器,也就是我们说的锁,
synchronized(Monitor){} // monitor就是一个监视器,也就是一个锁
- Monitor :是一种同步机制,他的义务是保证同时间只有一个线程可以访问被保护的数据和代码
- JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象,
- Monitor对象会和Java对象一同创建并销毁,它底层是由C++语言来实现的。
- 执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。
- 管程:Monitor,监视器,也就是我们说的锁,
- 用户线程和守护线程
- Java线程分为两种,用户线程和守护线程
- 一般而已说的都是用户线程
- 守护线程用于在后台完成一些必要的操作,例如垃圾回收线程
- 当用户线程运行时,守护线程会一直运行;用户线程结束,守护线程也会随之结束
- 线程的daemon属性:
- 源码解读:
public final boolean isDaemon() { return daemon; }
- true就是守护线程,false是用户线程
- code演示
- main线程也是用户线程,main线程结束了,用户线程并不一定结束
- 总结:
- 如果用户线程全部结束意味着程序需要完成的业务操作已经结束了,守护线程随着JVM一同结束工作
- setDaemon(true)方法必须在start()之前设置,否则报llegalThreadState Exception异常
- 源码解读:
- Java线程分为两种,用户线程和守护线程
3.CompletableFuture
3.1 Future接口理论复习
Future接口(FutureTask实现类)定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。
比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,忙其它事情或者先执行完,过了一会才去获取子任务的执行结果或变更的行务状态。
Future又称异步任务接口:
一句话:Future接口可以为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务。
3.2 Future接口常用实现类Future Task异步任务
- Future接口可以做什么
- Future是Java5新加的一个接口,它提供了一种异步并行计算的功能。如果主线程需要执行一个很耗时的计算任务,我们就可以通过future把这个任务放到异步线程中执行。主线程继续处理其他任务或者先行结束,再通过Future获取计算结果。
- 本源的Future接口相关架构
- 代码说话:
- Runnable接口 : 实现run方法,无返回值,不抛出异常
- Callable接口 :实现call方法,有返回值,抛出异常
- Future接口和FutureTask实现类
- 目的:异步多线程任务执行且返回有结果,
- 三个特点:多线程/有返回/异步任务
- 存在问题:我如果想创建一个线程实现类,需要多线程、有返回值、异步任务,则需要实现callable接口,但是使用Thread创建线程必须传Runnable类型参数,所以我们去找runnable的子类。有没有可以满足的:RunnableFuture<T>,这个接口有一个实现类:FutrueTask,但是FutureTask没有实现Callable接口,不过他支持构造注入。
- FutrueTask两种构造:不支持空参构造
- FutureTask(Callable <V> callable)
- FutureTask( Runnable runnable , V result)
- 代码实例:
// 如何获得一个带有返回值的异步多线程任务,结合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"; } }
- 代码说话:
- Future编码实战和优缺点分析
- 优点:
- future+线程池异步多线程任务配合,能显著提高程序的执行效率。
- 代码说话:
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个任务都交给主线程做,需要消耗1100ms,交给3个异步线程,800ms
- 缺点:
- get因为是异步的,一般建议放在程序最后,不然会阻塞主线程的其他任务执行
- 我希望可以过时不侯,可以自动离开:futureTask.get(3,TimeUnit.SECONDS);这个方法会抛出超时异常,不过可能会影响其他程序执行。
- isDone()轮询空转,消耗系统资源,而且也不见得能及时获取结果
- 如果想要异步获取结果,通常都会以轮询的方式去获取结果,尽量不要阻塞。
- 结论:Future对于结果的获取并不友好,只能轮询或者阻塞的方式去获取
- 优点:
- 完成一些复杂的任务
- 对于简单的业务场景使用Future完全OK
- 回调通知
- 应对Future的完成时间,完成了可以告诉我,也就是我们的回调通知
- 通过轮询的方式去判断任务是否完成这样非常古cPU并且代码也不优雅
- 创建异步任务:Future+线程池配合
- 多个任务前后依赖可以组合处理(水煮鱼)Future做不到
- 想将多个异步任务的计算结果组合起来,后一个异步任务的计算结果需要前一个异步任务的值
- 将两个或多个异步计算合成一个异步计算,这几个异步计算互相独立,同时后面这个又依赖前一个处理的结果。
- 对计算速度选最快
- 当Future集合中某个任务最快结束时,返回结果,返回第一名处理结果。
- Future不足以胜任复杂任务:
- 使用Future之前提供的那点API就囊中羞涩,处理起来不够优雅,这时候还是让CompletableFuture 以声明式的方式优雅的处理这些需求。
- 从i到i++,o(n_n)0哈哈~
- Future能干的,CompletableFuture都能干
3.3 CompletableFuture对Future的改进
- Completable为什么出现?
- get()方法在Future 计算完成之前会一直处在阻寨状态下,
- isDone()方法容易耗费CPU资源,
- 对于真正的异步处理我们希望是可以通过传入回调函数,在Future结束时自动调用该回调函数,这样,我们就不用等待结果。
- 阻寨的方式和异步编程的设计理念相违背,而轮询的方式会耗费无谓的CPU资源。因此,JDK8设计出CompletableFuture。
- CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。
- CompletableFuture 和CompletionStage源码分别介绍
- 类架构说明
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
- 接口CompletionStage
- CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段
- 一个阶段的计算执行可以是一个Function,Consumer或者Runnable。
- 一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发
- 代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段,有些类似Linux系统的管道分隔符传参数。
- 在实际操作中,就是.whenComplete 和 .exceptionally 两个方法。
- 类CompletableFuture
- 在Java8中,CompletableFuture提供了非常强大的Future的扩展 可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合 CompletableFuture 的方法。
- 它可能代表一个明确完成的Future,也有可能代表一个完成阶段(CompletionStage),它支持在计算完成以后触发一些西数或执行某些动作。
- 它实现了 Future和CompletionStage接口
- 类架构说明
- 核心的四个静态方法,来创建一个异步任务(不推荐使用new CompletableFuture()去获得,创建的是不完备的)
0. 两组四个方法:- runAsync 无返回值
- public static Completable Future <Void> runAsync(Runnable runnable)
- public static CompletableFuture <Void> runAsync(Runnable runnable,Executor executor)
- supplyAsync 有返回值(常用)
- public static <U> Completable Future<U> supplyAsync(Supplier <U> supplier)
- public static <U> CompletableFuture <U> supplyAsync(Supplier <U> supplier, Executor executor)
- 上述Executor executor参数说明
- 没有指定Executor的方法,直接使用默认的Fork JoinPool.commonPool(),作为它的线程池执行异步代码。
- 如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码
- 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();
- code通用演示,减少阻塞和轮询:
- 从Java8开始引入了CompletableFuture,它是Future的功能增强版,减少阻塞和轮询可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法
- 解释下为什么默认线程池关闭,自定义线程池自动关闭
- 主线程不能立刻结束,否则CompletableFuture就以使用的线程池会立刻关闭:暂停3秒钟线程
- completable线程类似守护线程,主线程完成之后会关闭
- 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(); }
- Completable优点:
- 异步任务结束时,会自动回调某个对象的方法:
- 主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行
- 异步任务出错时,会自动回调某个对象的方法;
- 和ajax很像
- runAsync 无返回值
3.4 函数式编程
复习:
- Runnable,无参数,无返回值
@FunctionalInterface public interface Runnable { public abstract void run(); }
- Function<T,R>:功能性函数接口:接收一个参数,并且有返回值
@FunctionalInterface public interface Function<T, R> { R apply(T t); }
- Consumer<T> : 消费者型接口:接收一个参数,无返回值
@FunctionalInterface public interface Consumer<T> { void accept(T t); }
- Supplier:供给者型接口,无参数,有返回值
@FunctionalInterface public interface Supplier<T> { T get(); }
- BiConsumer : 双参数消费者,两个参数,无返回值
@FunctionalInterface public interface BiConsumer<T, U> { void accept(T t, U u); }
- 小总结
函数式接口名称 | 方法名称 | 参数 | 返回值 |
---|---|---|---|
Runnable | run | 无参数 | 无返回值 |
Function | apply | 1个参数 | 有返回值 |
Consumer | accept | 1个参数 | 无返回值 |
Supplier | get | 无参数 | 有返回值 |
BiComsumer | accept | 2个参数 | 无返回值 |
- 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"); }
- join和get对比:都可以从conpletable里面取值:
- 作用几乎一样,只是join不抛出异常
3.5 案例精讲-电商网站比价需求
- 需求说明:
- 同一款产品,同时搜索出同款产品在各大电商平台的售价:
- 同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少
- 输出返回:
- 出来结果希望是同款产品的在不同地方的价格清单列表,返回一个List<String>
- 《mysql》 in jd price is 88.05
- 《mysql》 in dangdang price is 86.11
- 《mysql》 in taobao price is 90.43
- 解决方案,比对同一个商品在各个平台上的价格,要求获得一个清单列表,
- step by step,按部就班,查完京东查淘宝,查完淘宝查天猫……
- all in. 万箭齐发,一口气多线程异步任务同时查询。。。。。
- 核心逻辑写法:
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<CompletableFuture<String>> .collect(Collectors.toList()) // List<CompletableFuture<String>> .stream() // Stream<CompletableFuture<String>> .map(CompletableFuture::join) //Stream<String> .collect(Collectors.toList()); // List<String> } 以上是周阳代码,不知道为什么不这么写,减少一次来回转换 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常用方法
- Completable实现了Future和CompletionStage,前者方法少功能弱,后者方法多。现以分类的方式介绍Completable的方法。
- 获得结果和触发计算
- 获取结果
- public T get() 一定要拿到结果,容易阻塞
- public T get(long timeout, TimeUnit unit) 过时不候
- public T join() 和get一样,只是没有异常
- public T getNow (T valuelfAbsent) 现在取值,如果没完成给一个替代结果,不阻塞
- 主动触发计算
- public boolean complete(T value):返回是否打断了get阻塞,打断的话用替代值,没打断用计算值
-
// 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());
- 获取结果
- 对计算结果进行处理
- thenApply:(一般常用)
- 计算结果存在依赖关系,这两个线程串行化
- completable默认使用forkJoinPool线程池,如果主线程结束,他也会自动消失
- 异常:由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停。
-
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());
- handle
- 计算结果存在依赖关系,这两个线程串行化,这一点和上面一样
- 区别:有异常也可以往下一步走,根据带的异常参数可以进一步处理,只能走一步。
-
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());
- 总结:
- Exceptionally : try/catch
- whenComplete / handle -> try/finally
- thenApply:(一般常用)
- 对计算结果进行消费
- 接收任务的处理结果,并消费处理,无返回结果
- thenAccept
CompletableFuture.supplyAsync(() -> { return 1; }).thenApply(f -> { return f+2; }).thenApply(f -> { return f+3; }).thenAccept(System.out::println);
- 对比补充:code之间执行顺序问题:
- thenRun
- thenRun(Runnable runnable)
- 任务A 执行完执行 B,并且R不需要A 的结果
-
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenRun(() -> {}).join()); // A忙完了忙B,两者没有依赖,只是有顺序关系
- thenAccept
- thenAccept(Consumer action)
- 任务A执行完执行 B,B需要A的结果,但是任务B无返回值
-
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenAccept(resultA -> {}).join()); // A忙完了,B需要A的结果,有顺序也有依赖关系,但是B无返回值
- thenApply
- thenApply(Function fn)
- 任务 A执行完执行 B,B需要A的结果,同时任务B有返回值
-
System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenApply(resultA -> resultA + " resultB").join()); // B需要A的结果,并且还有返回值
- thenRun
- CompletableFuture和线程池说明
- 以thenRun和thenRunAsync为例,有什么区别?
- 小总结:
- 没有传入自定义线程池,都用默认线程池ForkJoinPool
- 传入了一个自定义线程池,如果你执行第一个任务的时候,传入了一个自定义线程池:
- 调用thenRun方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池。
- 调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池
- 备注:有可能处理太快,系统优化切换原则,直接使用main线程处理
- 其它如: thenAccept和thenAcceptAsync, thenApply和thenApplyAsync等,它们之间的区别也是同理
- 源码分析:判断用户cpu核数,一般都大于1,所以用了Async默认就是用forkJoinPool
- 对计算速度进行选用
- 谁快用谁:
- applyToEither
-
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());
- 对计算结果进行合并
- 两个CompletionStage任务都完成后,最终能把两个任务的结果一起交给thenCombine来处理先完成的先等着,等待其它分支任务
- thenCombine
-
System.out.println(CompletableFuture.supplyAsync(() -> { return 10; }).thenCombine(CompletableFuture.supplyAsync(() -> { return 20; }), (r1, r2) -> { return r1 + r2; }).join());
- 二者的结果合并之后,依然可以继续合并。
4. Java多线程锁
4.1 乐观锁和悲观锁
- 悲观锁
- 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
- synchronized关键字和Lock的实现类都是悲观锁
- 适合写操作多的场景,先加锁可以保证写操作时数据正确。显式的锁定之后再操作同步资源
- 一句话:狼性锁
- 乐观锁
- 认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁。
- 在Java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。
- 如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
- 如果这个数据己经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等
- 判断规则
- 版本号机制Version
- 最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
4.2 8锁案例展示锁的原理
- 锁相关的8种案例演示code,和JUC基础里面的8锁案例一样,不再次记录。
- 本质上,锁的范围;是否同一把锁
4.3 synchronized三种方式及底层原理
- synchronized三种应用方式
- 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
- 作用于代码块,对括号里配置的对象加锁。
- 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
- synchronized底层原理:
- 同步代码块:实现使用的是monitorenter和monitorexit指令
- 现象:会有一个enter和两个exit:原因:为了避免异常导致锁无法退出,自动进行退出
- 相比synchronized,lock就必须写在finally里面保证必然释放
- 一定是一个enter对应两个exit吗?
- 默认情况是的
- 如果一个同步块必然会抛出异常,那么第一个exit就省略掉了
- 普通同步方法:ACC_SYNCHRONIZED
- 调用指念将会检查方法的ACCSYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitgr锁,然后再执行方法,最后在方法完成(无论是正常完成还是非馆常完成)时释放monitor
- 静态同步方法:ACC_PUBLIC, ACC_ SYNCHRONIZED
- 同步代码块:实现使用的是monitorenter和monitorexit指令
- synchronized锁的是什么?
0. 管程:- 管程(英语:Monitors,也称为监视器)是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般定硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
- 同步指令:Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor,更常见的是直接将它称为“锁”)来实现的。
方法级的同步是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟
机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为
同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如
果设置了,执行线程就要求先成功持有管程,然后才能热行方法,最后当方法完成(无论是正當完成
还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同
步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。 - 为什么任何一个对象都能成为锁
- 每个对象天生都带着一个对象监视器
- 每一个被锁住的对象都会和Monitor关联起来
- 什么是管程monitor,C++源码解读
- ObjectMonitor java 一> ObjectMonitor.cpp 一> objectMonitor.hpp
- objectMonitor.hpp:属性及作用
- _owner:指向持有ObjectMoniter对象的线程
- _WaitSet :存放处于wait状态的线程队列
- _EntryList :存放处于等待锁block状态的线程队列
- _recursions:锁的重入次数
- _count:用来记录该线程获取锁的次数
- 在Hotspot虚拟机中,monitor采用ObjectMonitor实现
- synchronized必须作用于某个对象中,所以Java在对象的头文件存储了锁的相关信息。锁升级功能主要依赖于 MarkWord 中的锁标志。位和释放偏向锁标志位,后续讲解锁升级时候我们再加深,
4.4 公平锁和非公平锁
- 从ReentrantLock卖票编码演示公平和非公平现象
- 何为公平锁/非公平锁
- 公平:是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的
Lock lock = new ReentrantLock(true);/true 表示公平锁,先来先得 - 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)
Lock lock = new ReentrantLock(false);/false 表示非公平锁,后来的也可能先获得锁
Lock lock = new ReentrantLock(😕/默认非公平锁
- 公平:是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的
- 为什么会有公平锁/非公平锁的设计?为什么默认非公平?
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间养存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
- 为什么非公平锁效率更高
- 非公平锁在锁被释放时不保证任何特定的线程获取锁。如果一个线程刚好在锁被释放时请求锁,那么这个线程可以立即获取锁,而不需要经过上下文切换。这可以大大减少上下文切换的开销,提高效率。
- 什么时候用公平?什么时候用非公平
- 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。
- 底层:AbstractQueuedSynchronizer简称AQS
4.5 可重入锁(递归锁)
- 指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。 - 可重入锁种类:
- 隐式锁(即synchronized关键字使用的锁)默认是可重入锁:由于底层设计原理,天生自带可重入属性
- 同步方法、同步块
- Synchronized的重入的实现机理:
- 每个锁对象拥有一个锁计数器(_count)和一个指向持有该锁的线程的指针(_owner)
- 当执行monitorenter,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
- 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程择放该锁。
- 当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁己被释放。
- 显式锁(即Lock)也有ReentrantLock这样的可重入锁:
- 必须lock和unlock一一匹配,每一层嵌套都要匹配起来,虽然自己的线程依然可以进入,但是由于锁有一个线程监视器,所以不进行unlock的话,其他线程会被卡住,自己不会被卡住。
- 隐式锁(即synchronized关键字使用的锁)默认是可重入锁:由于底层设计原理,天生自带可重入属性
4.6 死锁及排查
- 定义:死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进租的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
- 哪些会导致死锁:原因
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
- 如何排查死锁
- jps -l 查看进程号
- jstack pid -> Found deadlock
- 或图形化jconsole