当前位置:   article > 正文

JUC总结

JUC总结

四大口决

  1. 高内聚低耦合前提下,封装思想。线程---操作---资源类
  2. 判断、干活、通知
  3. 防止虚假唤醒,wait方法要注意使用while判断
  4. 注意标志位flag,可能是volatile的

1. 线程基础知识复习

为什么多线程极其重要??

硬件方面

摩尔定律:

它是由英特尔创始人之一Gordon Moore(戈登·摩尔)提出来的。其内容为:

当价格不变时,集成电路上可容纳的元器件的数目约每隔18-24个月便会增加一倍,性能也将提升一倍。

换言之,每一美元所能买到的电脑性能,将每隔18-24个月翻一倍以上。这一定律揭示了信息技术进步的速度。

可是从2003年开始CPU主频已经不再翻倍,而是采用多核而不是更快的主频。

摩尔定律失效。

在主频不再提高且核数在不断增加的情况下,要想让程序更快就要用到并行或并发编程。

软件方面

高并发系统,异步+回调等生产需求

从start一个线程说起

Java线程理解以及openjdk中的实现
  1. private native void start0();
  2. Java语言本身底层就是C++语言
  3. OpenJDK源码网址:OpenJDK ----openjdk8\hotspot\src\share\vm\runtime
更加底层的C++源码解读
  1. openjdk8\jdk\src\share\native\java\lang

  1. openjdk8\hotspot\src\share\vm\prims

  1. openjdk8\hotspot\src\share\vm\runtime

Java多线程相关概念

进程

是程序的⼀次执⾏,是系统进⾏资源分配和调度的独⽴单位,每⼀个进程都有它⾃⼰的内存空间和系统资源

线程

在同⼀个进程内⼜可以执⾏多个任务,⽽这每⼀个任务我们就可以看做是⼀个线程(⼀个进程会有1个或多个线程的)

面试题:何为进程和线程?

管程

Monitor其实是一种同步机制,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。

JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象,

  1. Object o = new Object();
  2. new Thread(() -> {
  3. synchronized (o)
  4. {
  5. }
  6. },"t1").start();

Monitor对象会和Java对象一同创建并销毁,它底层是由C++语言来实现的。

用户线程和守护线程

Java线程分为用户线程和守护线程,线程的daemon属性为true表示是守护线程,false表示是用户线程

守护线程

是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程

用户线程

是系统的工作线程,它会完成这个程序需要完成的业务操作

  1. package com.atguigu.itdachang;
  2. /**
  3. * @auther zzyy
  4. * @create 2020-07-07 15:39
  5. */
  6. public class DaemonDemo
  7. {
  8. public static void main(String[] args)
  9. {
  10. Thread t1 = new Thread(() -> {
  11. System.out.println(Thread.currentThread().getName()+"\t 开始运行,"+(Thread.currentThread().isDaemon() ? "守护线程":"用户线程"));
  12. while (true) {
  13. }
  14. }, "t1");
  15. //线程的daemon属性为true表示是守护线程,false表示是用户线程
  16. t1.setDaemon(true);
  17. t1.start();
  18. //3秒钟后主线程再运行
  19. try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
  20. System.out.println("----------main线程运行完毕");
  21. }
  22. }
重点
  1. 当程序中所有用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出。如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以当系统只剩下守护进程的时候,java虚拟机会自动退出
  2. 设置守护线程,需要在start()方法之前进行

2. CompletableFuture

Future和Callable接口

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

Callable接口中定义了需要有返回的任务需要实现的方法。

比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,

主线程就去做其他事情了,过了一会才去获取子任务的执行结果。

从之前的FutureTask说开去

get()阻塞

一旦调用get()方法,不管是否计算完成都会导致阻塞,o(╥﹏╥)o

  1. package com.zzyy.study.test;
  2. import java.util.concurrent.*;
  3. /**
  4. * @auther zzyy
  5. * @create 2020-06-14 17:02
  6. */
  7. public class CompletableFutureDemo
  8. {
  9. public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException
  10. {
  11. FutureTask<String> futureTask = new FutureTask<>(() -> {
  12. System.out.println("-----come in FutureTask");
  13. try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
  14. return ""+ThreadLocalRandom.current().nextInt(100);
  15. });
  16. Thread t1 = new Thread(futureTask,"t1");
  17. t1.start();
  18. //3秒钟后才出来结果,还没有计算你提前来拿(只要一调用get方法,对于结果就是不见不散,会导致阻塞)
  19. //System.out.println(Thread.currentThread().getName()+"\t"+futureTask.get());
  20. //3秒钟后才出来结果,我只想等待1秒钟,过时不候
  21. System.out.println(Thread.currentThread().getName()+"\t"+futureTask.get(1L,TimeUnit.SECONDS));
  22. System.out.println(Thread.currentThread().getName()+"\t"+" run... here");
  23. }
  24. }
isDone()轮询

轮询的方式会耗费无谓的CPU资源,而且也不见得能及时地得到计算结果.

如果想要异步获取结果,通常都会以轮询的方式去获取结果,尽量不要阻塞

  1. package com.zzyy.study.test;
  2. import java.util.concurrent.ExecutionException;
  3. import java.util.concurrent.FutureTask;
  4. import java.util.concurrent.ThreadLocalRandom;
  5. import java.util.concurrent.TimeUnit;
  6. /**
  7. * @auther zzyy
  8. * @create 2020-06-16 20:16
  9. */
  10. public class CompletableFutureDemo2
  11. {
  12. public static void main(String[] args) throws ExecutionException, InterruptedException
  13. {
  14. FutureTask<String> futureTask = new FutureTask<>(() -> {
  15. System.out.println("-----come in FutureTask");
  16. try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
  17. return ""+ThreadLocalRandom.current().nextInt(100);
  18. });
  19. new Thread(futureTask,"t1").start();
  20. System.out.println(Thread.currentThread().getName()+"\t"+"线程完成任务");
  21. /**
  22. * 用于阻塞式获取结果,如果想要异步获取结果,通常都会以轮询的方式去获取结果
  23. */
  24. while(true)
  25. {
  26. if (futureTask.isDone())
  27. {
  28. System.out.println(futureTask.get());
  29. break;
  30. }
  31. }
  32. }
  33. }
想完成一些复杂的任务
  1. 应对Future的完成时间,完成了可以告诉我,也就是我们的回调通知
  2. 将两个异步计算合成一个异步计算,这两个异步计算互相独立,同时第二个又依赖第一个的结果。
  3. 当Future集合中某个任务最快结束时,返回结果。
  4. 等待Future结合中的所有任务都完成。

对Future的改进

CompletableFuture和CompletionStage源码分别介绍

接口CompletionStage

类CompletableFuture

核心的四个静态方法,来创建一个异步操作
runAsync 无 返回值
  1. public static CompletableFuture<Void> runAsync(Runnable runnable)
  2. public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor)
  1. package com.zzyy.study.test;
  2. import java.util.concurrent.CompletableFuture;
  3. import java.util.concurrent.ExecutionException;
  4. import java.util.concurrent.TimeUnit;
  5. /**
  6. * @auther zzyy
  7. * @create 2020-06-16 20:16
  8. */
  9. public class CompletableFutureDemo2
  10. {
  11. public static void main(String[] args) throws ExecutionException, InterruptedException
  12. {
  13. CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
  14. System.out.println(Thread.currentThread().getName()+"\t"+"-----come in");
  15. //暂停几秒钟线程
  16. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  17. System.out.println("-----task is over");
  18. });
  19. System.out.println(future.get());
  20. }
  21. }

supplyAsync 有 返回值
  1. public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
  2. public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor)
  1. package com.zzyy.study.test;
  2. import java.util.concurrent.CompletableFuture;
  3. import java.util.concurrent.ExecutionException;
  4. import java.util.concurrent.ThreadLocalRandom;
  5. import java.util.concurrent.TimeUnit;
  6. /**
  7. * @auther zzyy
  8. * @create 2020-06-16 20:16
  9. */
  10. public class CompletableFutureDemo2
  11. {
  12. public static void main(String[] args) throws ExecutionException, InterruptedException
  13. {
  14. CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
  15. System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
  16. //暂停几秒钟线程
  17. try {
  18. TimeUnit.SECONDS.sleep(1);
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. return ThreadLocalRandom.current().nextInt(100);
  23. });
  24. System.out.println(completableFuture.get());
  25. }
  26. }
上述Executor executor参数说明

没有指定Executor的方法,直接使用默认的ForkJoinPool.commonPool() 作为它的线程池执行异步代码。

如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码

Code之通用演示,减少阻塞和轮询

从Java8开始引入了CompletableFuture,它是Future的功能增强版,

可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法

  1. package com.atguigu.juc.senior.inner.completablefuture;
  2. import java.util.concurrent.CompletableFuture;
  3. import java.util.concurrent.ThreadLocalRandom;
  4. import java.util.concurrent.TimeUnit;
  5. /**
  6. * @auther zzyy
  7. * @create 2020-06-23 12:59
  8. */
  9. public class cfuture4
  10. {
  11. public static void main(String[] args) throws Exception
  12. {
  13. CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
  14. System.out.println(Thread.currentThread().getName() + "\t" + "-----come in");
  15. int result = ThreadLocalRandom.current().nextInt(10);
  16. //暂停几秒钟线程
  17. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  18. System.out.println("-----计算结束耗时1秒钟,result: "+result);
  19. if(result > 6)
  20. {
  21. int age = 10/0;
  22. }
  23. return result;
  24. }).whenComplete((v,e) ->{
  25. if(e == null)
  26. {
  27. System.out.println("-----result: "+v);
  28. }
  29. }).exceptionally(e -> {
  30. System.out.println("-----exception: "+e.getCause()+"\t"+e.getMessage());
  31. return -44;
  32. });
  33. //主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:暂停3秒钟线程
  34. try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
  35. }
  36. }
CompletableFuture的优点
  1. 异步任务结束时,会自动回调某个对象的方法;
  2. 异步任务出错时,会自动回调某个对象的方法;
  3. 主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行

案例精讲-从电商网站的比价需求说开去

函数式编程已经主流

Lambda +Stream+链式调用+Java8函数式编程带走

大厂业务需求说明
  1. 经常出现在等待某条 SQL 执行完成后,再继续执行下一条 SQL ,而这两条 SQL 本身是并无关系的,可以同时进行执行的。
  2. 我们希望能够两条 SQL 同时进行处理,而不是等待其中的某一条 SQL 完成后,再继续下一条。同理,
  3. 对于分布式微服务的调用,按照实际业务,如果是无关联step by step的业务,可以尝试是否可以多箭齐发,同时调用。
  4. 我们去比同一个商品在各个平台上的价格,要求获得一个清单列表,
  5. 1 step by step,查完京东查淘宝,查完淘宝查天猫......
  6. 2 all 一口气同时查询。。。。。

CompletableFuture常用方法
1. 获得结果和触发计算
  1. 获取结果
  2. public T get() 不见不散
  3. public T get(long timeout, TimeUnit unit) 过时不候
  4. public T getNow(T valueIfAbsent) 没有计算完成的情况下,给我一个替代结果
  5. 立即获取结果不阻塞 计算完,返回计算完成后的结果;没算完,返回设定的valueIfAbsent值
  6. package com.zzyy.study.test;
  7. import java.util.concurrent.CompletableFuture;
  8. import java.util.concurrent.ExecutionException;
  9. import java.util.concurrent.TimeUnit;
  10. /**
  11. * @auther zzyy
  12. * @create 2020-06-16 20:16
  13. */
  14. public class CompletableFutureDemo2
  15. {
  16. public static void main(String[] args) throws ExecutionException, InterruptedException
  17. {
  18. CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
  19. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  20. return 533;
  21. });
  22. //去掉注释上面计算没有完成,返回444
  23. //开启注释上满计算完成,返回计算结果
  24. try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
  25. System.out.println(completableFuture.getNow(444));
  26. }
  27. }
  28. public T join()
  29. package com.zzyy.study.test;
  30. import java.util.concurrent.CompletableFuture;
  31. import java.util.concurrent.ExecutionException;
  32. /**
  33. * @auther zzyy
  34. * @create 2020-06-16 20:16
  35. */
  36. public class CompletableFutureDemo2
  37. {
  38. public static void main(String[] args) throws ExecutionException, InterruptedException
  39. {
  40. System.out.println(CompletableFuture.supplyAsync(() -> "abc").thenApply(r -> r + "123").join());
  41. }
  42. }
  1. 主动触发计算
  2. public boolean complete(T value) 是否打断get方法立即返回括号值
  3. package com.zzyy.study.test;
  4. import java.util.concurrent.CompletableFuture;
  5. import java.util.concurrent.ExecutionException;
  6. import java.util.concurrent.TimeUnit;
  7. /**
  8. * @auther zzyy
  9. * @create 2020-06-16 20:16
  10. */
  11. public class CompletableFutureDemo2
  12. {
  13. public static void main(String[] args) throws ExecutionException, InterruptedException
  14. {
  15. CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
  16. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  17. return 533;
  18. });
  19. //注释掉暂停线程,get还没有算完只能返回complete方法设置的444;暂停2秒钟线程,异步线程能够计算完成返回get
  20. try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
  21. //当调用CompletableFuture.get()被阻塞的时候,complete方法就是结束阻塞并get()获取设置的complete里面的值.
  22. System.out.println(completableFuture.complete(444)+"\t"+completableFuture.get());
  23. }
  24. }
2. 对计算结果进行处理
  1. thenApply 计算结果存在依赖关系,这两个线程串行化
  2. 由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停。
  3. package com.zzyy.study.test;
  4. import java.util.concurrent.*;
  5. /**
  6. * @auther zzyy
  7. * @create 2020-06-16 20:16
  8. */
  9. public class CompletableFutureDemo2
  10. {
  11. public static void main(String[] args) throws ExecutionException, InterruptedException
  12. {
  13. //当一个线程依赖另一个线程时用 thenApply 方法来把这两个线程串行化,
  14. CompletableFuture.supplyAsync(() -> {
  15. //暂停几秒钟线程
  16. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  17. System.out.println("111");
  18. return 1024;
  19. }).thenApply(f -> {
  20. System.out.println("222");
  21. return f + 1;
  22. }).thenApply(f -> {
  23. //int age = 10/0; // 异常情况:那步出错就停在那步。
  24. System.out.println("333");
  25. return f + 1;
  26. }).whenCompleteAsync((v,e) -> {
  27. System.out.println("*****v: "+v);
  28. }).exceptionally(e -> {
  29. e.printStackTrace();
  30. return null;
  31. });
  32. System.out.println("-----主线程结束,END");
  33. // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
  34. try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
  35. }
  36. }
  37. handle 有异常也可以往下一步走,根据带的异常参数可以进一步处理
  38. package com.zzyy.study.test;
  39. import lombok.Getter;
  40. import lombok.Setter;
  41. import java.util.concurrent.CompletableFuture;
  42. import java.util.concurrent.ExecutionException;
  43. import java.util.concurrent.TimeUnit;
  44. /**
  45. * @auther zzyy
  46. * @create 2020-06-16 20:16
  47. */
  48. public class CompletableFutureDemo2
  49. {
  50. public static void main(String[] args) throws ExecutionException, InterruptedException
  51. {
  52. //当一个线程依赖另一个线程时用 handle 方法来把这两个线程串行化,
  53. // 异常情况:有异常也可以往下一步走,根据带的异常参数可以进一步处理
  54. CompletableFuture.supplyAsync(() -> {
  55. //暂停几秒钟线程
  56. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  57. System.out.println("111");
  58. return 1024;
  59. }).handle((f,e) -> {
  60. int age = 10/0;
  61. System.out.println("222");
  62. return f + 1;
  63. }).handle((f,e) -> {
  64. System.out.println("333");
  65. return f + 1;
  66. }).whenCompleteAsync((v,e) -> {
  67. System.out.println("*****v: "+v);
  68. }).exceptionally(e -> {
  69. e.printStackTrace();
  70. return null;
  71. });
  72. System.out.println("-----主线程结束,END");
  73. // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
  74. try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
  75. }
  76. }

3. 对计算结果进行消费
  1. 接收任务的处理结果,并消费处理,无返回结果
  2. thenAccept
  3. public static void main(String[] args) throws ExecutionException, InterruptedException
  4. {
  5. CompletableFuture.supplyAsync(() -> {
  6. return 1;
  7. }).thenApply(f -> {
  8. return f + 2;
  9. }).thenApply(f -> {
  10. return f + 3;
  11. }).thenApply(f -> {
  12. return f + 4;
  13. }).thenAccept(r -> System.out.println(r));
  14. }
  15. 补充
  16. Code之任务之间的顺序执行
  17. thenRun thenRun(Runnable runnable) 任务 A 执行完执行 B,并且 B 不需要 A 的结果
  18. thenAccept thenAccept(Consumer action) 任务 A 执行完执行 BB 需要 A 的结果,但是任务 B 无返回值
  19. thenApply thenApply(Function fn) 任务 A 执行完执行 BB 需要 A 的结果,同时任务 B 有返回值
  20. System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenRun(() -> {}).join());
  21. System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenAccept(resultA -> {}).join());
  22. System.out.println(CompletableFuture.supplyAsync(() -> "resultA").thenApply(resultA -> resultA + " resultB").join());
4. 对计算速度进行选用
  1. 谁快用谁
  2. applyToEither
  3. package com.zzyy.study.test;
  4. import java.util.concurrent.CompletableFuture;
  5. import java.util.concurrent.ExecutionException;
  6. import java.util.concurrent.TimeUnit;
  7. /**
  8. * @auther zzyy
  9. * @create 2020-06-16 20:16
  10. */
  11. public class CompletableFutureDemo2
  12. {
  13. public static void main(String[] args) throws ExecutionException, InterruptedException
  14. {
  15. CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(() -> {
  16. System.out.println(Thread.currentThread().getName() + "\t" + "---come in ");
  17. //暂停几秒钟线程
  18. try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
  19. return 10;
  20. });
  21. CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> {
  22. System.out.println(Thread.currentThread().getName() + "\t" + "---come in ");
  23. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  24. return 20;
  25. });
  26. CompletableFuture<Integer> thenCombineResult = completableFuture1.applyToEither(completableFuture2,f -> {
  27. System.out.println(Thread.currentThread().getName() + "\t" + "---come in ");
  28. return f + 1;
  29. });
  30. System.out.println(Thread.currentThread().getName() + "\t" + thenCombineResult.get());
  31. }
  32. }
5. 对计算结果进行合并
  1. 两个CompletionStage任务都完成后,最终能把两个任务的结果一起交给thenCombine 来处理
  2. 先完成的先等着,等待其它分支任务
  3. thenCombine
  4. 1. code标准版,好理解先拆分
  5. package com.zzyy.study.test;
  6. import java.util.concurrent.CompletableFuture;
  7. import java.util.concurrent.ExecutionException;
  8. /**
  9. * @auther zzyy
  10. * @create 2020-06-16 20:16
  11. */
  12. public class CompletableFutureDemo2
  13. {
  14. public static void main(String[] args) throws ExecutionException, InterruptedException
  15. {
  16. CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(() -> {
  17. System.out.println(Thread.currentThread().getName() + "\t" + "---come in ");
  18. return 10;
  19. });
  20. CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> {
  21. System.out.println(Thread.currentThread().getName() + "\t" + "---come in ");
  22. return 20;
  23. });
  24. CompletableFuture<Integer> thenCombineResult = completableFuture1.thenCombine(completableFuture2, (x, y) -> {
  25. System.out.println(Thread.currentThread().getName() + "\t" + "---come in ");
  26. return x + y;
  27. });
  28. System.out.println(thenCombineResult.get());
  29. }
  30. }
  31. 2. code表达式
  32. package com.zzyy.study.test;
  33. import java.util.concurrent.CompletableFuture;
  34. import java.util.concurrent.ExecutionException;
  35. /**
  36. * @auther zzyy
  37. * @create 2020-06-16 20:16
  38. */
  39. public class CompletableFutureDemo2
  40. {
  41. public static void main(String[] args) throws ExecutionException, InterruptedException
  42. {
  43. CompletableFuture<Integer> thenCombineResult = CompletableFuture.supplyAsync(() -> {
  44. System.out.println(Thread.currentThread().getName() + "\t" + "---come in 1");
  45. return 10;
  46. }).thenCombine(CompletableFuture.supplyAsync(() -> {
  47. System.out.println(Thread.currentThread().getName() + "\t" + "---come in 2");
  48. return 20;
  49. }), (x,y) -> {
  50. System.out.println(Thread.currentThread().getName() + "\t" + "---come in 3");
  51. return x + y;
  52. }).thenCombine(CompletableFuture.supplyAsync(() -> {
  53. System.out.println(Thread.currentThread().getName() + "\t" + "---come in 4");
  54. return 30;
  55. }),(a,b) -> {
  56. System.out.println(Thread.currentThread().getName() + "\t" + "---come in 5");
  57. return a + b;
  58. });
  59. System.out.println("-----主线程结束,END");
  60. System.out.println(thenCombineResult.get());
  61. // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
  62. try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
  63. }
  64. }

3. 说说Java"锁"事

从轻松的乐观锁和悲观锁开讲

悲观锁

认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。

synchronized关键字和Lock的实现类都是悲观锁

适合写操作多的场景,先加锁可以保证写操作时数据正确。显式的锁定之后再操作同步资源

伪代码说明
  1. //=============悲观锁的调用方式
  2. public synchronized void m1()
  3. {
  4. //加锁后的业务逻辑......
  5. }
  6. // 保证多个线程使用的是同一个lock对象的前提下
  7. ReentrantLock lock = new ReentrantLock();
  8. public void m2() {
  9. lock.lock();
  10. try {
  11. // 操作同步资源
  12. }finally {
  13. lock.unlock();
  14. }
  15. }
  16. //=============乐观锁的调用方式
  17. // 保证多个线程使用的是同一个AtomicInteger
  18. private AtomicInteger atomicInteger = new AtomicInteger();
  19. atomicInteger.incrementAndGet();

乐观锁

乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。

如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作

乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的

适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再抢

乐观锁一般有两种实现方式:

  1. 采用版本号机制
  2. CAS(Compare-and-Swap,即比较并替换)算法实现

通过8种情况演示锁运行案例,看看我们到底锁的是什么

承前启后的复习一下

看看JVM中对应的锁在哪里?

synchronized有三种应用方式
JDK源码(notify方法)说明举例

8种锁的案例实际体现在3个地方

作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;

作用于代码块,对括号里配置的对象加锁。

作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;

从字节码角度分析synchronized实现
javap -c ***.class文件反编译 -c 对代码进行反汇编

javap -v ***.class文件反编译

-v -verbose 输出附加信息(包括行号、本地变量表,反汇编等详细信息)

synchronized同步代码块

javap -c ***.class文件反编译

synchronized同步代码块 实现使用的是monitorenter和monitorexit指令

一定是一个enter两个exit吗?

m1方法里面自己添加一个异常试试

synchronized普通同步方法

javap -v ***.class文件反编译

调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。

如果设置了,执行线程会将先持有monitor然后再执行方法,

最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor

synchronized静态同步方法

ACC_STATIC, ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法

反编译synchronized锁的是什么
什么是管程monitor

管程 (英语:Monitors,也称为监视器) 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。

这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。

在HotSpot虚拟机中,monitor采用ObjectMonitor实现

上述C++源码解读
  1. ObjectMonitor.java→ObjectMonitor.cpp→objectMonitor.hpp

ObjectMonitor中有几个关键属性

_owner 指向持有ObjectMonitor对象的线程

_WaitSet 存放处于wait状态的线程队列

_EntryList 存放处于等待锁block状态的线程队列

_recursions 锁的重入次数

_count 用来记录该线程获取锁的次数

每个对象天生都带着一个对象监视器

公平锁和非公平锁

从ReentrantLock卖票编码演示公平和非公平现象
  1. package com.atguigu.juc.senior.test;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. class Ticket
  4. {
  5. private int number = 30;
  6. ReentrantLock lock = new ReentrantLock();
  7. public void sale()
  8. {
  9. lock.lock();
  10. try
  11. {
  12. if(number > 0)
  13. {
  14. System.out.println(Thread.currentThread().getName()+"卖出第:\t"+(number--)+"\t 还剩下:"+number);
  15. }
  16. }catch (Exception e){
  17. e.printStackTrace();
  18. }finally {
  19. lock.unlock();
  20. }
  21. }
  22. }
  23. /**
  24. * @auther zzyy
  25. * @create 2020-05-14 17:26
  26. */
  27. public class SaleTicketDemo
  28. {
  29. public static void main(String[] args)
  30. {
  31. Ticket ticket = new Ticket();
  32. new Thread(() -> { for (int i = 0; i <35; i++) ticket.sale(); },"a").start();
  33. new Thread(() -> { for (int i = 0; i <35; i++) ticket.sale(); },"b").start();
  34. new Thread(() -> { for (int i = 0; i <35; i++) ticket.sale(); },"c").start();
  35. }
  36. }
何为公平锁/非公平锁?

按序排队公平锁,就是判断同步队列是否还有先驱节点的存在(我前面还有人吗?),如果没有先驱节点才能获取锁;

先占先得非公平锁,是不管这个事的,只要能抢获到同步状态就可以

为什么会有公平锁/非公平锁的设计为什么默认非公平?
  1. 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
  2. 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
使⽤公平锁会有什么问题

公平锁保证了排队的公平性,非公平锁霸气的忽视这个规则,所以就有可能导致排队的长时间在排队,也没有机会获取到锁,

这就是传说中的 “锁饥饿”

什么时候用公平?什么时候用非公平?

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;

否则那就用公平锁,大家公平使用。

可重入锁(又名递归锁)

是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。

所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

“可重入锁”这四个字分开来解释:

可:可以。

重:再次。

入:进入。

锁:同步锁。

进入同步域(即同步代码块/方法或显式锁锁定的代码)

一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁

可重入锁种类
隐式锁(即synchronized关键字使用的锁)默认是可重入锁
  1. package com.atguigu.juc.senior.prepare;
  2. /**
  3. * @auther zzyy
  4. * @create 2020-05-14 11:59
  5. */
  6. public class ReEntryLockDemo
  7. {
  8. public static void main(String[] args)
  9. {
  10. final Object objectLockA = new Object();
  11. new Thread(() -> {
  12. synchronized (objectLockA)
  13. {
  14. System.out.println("-----外层调用");
  15. synchronized (objectLockA)
  16. {
  17. System.out.println("-----中层调用");
  18. synchronized (objectLockA)
  19. {
  20. System.out.println("-----内层调用");
  21. }
  22. }
  23. }
  24. },"a").start();
  25. }
  26. }
  1. package com.atguigu.juc.senior.prepare;
  2. /**
  3. * @auther zzyy
  4. * @create 2020-05-14 11:59
  5. * 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
  6. */
  7. public class ReEntryLockDemo
  8. {
  9. public synchronized void m1()
  10. {
  11. System.out.println("-----m1");
  12. m2();
  13. }
  14. public synchronized void m2()
  15. {
  16. System.out.println("-----m2");
  17. m3();
  18. }
  19. public synchronized void m3()
  20. {
  21. System.out.println("-----m3");
  22. }
  23. public static void main(String[] args)
  24. {
  25. ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
  26. reEntryLockDemo.m1();
  27. }
  28. }

Synchronized的重入的实现机理

  1. 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
  2. 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
  3. 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
  4. 当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

显式锁(即Lock)也有ReentrantLock这样的可重入锁。
  1. package com.atguigu.juc.senior.prepare;
  2. import java.util.concurrent.locks.Lock;
  3. import java.util.concurrent.locks.ReentrantLock;
  4. /**
  5. * @auther zzyy
  6. * @create 2020-05-14 11:59
  7. * 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
  8. */
  9. public class ReEntryLockDemo
  10. {
  11. static Lock lock = new ReentrantLock();
  12. public static void main(String[] args)
  13. {
  14. new Thread(() -> {
  15. lock.lock();
  16. try
  17. {
  18. System.out.println("----外层调用lock");
  19. lock.lock();
  20. try
  21. {
  22. System.out.println("----内层调用lock");
  23. }finally {
  24. // 这里故意注释,实现加锁次数和释放次数不一样
  25. // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
  26. lock.unlock(); // 正常情况,加锁几次就要解锁几次
  27. }
  28. }finally {
  29. lock.unlock();
  30. }
  31. },"a").start();
  32. new Thread(() -> {
  33. lock.lock();
  34. try
  35. {
  36. System.out.println("b thread----外层调用lock");
  37. }finally {
  38. lock.unlock();
  39. }
  40. },"b").start();
  41. }
  42. }

死锁及排查

产生死锁主要原因
  1. 系统资源不足
  2. 进程运行推进的顺序不合适
  3. 资源分配不当
  1. package com.atguigu.juc.senior.prepare;
  2. import java.util.concurrent.TimeUnit;
  3. /**
  4. * @auther zzyy
  5. * @create 2020-05-14 10:56
  6. */
  7. public class DeadLockDemo
  8. {
  9. public static void main(String[] args)
  10. {
  11. final Object objectLockA = new Object();
  12. final Object objectLockB = new Object();
  13. new Thread(() -> {
  14. synchronized (objectLockA)
  15. {
  16. System.out.println(Thread.currentThread().getName()+"\t"+"自己持有A,希望获得B");
  17. //暂停几秒钟线程
  18. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  19. synchronized (objectLockB)
  20. {
  21. System.out.println(Thread.currentThread().getName()+"\t"+"A-------已经获得B");
  22. }
  23. }
  24. },"A").start();
  25. new Thread(() -> {
  26. synchronized (objectLockB)
  27. {
  28. System.out.println(Thread.currentThread().getName()+"\t"+"自己持有B,希望获得A");
  29. //暂停几秒钟线程
  30. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  31. synchronized (objectLockA)
  32. {
  33. System.out.println(Thread.currentThread().getName()+"\t"+"B-------已经获得A");
  34. }
  35. }
  36. },"B").start();
  37. }
  38. }
如何排查死锁

纯命令

  1. jps -l
  2. jstack 进程编号

图形化

jconsole

4. LockSupport与线程中断

线程中断机制

什么是中断?

首先

一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。

所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。

其次

在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。

因此,Java提供了一种用于停止线程的机制——中断。

中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。

若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;

接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。

每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;

通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。

中断的相关API方法

面试题:如何使用中断标识停止线程?

在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑。

  1. 修改状态
  2. 停止程序的运行

方法

通过一个volatile变量实现

  1. package com.atguigu.juc.senior.test;
  2. import java.util.concurrent.TimeUnit;
  3. /**
  4. * @auther zzyy
  5. * @create 2020-05-12 14:19
  6. */
  7. public class InterruptDemo
  8. {
  9. private static volatile boolean isStop = false;
  10. public static void main(String[] args)
  11. {
  12. new Thread(() -> {
  13. while(true)
  14. {
  15. if(isStop)
  16. {
  17. System.out.println(Thread.currentThread().getName()+"线程------isStop = true,自己退出了");
  18. break;
  19. }
  20. System.out.println("-------hello interrupt");
  21. }
  22. },"t1").start();
  23. //暂停几秒钟线程
  24. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  25. isStop = true;
  26. }
  27. }

通过AtomicBoolean

  1. package com.zzyy.study.test;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.atomic.AtomicBoolean;
  4. /**
  5. * @auther zzyy
  6. * @create 2020-05-26 23:24
  7. */
  8. public class StopThreadDemo
  9. {
  10. private final static AtomicBoolean atomicBoolean = new AtomicBoolean(true);
  11. public static void main(String[] args)
  12. {
  13. Thread t1 = new Thread(() -> {
  14. while(atomicBoolean.get())
  15. {
  16. try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
  17. System.out.println("-----hello");
  18. }
  19. }, "t1");
  20. t1.start();
  21. try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
  22. atomicBoolean.set(false);
  23. }
  24. }

通过Thread类自带的中断api方法实现

  1. 实例方法interrupt(),没有返回值

  1. 实例方法isInterrupted,返回布尔值

  1. package com.atguigu.itdachang;
  2. import java.util.concurrent.TimeUnit;
  3. /**
  4. * @auther zzyy
  5. * @create 2020-07-10 17:33
  6. */
  7. public class InterruptDemo
  8. {
  9. public static void main(String[] args)
  10. {
  11. Thread t1 = new Thread(() -> {
  12. while(true)
  13. {
  14. if(Thread.currentThread().isInterrupted())
  15. {
  16. System.out.println("-----t1 线程被中断了,break,程序结束");
  17. break;
  18. }
  19. System.out.println("-----hello");
  20. }
  21. }, "t1");
  22. t1.start();
  23. System.out.println("**************"+t1.isInterrupted());
  24. //暂停5毫秒
  25. try { TimeUnit.MILLISECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
  26. t1.interrupt();
  27. System.out.println("**************"+t1.isInterrupted());
  28. }
  29. }
当前线程的中断标识为true,是不是就立刻停止?

具体来说,当对一个线程,调用 interrupt() 时:

① 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。

被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。

② 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法,

那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。

  1. package com.atguigu.juc.senior.test;
  2. import java.util.concurrent.TimeUnit;
  3. /**
  4. * @auther zzyy
  5. * @create 2020-05-13 10:25
  6. */
  7. public class InterruptDemo2
  8. {
  9. public static void main(String[] args) throws InterruptedException
  10. {
  11. Thread t1 = new Thread(() -> {
  12. for (int i=0;i<300;i++) {
  13. System.out.println("-------"+i);
  14. }
  15. System.out.println("after t1.interrupt()--第2次---: "+Thread.currentThread().isInterrupted());
  16. },"t1");
  17. t1.start();
  18. System.out.println("before t1.interrupt()----: "+t1.isInterrupted());
  19. //实例方法interrupt()仅仅是设置线程的中断状态位设置为true,不会停止线程
  20. t1.interrupt();
  21. //活动状态,t1线程还在执行中
  22. try { TimeUnit.MILLISECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
  23. System.out.println("after t1.interrupt()--第1次---: "+t1.isInterrupted());
  24. //非活动状态,t1线程不在执行中,已经结束执行了。
  25. try { TimeUnit.MILLISECONDS.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
  26. System.out.println("after t1.interrupt()--第3次---: "+t1.isInterrupted());
  27. }
  28. }

中断只是一种协同机制,修改中断标识位仅此而已,不是立刻stop打断

静态方法Thread.interrupted()
静态方法Thread.interrupted()
  1. package com.atguigu.juc.senior.test;
  2. /**
  3. * @auther zzyy
  4. * @create 2020-05-12 14:19
  5. * 作用是测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,
  6. * 第二次再调用时中断状态已经被清除,将返回一个false。
  7. */
  8. public class InterruptDemo
  9. {
  10. public static void main(String[] args) throws InterruptedException
  11. {
  12. System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
  13. System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
  14. System.out.println("111111");
  15. Thread.currentThread().interrupt();
  16. System.out.println("222222");
  17. System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
  18. System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
  19. }
  20. }

都会返回中断状态,两者对比

LockSupport是什么

线程等待唤醒机制

3种让线程等待和唤醒的方法

方式1:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程

方式2:使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程

方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

Object类中的wait和notify方法实现线程等待和唤醒
  1. package com.atguigu.juc.prepare;
  2. import java.util.concurrent.TimeUnit;
  3. /**
  4. * @auther zzyy
  5. * @create 2020-04-13 17:12
  6. *
  7. * 要求:t1线程等待3秒钟,3秒钟后t2线程唤醒t1线程继续工作
  8. *
  9. * 1 正常程序演示
  10. *
  11. * 以下异常情况:
  12. * 2 wait方法和notify方法,两个都去掉同步代码块后看运行效果
  13. * 2.1 异常情况
  14. * Exception in thread "t1" java.lang.IllegalMonitorStateException at java.lang.Object.wait(Native Method)
  15. * Exception in thread "t2" java.lang.IllegalMonitorStateException at java.lang.Object.notify(Native Method)
  16. * 2.2 结论
  17. * Object类中的wait、notify、notifyAll用于线程等待和唤醒的方法,都必须在synchronized内部执行(必须用到关键字synchronized)。
  18. *
  19. * 3 将notify放在wait方法前面
  20. * 3.1 程序一直无法结束
  21. * 3.2 结论
  22. * 先wait后notify、notifyall方法,等待中的线程才会被唤醒,否则无法唤醒
  23. */
  24. public class LockSupportDemo
  25. {
  26. public static void main(String[] args)//main方法,主线程一切程序入口
  27. {
  28. Object objectLock = new Object(); //同一把锁,类似资源类
  29. new Thread(() -> {
  30. synchronized (objectLock) {
  31. try {
  32. objectLock.wait();
  33. } catch (InterruptedException e) {
  34. e.printStackTrace();
  35. }
  36. }
  37. System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒了");
  38. },"t1").start();
  39. //暂停几秒钟线程
  40. try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
  41. new Thread(() -> {
  42. synchronized (objectLock) {
  43. objectLock.notify();
  44. }
  45. //objectLock.notify();
  46. /*synchronized (objectLock) {
  47. try {
  48. objectLock.wait();
  49. } catch (InterruptedException e) {
  50. e.printStackTrace();
  51. }
  52. }*/
  53. },"t2").start();
  54. }
  55. }

wait和notify方法必须要在同步块或者方法里面,且成对出现使用

先wait后notify才OK

Condition接口中的await后signal方法实现线程的等待和唤醒
  1. package com.atguigu.juc.prepare;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.locks.Condition;
  4. import java.util.concurrent.locks.Lock;
  5. import java.util.concurrent.locks.ReentrantLock;
  6. /**
  7. * @auther zzyy
  8. * @create 2020-04-13 17:55
  9. */
  10. public class LockSupportDemo2
  11. {
  12. public static void main(String[] args)
  13. {
  14. Lock lock = new ReentrantLock();
  15. Condition condition = lock.newCondition();
  16. new Thread(() -> {
  17. lock.lock();
  18. try
  19. {
  20. System.out.println(Thread.currentThread().getName()+"\t"+"start");
  21. condition.await();
  22. System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒");
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. } finally {
  26. lock.unlock();
  27. }
  28. },"t1").start();
  29. //暂停几秒钟线程
  30. try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
  31. new Thread(() -> {
  32. lock.lock();
  33. try
  34. {
  35. condition.signal();
  36. } catch (Exception e) {
  37. e.printStackTrace();
  38. } finally {
  39. lock.unlock();
  40. }
  41. System.out.println(Thread.currentThread().getName()+"\t"+"通知了");
  42. },"t2").start();
  43. }
  44. }
异常1
  1. package com.atguigu.juc.prepare;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.locks.Condition;
  4. import java.util.concurrent.locks.Lock;
  5. import java.util.concurrent.locks.ReentrantLock;
  6. /**
  7. * @auther zzyy
  8. * @create 2020-04-13 17:55
  9. * 异常:
  10. * condition.await();和condition.signal();都触发了IllegalMonitorStateException异常
  11. *
  12. * 原因:调用condition中线程等待和唤醒的方法的前提是,要在lock和unlock方法中,要有锁才能调用
  13. */
  14. public class LockSupportDemo2
  15. {
  16. public static void main(String[] args)
  17. {
  18. Lock lock = new ReentrantLock();
  19. Condition condition = lock.newCondition();
  20. new Thread(() -> {
  21. try
  22. {
  23. System.out.println(Thread.currentThread().getName()+"\t"+"start");
  24. condition.await();
  25. System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒");
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. },"t1").start();
  30. //暂停几秒钟线程
  31. try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
  32. new Thread(() -> {
  33. try
  34. {
  35. condition.signal();
  36. } catch (Exception e) {
  37. e.printStackTrace();
  38. }
  39. System.out.println(Thread.currentThread().getName()+"\t"+"通知了");
  40. },"t2").start();
  41. }
  42. }

去掉lock/unlock

异常2
  1. package com.atguigu.juc.prepare;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.locks.Condition;
  4. import java.util.concurrent.locks.Lock;
  5. import java.util.concurrent.locks.ReentrantLock;
  6. /**
  7. * @auther zzyy
  8. * @create 2020-04-13 17:55
  9. * 异常:
  10. * 程序无法运行
  11. *
  12. * 原因:先await()后signal才OK,否则线程无法被唤醒
  13. */
  14. public class LockSupportDemo2
  15. {
  16. public static void main(String[] args)
  17. {
  18. Lock lock = new ReentrantLock();
  19. Condition condition = lock.newCondition();
  20. new Thread(() -> {
  21. lock.lock();
  22. try
  23. {
  24. condition.signal();
  25. System.out.println(Thread.currentThread().getName()+"\t"+"signal");
  26. } catch (Exception e) {
  27. e.printStackTrace();
  28. }finally {
  29. lock.unlock();
  30. }
  31. },"t1").start();
  32. //暂停几秒钟线程
  33. try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
  34. new Thread(() -> {
  35. lock.lock();
  36. try
  37. {
  38. System.out.println(Thread.currentThread().getName()+"\t"+"等待被唤醒");
  39. condition.await();
  40. System.out.println(Thread.currentThread().getName()+"\t"+"被唤醒");
  41. } catch (Exception e) {
  42. e.printStackTrace();
  43. }finally {
  44. lock.unlock();
  45. }
  46. },"t2").start();
  47. }
  48. }

先signal后await

Condtion中的线程等待和唤醒方法之前,需要先获取锁

一定要先await后signal,不要反了

Object和Condition使用的限制条件

线程先要获得并持有锁,必须在锁块(synchronized或lock)中

必须要先等待后唤醒,线程才能够被唤醒

LockSupport类中的park等待和unpark唤醒

通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作

主要方法

阻塞 park() /park(Object blocker)

阻塞当前线程/阻塞传入的具体线程

唤醒 unpark(Thread thread)

正常+无锁块要求

  1. package com.atguigu.juc.prepare;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.locks.LockSupport;
  4. /**
  5. * @auther zzyy
  6. * @create 2020-04-13 20:30
  7. */
  8. public class LockSupportDemo3
  9. {
  10. public static void main(String[] args)
  11. {
  12. //正常使用+不需要锁块
  13. Thread t1 = new Thread(() -> {
  14. System.out.println(Thread.currentThread().getName()+" "+"1111111111111");
  15. LockSupport.park();
  16. System.out.println(Thread.currentThread().getName()+" "+"2222222222222------end被唤醒");
  17. },"t1");
  18. t1.start();
  19. //暂停几秒钟线程
  20. try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
  21. LockSupport.unpark(t1);
  22. System.out.println(Thread.currentThread().getName()+" -----LockSupport.unparrk() invoked over");
  23. }
  24. }

之前错误的先唤醒后等待,LockSupport照样支持

  1. package com.zzyy.study.test;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.locks.LockSupport;
  4. /**
  5. * @auther zzyy
  6. * @create 2020-05-06 17:07
  7. */
  8. public class T1
  9. {
  10. public static void main(String[] args)
  11. {
  12. Thread t1 = new Thread(() -> {
  13. try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
  14. System.out.println(Thread.currentThread().getName()+"\t"+System.currentTimeMillis());
  15. LockSupport.park();
  16. System.out.println(Thread.currentThread().getName()+"\t"+System.currentTimeMillis()+"---被叫醒");
  17. },"t1");
  18. t1.start();
  19. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  20. LockSupport.unpark(t1);
  21. System.out.println(Thread.currentThread().getName()+"\t"+System.currentTimeMillis()+"---unpark over");
  22. }
  23. }

成双成对要牢记

5. Java内存模型之JMM

面试题

  1. 你知道什么是Java内存模型JMM吗?
  2. JMM与volatile它们两个之间的关系?(下一章详细讲解)
  3. JMM有哪些特性or它的三大特性是什么?
  4. 为什么要有JMM,它为什么出现?作用和功能是什么?
  5. happens-before先行发生原则你有了解过吗?

计算机硬件存储体系

问题?和推导出我们需要知道JMM

Java内存模型Java Memory Model

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。

原则:

JMM的关键技术点都是围绕多线程的原子性、可见性和有序性展开的

能干嘛?

1 通过JMM来实现线程和主内存之间的抽象关系。

2 屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果。

JMM规范下,三大特性

可见性

是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更 ,JMM规定了所有的变量都存储在主内存中。

Java中普通的共享变量不保证可见性,因为数据修改被写入内存的时机是不确定的,多线程并发下很可能出现"脏读",所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等 )都必需在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成

线程脏读:如果没有可见性保证

主内存中有变量 x,初始值为 0

线程 A 要将 x 加 1,先将 x=0 拷贝到自己的私有内存中,然后更新 x 的值

线程 A 将更新后的 x 值回刷到主内存的时间是不固定的

刚好在线程 A 没有回刷 x 到主内存时,线程 B 同样从主内存中读取 x,此时为 0,和线程 A 一样的操作,最后期盼的 x=2 就会变成 x=1

原子性

指一个操作是不可中断的,即多线程环境下,操作不能被其他线程干扰

有序性

JMM规范下,多线程对变量的读写过程

读取过程:

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

JMM定义了线程和主内存之间的抽象关系

1 线程之间的共享变量存储在主内存中(从硬件角度来说就是内存条)

2 每个线程都有一个私有的本地工作内存,本地工作内存中存储了该线程用来读/写共享变量的副本(从硬件角度来说就是CPU的缓存,比如寄存器、L1、L2、L3缓存等)

小总结
  1. 我们定义的所有共享变量都储存在物理主内存中
  2. 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
  3. 线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
  4. 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)

JMM规范下,多线程先行发生原则之happens-before

在JMM中,如果一个操作执行的结果需要对另一个操作可见性或者 代码重排序,那么这两个操作之间必须存在happens-before关系。

先行发生原则说明

如果Java内存模型中所有的有序性都仅靠volatile和synchronized来完成,那么有很多操作都将会变得非常啰嗦,但是我们在编写Java并发代码的时候并没有察觉到这一点。

我们没有时时、处处、次次,添加volatile和synchronized来完成程序,这是因为Java语言中JMM原则下

有一个“先行发生”(Happens-Before)的原则限制和规矩

这个原则非常重要:

它是判断数据是否存在竞争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题,而不需要陷入Java内存模型苦涩难懂的底层编译原理之中。

happens-before总原则
  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
happens-before之8条
1. 次序规则

一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作;

前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量X赋值为1,那后面一个操作肯定能知道X已经变成了1。

2. 锁定规则

一个unLock操作先行发生于后面((这里的“后面”是指时间上的先后))对同一个锁的lock操作;

  1. package com.zzyy.study.test;
  2. /**
  3. * @auther zzyy
  4. * @create 2020-06-11 16:01
  5. */
  6. public class HappenBeforeDemo
  7. {
  8. static Object objectLock = new Object();
  9. public static void main(String[] args) throws InterruptedException
  10. {
  11. //对于同一把锁objectLock,threadA一定先unlock同一把锁后B才能获得该锁,
  12. // A 先行发生于B
  13. synchronized (objectLock)
  14. {
  15. }
  16. }
  17. }
3. volatile变量规则

对一个volatile变量的写操作先行发生于后面对这个变量的读操作,

前面的写对后面的读是可见的,这里的“后面”同样是指时间上的先后。

4. 传递规则

如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

5. 线程启动规则(Thread Start Rule)

Thread对象的start()方法先行发生于此线程的每一个动作

6. 线程中断规则(Thread Interruption Rule)
  1. 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  2. 可以通过Thread.interrupted()检测到是否发生中断

7. 线程终止规则(Thread Termination Rule)

线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行。

8. 对象终结规则(Finalizer Rule)

一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始

说人话。对象没有完成初始化之前,是不能调用finalized()方法的

什么是finalized()方法

finalize()方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法

protected void finalize() throws Throwable { }

1、 finalize()调用的时机

与C++的析构函数(对象在清除之前析构函数会被调用)不同,在Java中,由于GC的自动回收机制,因而并不能保证finalize方法会被及时地执行(垃圾对象的回收时机具有不确定性),也不能保证它们会被执行(程序由始至终都未触发垃圾回收)。

2、 什么时候应该使用它

finalize()方法中一般用于释放非Java 资源(如打开的文件资源、数据库连接等),或是调用非Java方法(native方法)时分配的内存(比如C语言的malloc()系列函数)。

3、 为什么应该避免使用它

首先,由于finalize()方法的调用时机具有不确定性,从一个对象变得不可到达开始,到finalize()方法被执行,所花费的时间这段时间是任意长的。我们并不能依赖finalize()方法能及时的回收占用的资源,可能出现的情况是在我们耗尽资源之前,gc却仍未触发,因而通常的做法是提供显示的close()方法供客户端手动调用。
另外,重写finalize()方法意味着延长了回收对象时需要进行更多的操作,从而延长了对象回收的时间。

4、让对象再活一次

利用finalize()方法最多只会被调用一次的特性,我们可以实现延长对象的生命周期。

案例说明

如何修复?
  1. 把getter/setter方法都定义为synchronized方法
  2. 把value定义为volatile变量,由于setter方法对value的修改不依赖value的原值,满足volatile关键字使用场景

6. volatile与Java内存模型

被volatile修饰的变量有2大特点

特点:可见性和有序性

volatile的内存语义
  1. 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
  2. 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
  3. 所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。

内存屏障(面试重点必须拿下)

先说生活case

没有管控,顺序难保

设定规则,禁止乱序 上海南京路步行街武警“人墙”当红灯

是什么?

内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性。

内存屏障之前的所有写操作都要回写到主内存,

内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。

因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。

一句话:对一个 volatile 域的写, happens-before 于任意后续对这个 volatile 域的读,也叫写后读。

volatile凭什么可以保证可见性和有序性???

内存屏障 (Memory Barriers / Fences)

JVM中提供了四类内存屏障指令

上一章讲解过happens-before先行发生原则,类似接口规范,落地?

C++源码分析

IDEA工具里面找Unsafe.class

Unsafe.java

Unsafe.cpp

OrderAccess.hpp

orderAccess_linux_x86.inline.hpp

四大屏障分别是什么意思

巩固
happens-before 之 volatile 变量规则

JMM 就将内存屏障插⼊策略分为 4 种

写!!!

1. 在每个 volatile 写操作的前⾯插⼊⼀个 StoreStore 屏障

2. 在每个 volatile 写操作的后⾯插⼊⼀个 StoreLoad 屏障

读!!!

3. 在每个 volatile 读操作的后⾯插⼊⼀个 LoadLoad 屏障

4. 在每个 volatile 读操作的后⾯插⼊⼀个 LoadStore 屏障

volatile特性

保证可见性

保证不同线程对这个变量进行操作时的可见性,即变量一旦改变所有线程立即可见

  1. package com.zzyy.study.juc;
  2. import java.util.concurrent.TimeUnit;
  3. /**
  4. * @auther zzyy
  5. * @create 2020-06-30 11:29
  6. */
  7. public class VolatileSeeDemo
  8. {
  9. static boolean flag = true; //不加volatile,没有可见性
  10. //static volatile boolean flag = true; //加了volatile,保证可见性
  11. public static void main(String[] args)
  12. {
  13. new Thread(() -> {
  14. System.out.println(Thread.currentThread().getName()+"\t come in");
  15. while (flag)
  16. {
  17. }
  18. System.out.println(Thread.currentThread().getName()+"\t flag被修改为false,退出.....");
  19. },"t1").start();
  20. //暂停2秒钟后让main线程修改flag值
  21. try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
  22. flag = false;
  23. System.out.println("main线程修改完成");
  24. }
  25. }

不加volatile,没有可见性,程序无法停止

加了volatile,保证可见性,程序可以停止

volatile变量的读写过程

Java内存模型中定义的8种工作内存与主内存之间的原子操作

read(读取)→load(加载)→use(使用)→assign(赋值)→store(存储)→write(写入)→lock(锁定)→unlock(解锁)

read: 作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存

load: 作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载

use: 作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作

assign: 作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作

store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存

write: 作用于主内存,将store传输过来的变量值赋值给主内存中的变量

由于上述只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令:

lock: 作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。

unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用

没有原子性

volatile变量的复合操作(如i++)不具有原子性

  1. package com.zzyy.study.juc;
  2. import java.util.concurrent.TimeUnit;
  3. class MyNumber
  4. {
  5. volatile int number = 0;
  6. public void addPlusPlus()
  7. {
  8. number++;
  9. }
  10. }
  11. public class VolatileNoAtomicDemo
  12. {
  13. public static void main(String[] args) throws InterruptedException
  14. {
  15. MyNumber myNumber = new MyNumber();
  16. for (int i = 1; i <=10; i++) {
  17. new Thread(() -> {
  18. for (int j = 1; j <= 1000; j++) {
  19. myNumber.addPlusPlus();
  20. }
  21. },String.valueOf(i)).start();
  22. }
  23. //暂停几秒钟线程
  24. try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
  25. System.out.println(Thread.currentThread().getName() + "\t" + myNumber.number);
  26. }
  27. }
从i++的字节码角度说明

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。

  1. public void add()
  2. {
  3. i++; //不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分3步完成
  4. }

如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,

并执行相同值的加1操作,这也就造成了线程安全失败,因此对于add方法必须使用synchronized修饰,以便保证线程安全.

不保证原子性

读取赋值一个普通变量的情况

既然一修改就是可见,为什么还不能保证原子性?

volatile主要是对其中部分指令做了处理

要use(使用)一个变量的时候必需load(载入),要载入的时候必需从主内存read(读取)这样就解决了读的可见性。

写操作是把assign和store做了关联(在assign(赋值)后必需store(存储))。store(存储)后write(写入)。

也就是做到了给一个变量赋值的时候一串关联指令直接把变量值写到主内存。

就这样通过用的时候直接从主内存取,在赋值到直接写回主内存做到了内存可见性。注意蓝色框框的间隙。。。。。。o(╥﹏╥)o

结论

读取赋值一个volatile变量的情况

read-load-use 和 assign-store-write 成为了两个不可分割的原子操作,但是在use和assign之间依然有极小的一段真空期,有可能变量会被其他线程读取,导致写丢失一次...o(╥﹏╥)o

但是无论在哪一个时间点主内存的变量和任一工作内存的变量的值都是相等的。这个特性就导致了volatile变量不适合参与到依赖当前值的运算,如i = i + 1; i++;之类的那么依靠可见性的特点volatile可以用在哪些地方呢? 通常volatile用做保存某个状态的boolean值or int值。

《深入理解Java虚拟机》提到:

JVM的字节码,i++分成三步,间隙期不同步非原子操作(i++)

指令禁重排

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序

不存在数据依赖关系,可以重排序;

存在数据依赖关系,禁止重排序

但重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!

重排序的分类和执行流程

编译器优化的重排序: 编译器在不改变单线程串行语义的前提下,可以重新调整指令的执行顺序

指令级并行的重排序: 处理器使用指令级并行技术来讲多条指令重叠执行,若不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序

内存系统的重排序: 由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行

数据依赖性:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性。

volatile的底层实现是通过内存屏障,2次复习
volatile有关的禁止指令重排的行为

四大屏障的插入情况
  1. 1. 在每一个volatile写操作前面插入一个StoreStore屏障
  2. StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
  3. 2. 在每一个volatile写操作后面插入一个StoreLoad屏障
  4. StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序
  5. 3. 在每一个volatile读操作后面插入一个LoadLoad屏障
  6. LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
  7. 4. 在每一个volatile读操作后面插入一个LoadStore屏障
  8. LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

如何正确使用volatile

单一赋值可以,but含复合运算赋值不可以(i++之类)
  1. volatile int a = 10
  2. volatile boolean flag = false
状态标志,判断业务是否结束
  1. package com.atguigu.juc.prepare;
  2. import java.util.concurrent.TimeUnit;
  3. /**
  4. * @auther zzyy
  5. * @create 2020-04-14 18:11
  6. *
  7. * 使用:作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或任务结束
  8. * 理由:状态标志并不依赖于程序内任何其他状态,且通常只有一种状态转换
  9. * 例子:判断业务是否结束
  10. */
  11. public class UseVolatileDemo
  12. {
  13. private volatile static boolean flag = true;
  14. public static void main(String[] args)
  15. {
  16. new Thread(() -> {
  17. while(flag) {
  18. //do something......
  19. }
  20. },"t1").start();
  21. //暂停几秒钟线程
  22. try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }
  23. new Thread(() -> {
  24. flag = false;
  25. },"t2").start();
  26. }
  27. }
开销较低的读,写锁策略!!!
  1. public class UseVolatileDemo
  2. {
  3. /**
  4. * 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
  5. * 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
  6. */
  7. public class Counter
  8. {
  9. private volatile int value;
  10. public int getValue()
  11. {
  12. return value; //利用volatile保证读取操作的可见性
  13. }
  14. public synchronized int increment()
  15. {
  16. return value++; //利用synchronized保证复合操作的原子性
  17. }
  18. }
  19. }
DCL双端锁的发布
  1. package com.atguigu.itdachang;
  2. /**
  3. * @auther zzyy
  4. * @create 2020-07-13 15:14
  5. */
  6. public class SafeDoubleCheckSingleton
  7. {
  8. private static SafeDoubleCheckSingleton singleton;
  9. //私有化构造方法
  10. private SafeDoubleCheckSingleton(){
  11. }
  12. //双重锁设计
  13. public static SafeDoubleCheckSingleton getInstance(){
  14. if (singleton == null){
  15. //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
  16. synchronized (SafeDoubleCheckSingleton.class){
  17. if (singleton == null){
  18. //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
  19. singleton = new SafeDoubleCheckSingleton();
  20. }
  21. }
  22. }
  23. //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
  24. return singleton;
  25. }
  26. }

单线程看问题代码

由于存在指令重排序......

多线程看问题代码

解决01

加volatile修饰

解决02
  1. //现在比较好的做法就是采用静态内部内的方式实现
  2. public class SingletonDemo
  3. {
  4. private SingletonDemo() { }
  5. private static class SingletonDemoHandler
  6. {
  7. private static SingletonDemo instance = new SingletonDemo();
  8. }
  9. public static SingletonDemo getInstance()
  10. {
  11. return SingletonDemoHandler.instance;
  12. }
  13. }

总结

内存屏障是什么

内存屏障能干嘛
  1. 阻止屏障两边的指令重排序
  2. 写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
  3. 读数据时加入屏障,线程私有工作内存的数据失效,重新到主物理内存中获取最新数据
内存屏障四大指令
  1. 在每一个volatile写操作前面插入一个StoreStore屏障

  1. 在每一个volatile写操作后面插入一个StoreLoad屏障

  1. 在每一个volatile读操作后面插入一个LoadLoad屏障

  1. 在每一个volatile读操作后面插入一个LoadStore屏障‘

凭什么我们java写了一个volatile关键字,系统底层加入内存屏障?两者关系怎么勾搭上的?
字节码层面

关键字

volatile可见性

volatile禁重排

对比java.util.concurrent.locks.Lock来理解

一句话总结

7.CAS

8. 原子操作类之18罗汉增强

是什么?

atomic

为什么有原子类?

因为volatile可以解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。

所以使用该类实现:

  1. AtomicInteger count = new AtomicInteger();
  2. count.addAndGet(1);
  3. 如果是jdk8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)

再分类

基本类型原子类

AtomicInteger

AtomicBoolean

AtomicLong

常用API简介

  1. package com.atguigu.juc.senior.test2;
  2. import lombok.Getter;
  3. import java.util.concurrent.CountDownLatch;
  4. import java.util.concurrent.atomic.AtomicInteger;
  5. class MyNumber
  6. {
  7. @Getter
  8. private AtomicInteger atomicInteger = new AtomicInteger();
  9. public void addPlusPlus()
  10. {
  11. atomicInteger.incrementAndGet();
  12. }
  13. }
  14. /**
  15. * @auther zzyy
  16. * @create 2020-07-03 17:16
  17. */
  18. public class AtomicIntegerDemo
  19. {
  20. public static void main(String[] args) throws InterruptedException
  21. {
  22. MyNumber myNumber = new MyNumber();
  23. CountDownLatch countDownLatch = new CountDownLatch(100);
  24. for (int i = 1; i <=100; i++) {
  25. new Thread(() -> {
  26. try
  27. {
  28. for (int j = 1; j <=5000; j++)
  29. {
  30. myNumber.addPlusPlus();
  31. }
  32. }finally {
  33. countDownLatch.countDown();
  34. }
  35. },String.valueOf(i)).start();
  36. }
  37. countDownLatch.await();
  38. System.out.println(myNumber.getAtomicInteger().get());
  39. }
  40. }
  41. tsleep→countDownLatch
  42. 不再使用睡眠堵塞, 不能保证睡眠时间!!!!
  43. 改为countDownLatch
  44. 100个线程跑完一个少一个,等到100个线程都跑完了之后再去获取结果
  45. 不会因为时间的空转导致吞吐量变小
数组类型原子类

AtomicIntegerArray

AtomicLongArray

AtomicReferenceArray

  1. package com.atguigu.juc.prepare;
  2. import java.util.concurrent.atomic.AtomicIntegerArray;
  3. /**
  4. * @auther zzyy
  5. * @create 2020-04-16 14:59
  6. */
  7. public class AtomicIntegerArrayDemo
  8. {
  9. public static void main(String[] args)
  10. {
  11. AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
  12. //AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(5);
  13. //AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[]{1,2,3,4,5});
  14. for (int i = 0; i <atomicIntegerArray.length(); i++) {
  15. System.out.println(atomicIntegerArray.get(i));
  16. }
  17. System.out.println();
  18. System.out.println();
  19. System.out.println();
  20. int tmpInt = 0;
  21. tmpInt = atomicIntegerArray.getAndSet(0,1122);
  22. System.out.println(tmpInt+"\t"+atomicIntegerArray.get(0));
  23. atomicIntegerArray.getAndIncrement(1);
  24. atomicIntegerArray.getAndIncrement(1);
  25. tmpInt = atomicIntegerArray.getAndIncrement(1);
  26. System.out.println(tmpInt+"\t"+atomicIntegerArray.get(1));
  27. }
  28. }

引用类型原子类
AtomicReference
  1. package com.atguigu.Interview.study.thread;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Getter;
  4. import lombok.ToString;
  5. import java.util.concurrent.atomic.AtomicReference;
  6. @Getter
  7. @ToString
  8. @AllArgsConstructor
  9. class User
  10. {
  11. String userName;
  12. int age;
  13. }
  14. /**
  15. * @auther zzyy
  16. * @create 2018-12-31 17:22
  17. */
  18. public class AtomicReferenceDemo
  19. {
  20. public static void main(String[] args)
  21. {
  22. User z3 = new User("z3",24);
  23. User li4 = new User("li4",26);
  24. AtomicReference<User> atomicReferenceUser = new AtomicReference<>();
  25. atomicReferenceUser.set(z3);
  26. System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t"+atomicReferenceUser.get().toString());
  27. System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t"+atomicReferenceUser.get().toString());
  28. }
  29. }
  30. true 设计模式.creatation.prototype.User@677327b6
  31. false 设计模式.creatation.prototype.User@677327b6

自旋锁SpinLockDemo

  1. package com.atguigu.Interview.study.thread;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.atomic.AtomicReference;
  4. /**
  5. * @auther zzyy
  6. * @create 2018-12-28 17:57
  7. * 题目:实现一个自旋锁
  8. * 自旋锁好处:循环比较获取没有类似wait的阻塞。
  9. *
  10. * 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现
  11. * 当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到。
  12. */
  13. public class SpinLockDemo
  14. {
  15. AtomicReference<Thread> atomicReference = new AtomicReference<>();
  16. public void myLock()
  17. {
  18. Thread thread = Thread.currentThread();
  19. System.out.println(Thread.currentThread().getName()+"\t come in");
  20. while(!atomicReference.compareAndSet(null,thread))
  21. {
  22. }
  23. }
  24. public void myUnLock()
  25. {
  26. Thread thread = Thread.currentThread();
  27. atomicReference.compareAndSet(thread,null);
  28. System.out.println(Thread.currentThread().getName()+"\t myUnLock over");
  29. }
  30. public static void main(String[] args)
  31. {
  32. SpinLockDemo spinLockDemo = new SpinLockDemo();
  33. new Thread(() -> {
  34. spinLockDemo.myLock();
  35. //暂停一会儿线程
  36. try { TimeUnit.SECONDS.sleep( 5 ); } catch (InterruptedException e) { e.printStackTrace(); }
  37. spinLockDemo.myUnLock();
  38. },"A").start();
  39. //暂停一会儿线程,保证A线程先于B线程启动并完成
  40. try { TimeUnit.SECONDS.sleep( 1 ); } catch (InterruptedException e) { e.printStackTrace(); }
  41. new Thread(() -> {
  42. spinLockDemo.myLock();
  43. spinLockDemo.myUnLock();
  44. },"B").start();
  45. }
  46. }

AtomicStampedReference

携带版本号的引用类型原子类,可以解决ABA问题

解决修改过几次!!!!

  1. package com.atguigu.juc.cas;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.atomic.AtomicInteger;
  4. import java.util.concurrent.atomic.AtomicStampedReference;
  5. /**
  6. * @auther zzyy
  7. * @create 2021-03-18 15:34
  8. */
  9. public class ABADemo
  10. {
  11. static AtomicInteger atomicInteger = new AtomicInteger(100);
  12. static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);
  13. public static void main(String[] args)
  14. {
  15. abaProblem();
  16. abaResolve();
  17. }
  18. public static void abaResolve()
  19. {
  20. new Thread(() -> {
  21. int stamp = atomicStampedReference.getStamp();
  22. System.out.println("t3 ----第1次stamp "+stamp);
  23. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  24. atomicStampedReference.compareAndSet(100,101,stamp,stamp+1);
  25. System.out.println("t3 ----第2次stamp "+atomicStampedReference.getStamp());
  26. atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
  27. System.out.println("t3 ----第3次stamp "+atomicStampedReference.getStamp());
  28. },"t3").start();
  29. new Thread(() -> {
  30. int stamp = atomicStampedReference.getStamp();
  31. System.out.println("t4 ----第1次stamp "+stamp);
  32. //暂停几秒钟线程
  33. try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
  34. boolean result = atomicStampedReference.compareAndSet(100, 20210308, stamp, stamp + 1);
  35. System.out.println(Thread.currentThread().getName()+"\t"+result+"\t"+atomicStampedReference.getReference());
  36. },"t4").start();
  37. }
  38. public static void abaProblem()
  39. {
  40. new Thread(() -> {
  41. atomicInteger.compareAndSet(100,101);
  42. atomicInteger.compareAndSet(101,100);
  43. },"t1").start();
  44. try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
  45. new Thread(() -> {
  46. atomicInteger.compareAndSet(100,20210308);
  47. System.out.println(atomicInteger.get());
  48. },"t2").start();
  49. }
  50. }
  51. t2 ------20210308
  52. t4 ----第1次stamp 1
  53. t3 ----第1次stamp 1
  54. t3 ----第2次stamp 2
  55. t3 ----第3次stamp 3
  56. t4 false 100
  57. CAS问题引出的ABA问题,第一次将1修改成了2,第二次又把2改成了3,第三次把3改成1
  58. 表面上没有错,但是其中出现改了好几次
  59. 所以每次修改都会有版本号。t3和t4第一次都拿到了版本号是1。但是t3提前争到了修改成101
  60. 版本号就改变了。所以当t4要修改时,虽然结果都是100.但是版本号已经是对不上了!!!
AtomicMarkableReference

原子更新带有标记位的引用类型对象

解决是否修改过!!!

它的定义就是将状态戳简化为true|false

  1. package com.atguigu.juc.senior.inner.atomic;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.atomic.AtomicInteger;
  4. import java.util.concurrent.atomic.AtomicMarkableReference;
  5. import java.util.concurrent.atomic.AtomicStampedReference;
  6. /**
  7. * @auther zzyy
  8. * @create 2020-05-23 10:56
  9. */
  10. public class ABADemo
  11. {
  12. static AtomicInteger atomicInteger = new AtomicInteger(100);
  13. static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
  14. static AtomicMarkableReference<Integer> markableReference = new AtomicMarkableReference<>(100,false);
  15. public static void main(String[] args)
  16. {
  17. new Thread(() -> {
  18. atomicInteger.compareAndSet(100,101);
  19. atomicInteger.compareAndSet(101,100);
  20. System.out.println(Thread.currentThread().getName()+"\t"+"update ok");
  21. },"t1").start();
  22. new Thread(() -> {
  23. //暂停几秒钟线程
  24. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  25. atomicInteger.compareAndSet(100,2020);
  26. },"t2").start();
  27. //暂停几秒钟线程
  28. try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
  29. System.out.println(atomicInteger.get());
  30. System.out.println();
  31. System.out.println();
  32. System.out.println();
  33. System.out.println("============以下是ABA问题的解决,让我们知道引用变量中途被更改了几次=========================");
  34. new Thread(() -> {
  35. System.out.println(Thread.currentThread().getName()+"\t 1次版本号"+stampedReference.getStamp());
  36. //故意暂停200毫秒,让后面的t4线程拿到和t3一样的版本号
  37. try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
  38. stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
  39. System.out.println(Thread.currentThread().getName()+"\t 2次版本号"+stampedReference.getStamp());
  40. stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
  41. System.out.println(Thread.currentThread().getName()+"\t 3次版本号"+stampedReference.getStamp());
  42. },"t3").start();
  43. new Thread(() -> {
  44. int stamp = stampedReference.getStamp();
  45. System.out.println(Thread.currentThread().getName()+"\t =======1次版本号"+stamp);
  46. //暂停2秒钟,让t3先完成ABA操作了,看看自己还能否修改
  47. try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
  48. boolean b = stampedReference.compareAndSet(100, 2020, stamp, stamp + 1);
  49. System.out.println(Thread.currentThread().getName()+"\t=======2次版本号"+stampedReference.getStamp()+"\t"+stampedReference.getReference());
  50. },"t4").start();
  51. System.out.println();
  52. System.out.println();
  53. System.out.println();
  54. System.out.println("============AtomicMarkableReference不关心引用变量更改过几次,只关心是否更改过======================");
  55. new Thread(() -> {
  56. boolean marked = markableReference.isMarked();
  57. System.out.println(Thread.currentThread().getName()+"\t 1次版本号"+marked);
  58. try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
  59. markableReference.compareAndSet(100,101,marked,!marked);
  60. System.out.println(Thread.currentThread().getName()+"\t 2次版本号"+markableReference.isMarked());
  61. markableReference.compareAndSet(101,100,markableReference.isMarked(),!markableReference.isMarked());
  62. System.out.println(Thread.currentThread().getName()+"\t 3次版本号"+markableReference.isMarked());
  63. },"t5").start();
  64. new Thread(() -> {
  65. boolean marked = markableReference.isMarked();
  66. System.out.println(Thread.currentThread().getName()+"\t 1次版本号"+marked);
  67. //暂停几秒钟线程
  68. try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
  69. markableReference.compareAndSet(100,2020,marked,!marked);
  70. System.out.println(Thread.currentThread().getName()+"\t"+markableReference.getReference()+"\t"+markableReference.isMarked());
  71. },"t6").start();
  72. }
  73. }
  74. t1 update ok
  75. 2020
  76. ============以下是ABA问题的解决,让我们知道引用变量中途被更改了几次=========================
  77. t3 1次版本号1
  78. t4 =======1次版本号1
  79. ============AtomicMarkableReference不关心引用变量更改过几次,只关心是否更改过======================
  80. t5 1次版本号false
  81. t6 1次版本号false
  82. t5 2次版本号true
  83. t6 101 true
  84. t5 3次版本号false
  85. t3 2次版本号2
  86. t3 3次版本号3
  87. t4 =======2次版本号3 100

对象的属性修改原子类
AtomicIntegerFieldUpdater

原子更新对象中int类型字段的值

AtomicLongFieldUpdater

原子更新对象中Long类型字段的值

AtomicReferenceFieldUpdater

原子更新引用类型字段的值

使用目的

以一种线程安全的方式操作非线程安全对象内的某些字段

使用要求
  1. 更新的对象属性必须使用 public volatile 修饰符。
  2. 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
AtomicIntegerFieldUpdaterDemo
  1. package com.atguigu.itdachang;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
  4. class BankAccount
  5. {
  6. private String bankName = "CCB";//银行
  7. public volatile int money = 0;//钱数
  8. AtomicIntegerFieldUpdater<BankAccount> accountAtomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(BankAccount.class,"money");
  9. //不加锁+性能高,局部微创
  10. public void transferMoney(BankAccount bankAccount)
  11. {
  12. accountAtomicIntegerFieldUpdater.incrementAndGet(bankAccount);
  13. }
  14. }
  15. /**
  16. * @auther zzyy
  17. * @create 2020-07-14 18:06
  18. * 以一种线程安全的方式操作非线程安全对象的某些字段。
  19. * 需求:
  20. * 1000个人同时向一个账号转账一元钱,那么累计应该增加1000元,
  21. * 除了synchronized和CAS,还可以使用AtomicIntegerFieldUpdater来实现。
  22. */
  23. public class AtomicIntegerFieldUpdaterDemo
  24. {
  25. public static void main(String[] args)
  26. {
  27. BankAccount bankAccount = new BankAccount();
  28. for (int i = 1; i <=1000; i++) {
  29. int finalI = i;
  30. new Thread(() -> {
  31. bankAccount.transferMoney(bankAccount);
  32. },String.valueOf(i)).start();
  33. }
  34. //暂停毫秒
  35. try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
  36. System.out.println(bankAccount.money);
  37. }
  38. }
AtomicReferenceFieldUpdater
  1. package com.atguigu.juc.atomics;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
  4. class MyVar
  5. {
  6. public volatile Boolean isInit = Boolean.FALSE;
  7. AtomicReferenceFieldUpdater<MyVar,Boolean> atomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit");
  8. public void init(MyVar myVar)
  9. {
  10. if(atomicReferenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE))
  11. {
  12. System.out.println(Thread.currentThread().getName()+"\t"+"---init.....");
  13. //暂停几秒钟线程
  14. try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
  15. System.out.println(Thread.currentThread().getName()+"\t"+"---init.....over");
  16. }else{
  17. System.out.println(Thread.currentThread().getName()+"\t"+"------其它线程正在初始化");
  18. }
  19. }
  20. }
  21. /**
  22. * @auther zzyy
  23. * @create 2021-03-18 17:20
  24. * 多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,要求只能初始化一次
  25. */
  26. public class AtomicIntegerFieldUpdaterDemo
  27. {
  28. public static void main(String[] args) throws InterruptedException
  29. {
  30. MyVar myVar = new MyVar();
  31. for (int i = 1; i <=5; i++) {
  32. new Thread(() -> {
  33. myVar.init(myVar);
  34. },String.valueOf(i)).start();
  35. }
  36. }
  37. }
面试官问你:你在哪里用了volatile

AtomicReferenceFieldUpdater

原子操作增强类原理深度解析
DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder
点赞计数器,看看性能

LongAdder只能用来计算加法,且从零开始计算

LongAccumulator提供了自定义的函数操作

  1. long类型的聚合器,需要传入一个long类型的二元操作,可以用来计算各种聚合操作,包括加乘等
  2. package com.atguigu.juc.senior.inner.atomic;
  3. import java.util.concurrent.atomic.LongAccumulator;
  4. import java.util.concurrent.atomic.LongAdder;
  5. import java.util.function.LongBinaryOperator;
  6. /**
  7. * @auther zzyy
  8. * @create 2020-05-30 13:51
  9. */
  10. public class LongAccumulatorDemo
  11. {
  12. LongAdder longAdder = new LongAdder();
  13. public void add_LongAdder()
  14. {
  15. longAdder.increment();
  16. }
  17. //LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y,0);
  18. LongAccumulator longAccumulator = new LongAccumulator(new LongBinaryOperator()
  19. {
  20. @Override
  21. public long applyAsLong(long left, long right)
  22. {
  23. return left - right;
  24. }
  25. },777);
  26. public void add_LongAccumulator()
  27. {
  28. longAccumulator.accumulate(1);
  29. }
  30. public static void main(String[] args)
  31. {
  32. LongAccumulatorDemo demo = new LongAccumulatorDemo();
  33. demo.add_LongAccumulator();
  34. demo.add_LongAccumulator();
  35. System.out.println(demo.longAccumulator.longValue());
  36. }
  37. }

LongAdderAPIDemo

  1. package com.atguigu.juc.atomics;
  2. import java.util.concurrent.atomic.LongAccumulator;
  3. import java.util.concurrent.atomic.LongAdder;
  4. /**
  5. * @auther zzyy
  6. * @create 2021-03-19 15:59
  7. */
  8. public class LongAdderAPIDemo
  9. {
  10. public static void main(String[] args)
  11. {
  12. LongAdder longAdder = new LongAdder();
  13. longAdder.increment();
  14. longAdder.increment();
  15. longAdder.increment();
  16. System.out.println(longAdder.longValue());
  17. LongAccumulator longAccumulator = new LongAccumulator((x,y) -> x * y,2);
  18. longAccumulator.accumulate(1);
  19. longAccumulator.accumulate(2);
  20. longAccumulator.accumulate(3);
  21. System.out.println(longAccumulator.longValue());
  22. }
  23. }

LongAdder高性能对比Code演示

  1. package com.zzyy.study.day524;
  2. import java.util.concurrent.CountDownLatch;
  3. import java.util.concurrent.atomic.AtomicLong;
  4. import java.util.concurrent.atomic.LongAccumulator;
  5. import java.util.concurrent.atomic.LongAdder;
  6. class ClickNumberNet
  7. {
  8. int number = 0;
  9. public synchronized void clickBySync()
  10. {
  11. number++;
  12. }
  13. AtomicLong atomicLong = new AtomicLong(0);
  14. public void clickByAtomicLong()
  15. {
  16. atomicLong.incrementAndGet();
  17. }
  18. LongAdder longAdder = new LongAdder();
  19. public void clickByLongAdder()
  20. {
  21. longAdder.increment();
  22. }
  23. LongAccumulator longAccumulator = new LongAccumulator((x,y) -> x + y,0);
  24. public void clickByLongAccumulator()
  25. {
  26. longAccumulator.accumulate(1);
  27. }
  28. }
  29. /**
  30. * @auther zzyy
  31. * @create 2020-05-21 22:23
  32. * 50个线程,每个线程100W次,总点赞数出来
  33. */
  34. public class LongAdderDemo2
  35. {
  36. public static void main(String[] args) throws InterruptedException
  37. {
  38. ClickNumberNet clickNumberNet = new ClickNumberNet();
  39. long startTime;
  40. long endTime;
  41. CountDownLatch countDownLatch = new CountDownLatch(50);
  42. CountDownLatch countDownLatch2 = new CountDownLatch(50);
  43. CountDownLatch countDownLatch3 = new CountDownLatch(50);
  44. CountDownLatch countDownLatch4 = new CountDownLatch(50);
  45. startTime = System.currentTimeMillis();
  46. for (int i = 1; i <=50; i++) {
  47. new Thread(() -> {
  48. try
  49. {
  50. for (int j = 1; j <=100 * 10000; j++) {
  51. clickNumberNet.clickBySync();
  52. }
  53. }finally {
  54. countDownLatch.countDown();
  55. }
  56. },String.valueOf(i)).start();
  57. }
  58. countDownLatch.await();
  59. endTime = System.currentTimeMillis();
  60. System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickBySync result: "+clickNumberNet.number);
  61. startTime = System.currentTimeMillis();
  62. for (int i = 1; i <=50; i++) {
  63. new Thread(() -> {
  64. try
  65. {
  66. for (int j = 1; j <=100 * 10000; j++) {
  67. clickNumberNet.clickByAtomicLong();
  68. }
  69. }finally {
  70. countDownLatch2.countDown();
  71. }
  72. },String.valueOf(i)).start();
  73. }
  74. countDownLatch2.await();
  75. endTime = System.currentTimeMillis();
  76. System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickByAtomicLong result: "+clickNumberNet.atomicLong);
  77. startTime = System.currentTimeMillis();
  78. for (int i = 1; i <=50; i++) {
  79. new Thread(() -> {
  80. try
  81. {
  82. for (int j = 1; j <=100 * 10000; j++) {
  83. clickNumberNet.clickByLongAdder();
  84. }
  85. }finally {
  86. countDownLatch3.countDown();
  87. }
  88. },String.valueOf(i)).start();
  89. }
  90. countDownLatch3.await();
  91. endTime = System.currentTimeMillis();
  92. System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickByLongAdder result: "+clickNumberNet.longAdder.sum());
  93. startTime = System.currentTimeMillis();
  94. for (int i = 1; i <=50; i++) {
  95. new Thread(() -> {
  96. try
  97. {
  98. for (int j = 1; j <=100 * 10000; j++) {
  99. clickNumberNet.clickByLongAccumulator();
  100. }
  101. }finally {
  102. countDownLatch4.countDown();
  103. }
  104. },String.valueOf(i)).start();
  105. }
  106. countDownLatch4.await();
  107. endTime = System.currentTimeMillis();
  108. System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickByLongAccumulator result: "+clickNumberNet.longAccumulator.longValue());
  109. }
  110. }

源码、原理分析

小总结

AtomicLong

N个线程CAS操作修改线程的值,每次只有一个成功过,其它N - 1失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,一下子cpu就打高了。

LongAdder vs AtomicLong Performance

Java 8 Performance Improvements: LongAdder vs AtomicLong | Palomino Labs Blog

LongAdder

9. 聊聊ThreadLocal

ThreadLocal简介

恶心的大厂面试题

  1. ThreadLocal中ThreadLocalMap的数据结构和关系?
  2. ThreadLocal的key是弱引用,这是为什么?
  3. ThreadLocal内存泄露问题你知道吗?
  4. ThreadLocal中最后为什么要加remove方法?

是什么?

能干嘛?

api介绍

永远的helloworld讲起

按照总销售额统计,方便集团公司做计划统计(群雄逐鹿起纷争)

  1. package com.atguigu.juc.tl;
  2. import java.util.concurrent.TimeUnit;
  3. class MovieTicket
  4. {
  5. int number = 50;
  6. public synchronized void saleTicket()
  7. {
  8. if(number > 0)
  9. {
  10. System.out.println(Thread.currentThread().getName()+"\t"+"号售票员卖出第: "+(number--));
  11. }else{
  12. System.out.println("--------卖完了");
  13. }
  14. }
  15. }
  16. /**
  17. * @auther zzyy
  18. * @create 2021-03-23 15:03
  19. * 三个售票员卖完50张票务,总量完成即可,吃大锅饭,售票员每个月固定月薪
  20. */
  21. public class ThreadLocalDemo
  22. {
  23. public static void main(String[] args)
  24. {
  25. MovieTicket movieTicket = new MovieTicket();
  26. for (int i = 1; i <=3; i++) {
  27. new Thread(() -> {
  28. for (int j = 0; j <20; j++) {
  29. movieTicket.saleTicket();
  30. try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
  31. }
  32. },String.valueOf(i)).start();
  33. }
  34. }
  35. }

上述需求变化了...

---------------------------------------------------

不参加总和计算,希望各自分灶吃饭,

各凭销售本事提成,按照出单数各自统计

比如某找房软件,每个中介销售都有自己的销售额指标,自己专属自己的,不和别人掺和

---------------------------------------------------

上述需求该如何处理???

  1. package com.atguigu.juc.tl;
  2. class MovieTicket
  3. {
  4. int number = 50;
  5. public synchronized void saleTicket()
  6. {
  7. if(number > 0)
  8. {
  9. System.out.println(Thread.currentThread().getName()+"\t"+"号售票员卖出第: "+(number--));
  10. }else{
  11. System.out.println("--------卖完了");
  12. }
  13. }
  14. }
  15. class House
  16. {
  17. ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
  18. public void saleHouse()
  19. {
  20. Integer value = threadLocal.get();
  21. value++;
  22. threadLocal.set(value);
  23. }
  24. }
  25. /**
  26. * @auther zzyy
  27. * @create 2021-03-23 15:03
  28. * 1 三个售票员卖完50张票务,总量完成即可,吃大锅饭,售票员每个月固定月薪
  29. *
  30. * 2 分灶吃饭,各个销售自己动手,丰衣足食
  31. */
  32. public class ThreadLocalDemo
  33. {
  34. public static void main(String[] args)
  35. {
  36. /*MovieTicket movieTicket = new MovieTicket();
  37. for (int i = 1; i <=3; i++) {
  38. new Thread(() -> {
  39. for (int j = 0; j <20; j++) {
  40. movieTicket.saleTicket();
  41. try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
  42. }
  43. },String.valueOf(i)).start();
  44. }*/
  45. //===========================================
  46. House house = new House();
  47. new Thread(() -> {
  48. try {
  49. for (int i = 1; i <=3; i++) {
  50. house.saleHouse();
  51. }
  52. System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
  53. }finally {
  54. house.threadLocal.remove();//如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题
  55. }
  56. },"t1").start();
  57. new Thread(() -> {
  58. try {
  59. for (int i = 1; i <=2; i++) {
  60. house.saleHouse();
  61. }
  62. System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
  63. }finally {
  64. house.threadLocal.remove();
  65. }
  66. },"t2").start();
  67. new Thread(() -> {
  68. try {
  69. for (int i = 1; i <=5; i++) {
  70. house.saleHouse();
  71. }
  72. System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
  73. }finally {
  74. house.threadLocal.remove();
  75. }
  76. },"t3").start();
  77. System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
  78. }
  79. }

总结

10. Java对象内存布局和对象头

先从阿里及其它大厂面试题说起

Object object = new Object()谈谈你对这句话的理解?

一般而言JDK8按照默认情况下,new一个对象占多少内存空间

对象在堆内存中布局

再说对象头的MarkWord

32位(看一下即可,不用学了,以64位为准)

64位

markword(64位)分布图

对象布局、GC回收和后面的锁升级就是对象标记MarkWord里面标志位的变化

聊聊Object obj = new Object()

换成其他对象试试

11. Synchronized与锁升级

先从阿里及其它大厂面试题说起

  1. 谈谈你对Synchronized的理解
  2. Synchronized的锁升级你聊聊
  3. Synchronized的性能是不是一定弱于Lock

总纲

synchronized锁:由对象头中的Mark Word根据锁标志位的不同而被复用及锁升级策略

Synchronized的性能变化

java5以前,只有Synchronized,这个是操作系统级别的重量级操作

重量级锁,假如锁的竞争比较激烈的话,性能下降。

Java5之前,用户态和内核态之间的切换

为什么每一个对象都可以成为一个锁????

markOop.hpp

Monitor(监视器锁)

java6开始,优化Synchronized

Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁

需要有个逐步升级的过程,别一开始就捅到重量级锁

synchronized锁种类及升级步骤

多线程访问情况,3种
  1. 只有一个线程来访问,有且唯一Only One
  2. 有2个线程A、B来交替访问
  3. 竞争激烈,多个线程来访问
升级流程

synchronized用的锁是存在Java对象头里的Mark Word中锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位

64位标记图再看

无锁
  1. package com.atguigu.juc.senior.inner.object;
  2. import org.openjdk.jol.info.ClassLayout;
  3. /**
  4. * @auther zzyy
  5. * @create 2020-06-13 11:24
  6. */
  7. public class MyObject
  8. {
  9. public static void main(String[] args)
  10. {
  11. Object o = new Object();
  12. System.out.println("10进制hash码:"+o.hashCode());
  13. System.out.println("16进制hash码:"+Integer.toHexString(o.hashCode()));
  14. System.out.println("2进制hash码:"+Integer.toBinaryString(o.hashCode()));
  15. System.out.println( ClassLayout.parseInstance(o).toPrintable());
  16. }
  17. }

程序不会有锁的竞争

偏锁

a. 主要作用

当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁

同一个老顾客来访,直接老规矩行方便

Hotspot 的作者经过研究发现,大多数情况下:

多线程的情况下,锁不仅不存在多线程竞争,还存在锁由同一线程多次获得的情况,

偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步时提高性能。

b. 64位标记图再看

通过CAS方式修改markword中的线程ID

c. 偏向锁的持有

理论落地:

在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。

那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁)。

如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

假如不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。

技术实现:

一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word中将偏向锁修改状态位,同时还会有占用前54位来存储线程指针作为标识。若该线程再次访问同一个synchronized方法时,该线程只需去对象头的Mark Word 中去判断一下是否有偏向锁指向本身的ID,无需再进入 Monitor 去竞争对象了。

偏向锁JVM命令

java -XX:+PrintFlagsInitial |grep BiasedLock*

一切默认
  1. package com.atguigu.juc.senior.inner.object;
  2. import org.openjdk.jol.info.ClassLayout;
  3. /**
  4. * @auther zzyy
  5. * @create 2020-06-13 11:24
  6. */
  7. public class MyObject
  8. {
  9. public static void main(String[] args)
  10. {
  11. Object o = new Object();
  12. new Thread(() -> {
  13. synchronized (o){
  14. System.out.println(ClassLayout.parseInstance(o).toPrintable());
  15. }
  16. },"t1").start();
  17. }
  18. }

无效果!!!!

因为参数系统默认开启

关闭延时参数,启用该功能

好日子终会到头......o(╥﹏╥)o(开始有第2个线程来抢夺了)

偏向锁的撤销
  1. 当有另外线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁
  2. 竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁。

偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。

撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:

① 第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级

此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。

② 第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向 。

轻锁
主要作用
  1. 有线程来参与锁的竞争,但是获取锁的冲突时间极短
  2. 本质就是自旋锁

64位标记图再看

轻量级锁的获取

Code演示

如果关闭偏向锁,就可以直接进入轻量级锁

-XX:-UseBiasedLocking

自旋达到一定次数和程度
  1. java6之前

  1. java6之后

轻量锁与偏向锁的区别和不同
  1. 争夺轻量级锁失败时,自旋尝试抢占锁
  2. 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁

重锁

有大量的线程参与锁的竞争,冲突性很高

锁标志位

Code演示

总结(各种锁优缺点、synchronized锁升级和实现原理)

synchronized锁升级过程总结:一句话,就是先自旋,不行再阻塞。

实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式

synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。

JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁。

偏向锁:适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。

轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似), 存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。

重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。

JIT编译器对锁的优化

JIT

Just In Time Compiler,一般翻译为即时编译器

锁消除
  1. package com.atguigu.itdachang;
  2. /**
  3. * 锁消除
  4. * 从JIT角度看相当于无视它,synchronized (o)不存在了,这个锁对象并没有被共用扩散到其它线程使用,
  5. * 极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
  6. */
  7. public class LockClearUPDemo
  8. {
  9. static Object objectLock = new Object();//正常的
  10. public void m1()
  11. {
  12. //锁消除,JIT会无视它,synchronized(对象锁)不存在了。不正常的
  13. Object o = new Object();
  14. synchronized (o)
  15. {
  16. System.out.println("-----hello LockClearUPDemo"+"\t"+o.hashCode()+"\t"+objectLock.hashCode());
  17. }
  18. }
  19. public static void main(String[] args)
  20. {
  21. LockClearUPDemo demo = new LockClearUPDemo();
  22. for (int i = 1; i <=10; i++) {
  23. new Thread(() -> {
  24. demo.m1();
  25. },String.valueOf(i)).start();
  26. }
  27. }
  28. }
锁粗化
  1. package com.atguigu.itdachang;
  2. /**
  3. * 锁粗化
  4. * 假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块,
  5. * 加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能
  6. */
  7. public class LockBigDemo
  8. {
  9. static Object objectLock = new Object();
  10. public static void main(String[] args)
  11. {
  12. new Thread(() -> {
  13. synchronized (objectLock) {
  14. System.out.println("11111");
  15. }
  16. synchronized (objectLock) {
  17. System.out.println("22222");
  18. }
  19. synchronized (objectLock) {
  20. System.out.println("33333");
  21. }
  22. },"a").start();
  23. new Thread(() -> {
  24. synchronized (objectLock) {
  25. System.out.println("44444");
  26. }
  27. synchronized (objectLock) {
  28. System.out.println("55555");
  29. }
  30. synchronized (objectLock) {
  31. System.out.println("66666");
  32. }
  33. },"b").start();
  34. }
  35. }

12. AbstractQueuedSynchronizer之AQS

先从阿里及其它大厂面试题说起

是什么

字面意思

抽象的队列同步器

技术解释

是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量。表示持有锁的状态

AQS为什么是JUC内容中最重要的基石

和AQS有关的
1. ReentrantLock

2. CountDownLatch

3.ReentrantReadWriteLock

4. Semaphore

进一步理解锁和同步器的关系
1. 锁,面向锁的使用者

定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可。

2. 同步器,面向锁的实现者

比如Java并发大神DougLee,提出统一规范并简化了锁的实现,

屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。

能干嘛

加锁会导致阻塞

有阻塞就需要排队,实现排队必然需要队列

解释说明

AQS初步

AQS初识
1. 官网解释

2. 有阻塞就需要排队,实现排队必然需要队列

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。

AQS内部体系架构

AQS同步队列的基本结构

从我们的ReentrantLock开始解读AQS

Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的
ReentrantLock的原理

从最简单的lock方法开始看看公平和非公平

非公平锁走起,方法lock()

对比公平锁和非公平锁的 tryAcquire()方法的实现代码,其实差别就在于非公平锁获取锁时比公平锁中少了一个判断 !hasQueuedPredecessors()

hasQueuedPredecessors() 中判断了是否需要排队

导致公平锁和非公平锁的差异如下:

公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;

非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)

13. ReentrantLock、ReentrantReadWriteLock、StampedLock讲解

本章路线总纲

无锁→独占锁→读写锁→邮戳锁

关于锁的大厂面试题

  1. 你知道Java里面有哪些锁?
  2. 你说你用过读写锁,锁饥饿问题是什么?
  3. 有没有比读写锁更快的锁?
  4. StampedLock知道吗?(邮戳锁/票据锁)
  5. ReentrantReadWriteLock有锁降级机制策略你知道吗?

请你简单聊聊ReentrantReadWriteLock

是什么

读写锁说明

『读写锁』意义和特点

特点
  1. 可重入
  2. 读写分离
  3. 无锁无序→加锁→读写锁演变复习

code演示ReentrantReadWriteLockDemo

  1. package com.atguigu.juc.rwlock;
  2. import java.util.HashMap;
  3. import java.util.Map;
  4. import java.util.concurrent.TimeUnit;
  5. import java.util.concurrent.locks.Lock;
  6. import java.util.concurrent.locks.ReentrantLock;
  7. import java.util.concurrent.locks.ReentrantReadWriteLock;
  8. class MyResource
  9. {
  10. Map<String,String> map = new HashMap<>();
  11. //=====ReentrantLock 等价于 =====synchronized
  12. Lock lock = new ReentrantLock();
  13. //=====ReentrantReadWriteLock 一体两面,读写互斥,读读共享
  14. ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
  15. public void write(String key,String value)
  16. {
  17. rwLock.writeLock().lock();
  18. try
  19. {
  20. System.out.println(Thread.currentThread().getName()+"\t"+"---正在写入");
  21. map.put(key,value);
  22. //暂停毫秒
  23. try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
  24. System.out.println(Thread.currentThread().getName()+"\t"+"---完成写入");
  25. }finally {
  26. rwLock.writeLock().unlock();
  27. }
  28. }
  29. public void read(String key)
  30. {
  31. rwLock.readLock().lock();
  32. try
  33. {
  34. System.out.println(Thread.currentThread().getName()+"\t"+"---正在读取");
  35. String result = map.get(key);
  36. //后续开启注释修改为2000,演示一体两面,读写互斥,读读共享,读没有完成时候写锁无法获得
  37. //try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
  38. System.out.println(Thread.currentThread().getName()+"\t"+"---完成读取result:"+result);
  39. }finally {
  40. rwLock.readLock().unlock();
  41. }
  42. }
  43. }
  44. /**
  45. * @auther zzyy
  46. * @create 2021-03-28 11:04
  47. */
  48. public class ReentrantReadWriteLockDemo
  49. {
  50. public static void main(String[] args)
  51. {
  52. MyResource myResource = new MyResource();
  53. for (int i = 1; i <=10; i++) {
  54. int finalI = i;
  55. new Thread(() -> {
  56. myResource.write(finalI +"", finalI +"");
  57. },String.valueOf(i)).start();
  58. }
  59. for (int i = 1; i <=10; i++) {
  60. int finalI = i;
  61. new Thread(() -> {
  62. myResource.read(finalI +"");
  63. },String.valueOf(i)).start();
  64. }
  65. //暂停几秒钟线程
  66. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  67. //读全部over才可以继续写
  68. for (int i = 1; i <=3; i++) {
  69. int finalI = i;
  70. new Thread(() -> {
  71. myResource.write(finalI +"", finalI +"");
  72. },"newWriteThread==="+String.valueOf(i)).start();
  73. }
  74. }
  75. }
  1. 从写锁→读锁,ReentrantReadWriteLock可以降级

《Java 并发编程的艺术》中关于锁降级的说明:

锁降级:将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样)

读写锁降级演示

可以降级

锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性

code演示LockDownGradingDemo

如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略

  1. package com.atguigu.juc.rwlock;
  2. import java.util.concurrent.locks.ReentrantReadWriteLock;
  3. /**
  4. * @auther zzyy
  5. * @create 2021-03-28 10:18
  6. * 锁降级:遵循获取写锁→再获取读锁→再释放写锁的次序,写锁能够降级成为读锁。
  7. *
  8. * 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
  9. */
  10. public class LockDownGradingDemo
  11. {
  12. public static void main(String[] args)
  13. {
  14. ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  15. ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
  16. ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
  17. writeLock.lock();
  18. System.out.println("-------正在写入");
  19. readLock.lock();
  20. System.out.println("-------正在读取");
  21. writeLock.unlock();
  22. }
  23. }

不可锁升级

写锁和读锁是互斥的

写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。

因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。

因此,

分析读写锁ReentrantReadWriteLock,会发现它有个潜在的问题:

读锁全完,写锁有望;写锁独占,读写全堵;

如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,见前面Case《code演示LockDownGradingDemo》

即ReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁

也就是写入必须等待,这是一种悲观的读锁,o(╥﹏╥)o,人家还在读着那,你先别去写,省的数据乱。

================================后续讲解StampedLock时再详细展开=======================

分析StampedLock(后面详细讲解),会发现它改进之处在于:

读的过程中也允许获取写锁介入(相当牛B,读和写两个操作也让你“共享”(注意引号)),这样会导致我们读的数据就可能不一致!

所以,需要额外的方法来判断读的过程中是否有写入,这是一种乐观的读锁,O(∩_∩)O哈哈~。

显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

读写锁之读写规矩,再说降级

Oracle公司ReentrantWriteReadLock源码总结

锁降级 下面的示例代码摘自ReentrantWriteReadLock源码中:

ReentrantWriteReadLock支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级。

解读在最下面:

面试题:有没有比读写锁更快的锁?

邮戳锁StampedLock

无锁→独占锁→读写锁→邮戳锁
是什么
  • StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化。
  • 邮戳锁也叫票据锁
  • stamp(戳记,long类型) 代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值。
它是由锁饥饿问题引出
锁饥饿问题

ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,

假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了

因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写,o(╥﹏╥)o

如何缓解锁饥饿问题?
  • 使用“公平”策略可以一定程度上缓解这个问题 new ReentrantReadWriteLock(true);
  • 但是“公平”策略是以牺牲系统吞吐量为代价的
StampedLock类的乐观读锁闪亮登场

ReentrantReadWriteLock

允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,

读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的synchronized速度要快很多,

原因就是在于ReentrantReadWriteLock支持读并发

StampedLock横空出世

ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。

但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,

所以,在获取乐观读锁后,还需要对结果进行校验。

StampedLock的特点
  1. 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功;
  2. 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
  3. StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)

StampedLock有三种访问模式

①Reading(读模式):功能和ReentrantReadWriteLock的读锁类似

②Writing(写模式):功能和ReentrantReadWriteLock的写锁类似

③Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,

支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式

  1. 乐观读模式code演示 读的过程中也允许获取写锁介入
  1. package com.atguigu.itdachang;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.locks.StampedLock;
  4. /**
  5. * @auther zzyy
  6. * @create 2020-07-22 16:03
  7. */
  8. public class StampedLockDemo
  9. {
  10. static int number = 37;
  11. static StampedLock stampedLock = new StampedLock();
  12. public void write()
  13. {
  14. long stamp = stampedLock.writeLock();
  15. System.out.println(Thread.currentThread().getName()+"\t"+"=====写线程准备修改");
  16. try
  17. {
  18. number = number + 13;
  19. }catch (Exception e){
  20. e.printStackTrace();
  21. }finally {
  22. stampedLock.unlockWrite(stamp);
  23. }
  24. System.out.println(Thread.currentThread().getName()+"\t"+"=====写线程结束修改");
  25. }
  26. //悲观读
  27. public void read()
  28. {
  29. long stamp = stampedLock.readLock();
  30. System.out.println(Thread.currentThread().getName()+"\t come in readlock block,4 seconds continue...");
  31. //暂停几秒钟线程
  32. for (int i = 0; i <4 ; i++) {
  33. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  34. System.out.println(Thread.currentThread().getName()+"\t 正在读取中......");
  35. }
  36. try
  37. {
  38. int result = number;
  39. System.out.println(Thread.currentThread().getName()+"\t"+" 获得成员变量值result:" + result);
  40. System.out.println("写线程没有修改值,因为 stampedLock.readLock()读的时候,不可以写,读写互斥");
  41. }catch (Exception e){
  42. e.printStackTrace();
  43. }finally {
  44. stampedLock.unlockRead(stamp);
  45. }
  46. }
  47. //乐观读
  48. public void tryOptimisticRead()
  49. {
  50. long stamp = stampedLock.tryOptimisticRead();
  51. int result = number;
  52. //间隔4秒钟,我们很乐观的认为没有其他线程修改过number值,实际靠判断。
  53. System.out.println("4秒前stampedLock.validate值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
  54. for (int i = 1; i <=4 ; i++) {
  55. try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
  56. System.out.println(Thread.currentThread().getName()+"\t 正在读取中......"+i+
  57. "秒后stampedLock.validate值(true无修改,false有修改)"+"\t"
  58. +stampedLock.validate(stamp));
  59. }
  60. if(!stampedLock.validate(stamp)) {
  61. System.out.println("有人动过--------存在写操作!");
  62. stamp = stampedLock.readLock();
  63. try {
  64. System.out.println("从乐观读 升级为 悲观读");
  65. result = number;
  66. System.out.println("重新悲观读锁通过获取到的成员变量值result:" + result);
  67. }catch (Exception e){
  68. e.printStackTrace();
  69. }finally {
  70. stampedLock.unlockRead(stamp);
  71. }
  72. }
  73. System.out.println(Thread.currentThread().getName()+"\t finally value: "+result);
  74. }
  75. public static void main(String[] args)
  76. {
  77. StampedLockDemo resource = new StampedLockDemo();
  78. new Thread(() -> {
  79. resource.read();
  80. //resource.tryOptimisticRead();
  81. },"readThread").start();
  82. // 2秒钟时乐观读失败,6秒钟乐观读取成功resource.tryOptimisticRead();,修改切换演示
  83. //try { TimeUnit.SECONDS.sleep(6); } catch (InterruptedException e) { e.printStackTrace(); }
  84. new Thread(() -> {
  85. resource.write();
  86. },"writeThread").start();
  87. }
  88. }
StampedLock的缺点
  1. StampedLock 不支持重入,没有Re开头
  2. StampedLock 的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意。
  3. 使用 StampedLock一定不要调用中断操作,即不要调用interrupt() 方法 (如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly()和写锁writeLockInterruptibly())

终章の回顾

CompletableFuture

“锁”事儿

JMM

synchronized及升级优化

锁的到底是什么

无锁→偏向锁→轻量锁→重量锁
Java对象内存布局和对象头
64位图

CAS

CAS的底层原理

CAS问题

volatile

特性
内存屏障

LockSupport

LockSupport是基于Unsafe类,由JDK提供的线程操作工具类,主要作用就是挂起线程,唤醒线程。

  1. LockSupport.park
  2. LockSupport.unpark
LockSupport.park和Object.wait区别

线程在Object.wait之后必须等到Object.notify才能唤醒

LockSupport可以先unpark线程,等线程执行LockSupport.park是不会挂起的,可以继续执行

AbstractQueuedSynchronizer

volatile+cas机制实现的锁模板,保证了代码的同步性和可见性,而AQS封装了线程阻塞等待挂起,解锁唤醒其他线程的逻辑。AQS子类只需根据状态变量,判断是否可获取锁,是否释放锁,使用LockSupport挂起、唤醒线程即可

  1. //AbstractQueuedSynchronizer.java
  2. public class AbstractQueuedSynchronizer{
  3. //线程节点
  4. static final class Node {
  5. volatile Node prev;
  6. volatile Node next;
  7. volatile Thread thread;
  8. ...
  9. }
  10. //head 等待队列头尾节点
  11. private transient volatile Node head;
  12. private transient volatile Node tail;
  13. private volatile int state; // The synchronization state. 同步状态
  14. ...
  15. //提供CAS操作,状态具体的修改由子类实现
  16. protected final boolean compareAndSetState(int expect, int update) {
  17. return STATE.compareAndSet(this, expect, update);
  18. }
  19. }
出队入队Node

AQS内部维护一个同步队列,元素就是包装了线程的Node。

同步队列中首节点是获取到锁的节点,它在释放锁的时会唤醒后继节点,后继节点获取到锁的时候,会把自己设为首节点。

线程会先尝试获取锁,失败则封装成Node,CAS加入同步队列的尾部。在加入同步队列的尾部时,会判断前驱节点是否是head结点,并尝试加锁(可能前驱节点刚好释放锁),否则线程进入阻塞等待。

ThreadLocal

当使用ThreadLocal声明变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,

每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本

原子增强类

Java高并发编程基础三大利器之Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。很多年以来,我都觉得从字面上很难理解Semaphore所表达的含义,只能把它比作是控制流量的红绿灯,比如XX马路要限制流量,只允许同时有一百辆车在这条路上行使,其他的都必须在路口等待,所以前一百辆车会看到绿灯,可以开进这条马路,后面的车会看到红灯,不能驶入XX马路,但是如果前一百辆中有五辆车已经离开了XX马路,那么后面就允许有5辆车驶入马路,这个例子里说的车就是线程,驶入马路就表示线程在执行,离开马路就表示线程执行完成,看见红灯就表示线程被阻塞,不能执行。

  • Semaphore机制是提供给线程抢占式获取许可,所以他可以实现公平或者非公平,类似于ReentrantLock

说了这么多我们来个实际的例子看一看,比如我们去停车场停车,停车场总共只有5个车位,但是现在有8辆汽车来停车,剩下的3辆汽车要么等其他汽车开走后进行停车,或者去找别的停车位?

  1. public class SemaphoreTest {
  2. public static void main(String[] args) throws InterruptedException {
  3. // 初始化五个车位
  4. Semaphore semaphore = new Semaphore(5);
  5. // 等所有车子
  6. final CountDownLatch latch = new CountDownLatch(8);
  7. for (int i = 0; i < 8; i++) {
  8. int finalI = i;
  9. if (i == 5) {
  10. Thread.sleep(1000);
  11. new Thread(() -> {
  12. stopCarNotWait(semaphore, finalI);
  13. latch.countDown();
  14. }).start();
  15. continue;
  16. }
  17. new Thread(() -> {
  18. stopCarWait(semaphore, finalI);
  19. latch.countDown();
  20. }).start();
  21. }
  22. latch.await();
  23. log("总共还剩:" + semaphore.availablePermits() + "个车位");
  24. }
  25. private static void stopCarWait(Semaphore semaphore, int finalI) {
  26. String format = String.format("车牌号%d", finalI);
  27. try {
  28. semaphore.acquire(1);
  29. log(format + "找到车位了,去停车了");
  30. Thread.sleep(10000);
  31. } catch (Exception e) {
  32. e.printStackTrace();
  33. } finally {
  34. semaphore.release(1);
  35. log(format + "开走了");
  36. }
  37. }
  38. private static void stopCarNotWait(Semaphore semaphore, int finalI) {
  39. String format = String.format("车牌号%d", finalI);
  40. try {
  41. if (semaphore.tryAcquire()) {
  42. log(format + "找到车位了,去停车了");
  43. Thread.sleep(10000);
  44. log(format + "开走了");
  45. semaphore.release();
  46. } else {
  47. log(format + "没有停车位了,不在这里等了去其他地方停车去了");
  48. }
  49. } catch (Exception e) {
  50. e.printStackTrace();
  51. }
  52. }
  53. public static void log(String content) {
  54. // 格式化
  55. DateTimeFormatter fmTime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
  56. // 当前时间
  57. LocalDateTime now = LocalDateTime.now();
  58. System.out.println(now.format(fmTime) + " "+content);
  59. }
  60. }
  61. 2021-03-01 18:54:57 车牌号0找到车位了,去停车了
  62. 2021-03-01 18:54:57 车牌号3找到车位了,去停车了
  63. 2021-03-01 18:54:57 车牌号2找到车位了,去停车了
  64. 2021-03-01 18:54:57 车牌号1找到车位了,去停车了
  65. 2021-03-01 18:54:57 车牌号4找到车位了,去停车了
  66. 2021-03-01 18:54:58 车牌号5没有停车位了,不在这里等了去其他地方停车去了
  67. 2021-03-01 18:55:07 车牌号7找到车位了,去停车了
  68. 2021-03-01 18:55:07 车牌号6找到车位了,去停车了
  69. 2021-03-01 18:55:07 车牌号2开走了
  70. 2021-03-01 18:55:07 车牌号0开走了
  71. 2021-03-01 18:55:07 车牌号3开走了
  72. 2021-03-01 18:55:07 车牌号4开走了
  73. 2021-03-01 18:55:07 车牌号1开走了
  74. 2021-03-01 18:55:17 车牌号7开走了
  75. 2021-03-01 18:55:17 车牌号6开走了
  76. 2021-03-01 18:55:17 总共还剩:5个车位

从输出结果我们可以看到车牌号5这辆车看见没有车位了,就不在这个地方傻傻的等了,而是去其他地方了,但是车牌号6和车牌号7分别需要等到车库开出两辆车空出两个车位后才停进去。这就体现了Semaphore 的acquire 方法如果没有获取到凭证它就会阻塞,而tryAcquire方法如果没有获取到凭证不会阻塞的。

Java高并发编程基础三大利器之Semaphore-腾讯云开发者社区-腾讯云

Java高并发编程基础三大利器之CountDownLatch

CountDownLatch是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就减1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上(调用await方法的线程)等待的线程就可以恢复工作了。

应用场景

CountDownLatch可以用来干什么呢?有什么应用场景?实际项目中有应用的场景吗?这应该才是大家比较关心的。我们先来看看官网提供的例子是如何进行应用的

CountDownLatch (Java Platform SE 8 )

官方提供了两个demo我直接把它转成了图片顺带推荐下这个代码转图片的网址https://www.dute.org/code-snapshot 还挺好用的。

官网demo1

★The first is a start signal that prevents any worker from proceeding until the driver is ready for them to proceed; The second is a completion signal that allows the driver to wait until all workers have completed. ”

  • 第一个开始信号(startSignal)会阻止任何工人(worker )开始工作,在司机到来之前。说白了就是司机没来工人就不能干活。
  • 第二个是完成信号 (doneSignal),允许司机 Driver 等待,直到所有的工人完成.说白了就是司机要等到所有工人完工为止。

官网demo2

★Another typical usage would be to divide a problem into N parts, describe each part with a Runnable that executes that portion and counts down on the latch, and queue all the Runnables to an Executor. When all sub-parts are complete, the coordinating thread will be able to pass through await. ”

另一种典型的用法就是把一个大任务拆分N个部分,让多个线程(Worker)执行,每个线程(Worker)执行完自己的部分计数器就减1,当所有子部分都完成后,Driver 才继续向下执行才继续执行。就好比富士康手机加工的流水线一样,组装一步手机需要一条条的流水线来相互配合完成。一条条流水线(Worker),每条线都干自己的活。有的流水线是贴膜的,有的流水线是打螺丝的,有的流水线是质检的、有的流水线充电的、有的流水线贴膜的。等这些流水线都干完了就把一部手机组装完成了。

Java高并发编程基础三大利器之CountDownLatch-腾讯云开发者社区-腾讯云

Java高并发编程基础三大利器之CyclicBarrier

前面一篇文章我们《Java高并发编程基础三大利器之CountDownLatch》它有一个缺点,就是它的计数器只能够使用一次,也就是说当计数器(state)减到为 0的时候,如果 再有线程调用去 await() 方法,该线程会直接通过,不会再起到等待其他线程执行结果起到同步的作用。为了解决这个问题CyclicBarrier就应运而生了。

CyclicBarrier是什么?把它拆开来翻译就是循环(Cycle)和屏障(Barrier)

它的主要作用其实和CountDownLanch差不多,都是让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障会被打开,所有被屏障阻塞的线程才会继续执行,不过它是可以循环执行的,这是它与CountDownLanch最大的不同。CountDownLanch是只有当最后一个线程把计数器置为0的时候,其他阻塞的线程才会继续执行。

如何使用

我们首先先来看下关于使用CyclicBarrier的一个demo:比如游戏中有个关卡的时候,每次进入下一关的时候都需要进行加载一些地图、特效背景音乐什么的只有全部加载完了才能够进行游戏:

  1. /**demo 来源https://blog.csdn.net/lstcui/article/details/107389371
  2. * 公众号【java金融】
  3. */
  4. public class CyclicBarrierExample {
  5. static class PreTaskThread implements Runnable {
  6. private String task;
  7. private CyclicBarrier cyclicBarrier;
  8. public PreTaskThread(String task, CyclicBarrier cyclicBarrier) {
  9. this.task = task;
  10. this.cyclicBarrier = cyclicBarrier;
  11. }
  12. @Override
  13. public void run() {
  14. for (int i = 0; i < 4; i++) {
  15. Random random = new Random();
  16. try {
  17. Thread.sleep(random.nextInt(1000));
  18. System.out.println(String.format("关卡 %d 的任务 %s 完成", i, task));
  19. cyclicBarrier.await();
  20. } catch (InterruptedException | BrokenBarrierException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. }
  25. public static void main(String[] args) {
  26. CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
  27. System.out.println("本关卡所有的前置任务完成,开始游戏... ...");
  28. });
  29. new Thread(new PreTaskThread("加载地图数据", cyclicBarrier)).start();
  30. new Thread(new PreTaskThread("加载人物模型", cyclicBarrier)).start();
  31. new Thread(new PreTaskThread("加载背景音乐", cyclicBarrier)).start();
  32. }
  33. }
  34. }

我们可以看到每次游戏开始都会等当前关卡把游戏的人物模型,地图数据、背景音乐加载完成后才会开始进行游戏。并且还是可以循环控制的。

Java高并发编程基础三大利器之CyclicBarrier-腾讯云开发者社区-腾讯云

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小惠珠哦/article/detail/812307
推荐阅读
相关标签
  

闽ICP备14008679号