# concurrent-programing **Repository Path**: freedyamazing/concurrent-programing ## Basic Information - **Project Name**: concurrent-programing - **Description**: 学习Java并发编程时写的代码 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-04-19 - **Last Updated**: 2022-06-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 介绍 本笔记是根据[b站的全面深入学习java并发编程](https://www.bilibili.com/video/BV16J411h7Rd)课程所做的笔记 ![img.png](README.assets/img.png) ![img_1.png](README.assets/img_1.png) # 进程与线程 ## 进程与线程 ### 进程 - 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的 - 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。 - 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等) ### 线程 - 一个进程之内可以分为一到多个线程。 - 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 - Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作 为线程的容器 ### 二者对比 - 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集 - 进程拥有共享的资源,如内存空间等,供其内部的线程共享 - 进程间通信较为复杂 - 同一台计算机的进程通信称为 IPC(Inter-process communication) - 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP - 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 - 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低 ## 并行与并发 单核 cpu 下,线程实际还是 `串行执行` 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是 `同时运行的 `。总结为一句话就是:` 微观串行,宏观并行 `, - 一般会将这种 线程轮流使用 CPU 的做法称为并发, concurrent ![img.png](README.assets/1img.png) - 多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。 ![img_2.png](README.assets/1img_2.png) 引用 Rob Pike 的一段描述: - 并发(concurrent)是同一时间应对(dealing with)多件事情的能力 - 并行(parallel)是同一时间动手做(doing)多件事情的能力 例子 - 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发 - 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待) - 雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行 ## 应用 以调用方角度来讲,如果 - 需要等待结果返回,才能继续运行就是同步 - 不需要等待结果返回,就能继续运行就是异步 设计: 多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如 果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停... 结论: - 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程 - tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程 - ui 程序中,开线程进行其他操作,避免阻塞 ui 线程 # java 线程 ## 创建和运行线程 ```java // 方法一,直接使用 Thread Thread t1=new Thread(){ @Override public void run(){ // 要执行的任务 } }; t1.start(); //方法二,使用 Runnable 配合 Thread Runnable runnable=new Runnable(){ @Override public void run(){ // 要执行的任务 } }; Thread t2=new Thread(runnable); t2.start(); //方法三,FutureTask 配合 Thread FutureTask task3=new FutureTask<>(()->{ // 要执行的任务 return null; }); // 参数1 是任务对象; 参数2 是线程名字,推荐 new Thread(task3,"t3").start(); // 主线程阻塞,同步等待 task 执行完毕的结果 Integer result=task3.get(); System.out.println(result); ``` #### 原理之 Thread 与 Runnable 的关系 - 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了 - 用 Runnable 更容易与线程池等高级 API 配合 - 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活 ## 原理之线程运行 ### 栈与栈帧 我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟 机就会为其分配一块栈内存。 - 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存 - 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法 ### 线程上下文切换(Thread Context Switch) 因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码 - 线程的 cpu 时间片用完 - 垃圾回收 - 有更高优先级的线程需要运行 - 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法 当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的 - 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等 - Context Switch 频繁发生会影响性能 ## start 与 run - 直接调用run并不会开启一个新的线程,而会直接在主线程里面执行run里面的代码。 - 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码,并且一个线程不能start多次。 ## sleep 与 yield ### sleep 1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞) 2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException 3. 睡眠结束后的线程未必会立刻得到执行 4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 ### yield 1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程 2. 具体的实现依赖于操作系统的任务调度器 ## 线程优先级 - 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它 - 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用 ```java Runnable task1=()->{ int count=0; for(;;){ System.out.println("---->1 "+count++); } }; Runnable task2=()->{ int count=0; for(;;){ // Thread.yield(); System.out.println(" ---->2 "+count++); } }; Thread t1=new Thread(task1,"t1"); Thread t2=new Thread(task2,"t2"); // t1.setPriority(Thread.MIN_PRIORITY); // t2.setPriority(Thread.MAX_PRIORITY); t1.start(); t2.start(); ``` 正常情况下两个线程打印的`---->1 数字` 和`---->2 数字`应该时差不多的,但是可以通过上述例子来调整优先级或者设置Thread.yield(),可以发现最终打印的`---->1 数字` 和`---->2 数字` 的数量是不一样的。 ## 防止死循环时cpu占用100% ### sleep实现 在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权 给其他程序 ```java while(true){ try{ Thread.sleep(50); }catch(InterruptedException e){ e.printStackTrace(); } } ``` 在没有加sleep的时候发现cpu 完全被循环进程占用了 ![img_3.png](README.assets/1img_3.png) 加了sleep之后cpu的占用率大大降低了,只占用了3.6% ![img_6.png](README.assets/1img_6.png) - 可以用 wait 或 条件变量达到类似的效果 - 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景 - sleep 适用于无需锁同步的场景 ### wait实现 ```java synchronized(锁对象){ while(条件不满足){ try{ 锁对象.wait(); }catch(InterruptedException e){ e.printStackTrace(); } } // do sth... } ``` ### 条件变量实现 ```java lock.lock(); try{ while(条件不满足){ try{ 条件变量.await(); }catch(InterruptedException e){ e.printStackTrace(); } } // do sth... }finally{ lock.unlock(); } ``` ## join 方法详解 ```java static int r=0; public static void main(String[]args)throws InterruptedException{ test1(); } private static void test1()throws InterruptedException{ log.debug("开始"); Thread t1=new Thread(()->{ log.debug("开始"); sleep(1); log.debug("结束"); r=10; }); t1.start(); log.debug("结果为:"+r); log.debug("结束"); } ``` 分析: 1. 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10 2. 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0 解决方法: 1. 用 sleep,但是不好把握时间 2. 用 join,加在 t1.start() 之后即可 以调用方角度来讲,如果 - 需要等待结果返回,才能继续运行就是同步 - 不需要等待结果返回,就能继续运行就是异步 ![img_1.png](README.assets/1img_1.png) ## interrupt 方法详解 ### 打断 sleep,wait,join 的线程 这几个方法都会让线程进入阻塞状态打断 sleep 的线程, 会清空打断状态,以 sleep 为例 ```java private static void test1()throws InterruptedException{ Thread t1=new Thread(()->{ sleep(1); },"t1"); t1.start(); sleep(0.5); t1.interrupt(); log.debug(" 打断状态: {}",t1.isInterrupted()); } ``` 输出 ```java java.lang.InterruptedException:sleep interrupted at java.lang.Thread.sleep(Native Method) at java.lang.Thread.sleep(Thread.java:340) at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) at cn.itcast.n2.util.Sleeper.sleep(Sleeper.java:8) at cn.itcast.n4.TestInterrupt.lambda$test1$3(TestInterrupt.java:59) at java.lang.Thread.run(Thread.java:745) 21:18:10.374[main]c.TestInterrupt-打断状态:false ``` ### 打断正常运行的线程 打断正常运行的线程, 不会清空打断状态 ```java private static void test2()throws InterruptedException{ Thread t2=new Thread(()->{ while(true){ Thread current=Thread.currentThread(); boolean interrupted=current.isInterrupted(); if(interrupted){ log.debug(" 打断状态: {}",interrupted); break; } } },"t2"); t2.start(); sleep(0.5); t2.interrupt(); } ``` 输出 ``` 20:57:37.964 [t2] c.TestInterrupt - 打断状态: true ``` ### 打断 park 线程 打断 park 线程, 不会清空打断状态 ```java private static void test3()throws InterruptedException{ Thread t1=new Thread(()->{ log.debug("park..."); LockSupport.park();//这里会暂停 log.debug("unpark..."); log.debug("打断状态:{}",Thread.currentThread().isInterrupted()); },"t1"); t1.start(); sleep(0.5); t1.interrupt();//打断上面的暂停,即会继续执行下面的代码 } ``` 输出 ```java 21:11:52.795[t1]c.TestInterrupt-park... 21:11:53.295[t1]c.TestInterrupt-unpark... 21:11:53.295[t1]c.TestInterrupt-打断状态:true ``` 如果打断标记已经是 true, 则 park 会失效 ```java private static void test4(){ Thread t1=new Thread(()->{ for(int i=0;i< 5;i++){ log.debug("park..."); LockSupport.park(); log.debug("打断状态:{}",Thread.currentThread().isInterrupted()); } }); t1.start(); sleep(1); t1.interrupt(); } ``` 输出 ```java 21:13:48.783[Thread-0]c.TestInterrupt-park... 21:13:49.809[Thread-0]c.TestInterrupt-打断状态:true 21:13:49.812[Thread-0]c.TestInterrupt-park... 21:13:49.813[Thread-0]c.TestInterrupt-打断状态:true 21:13:49.813[Thread-0]c.TestInterrupt-park... 21:13:49.813[Thread-0]c.TestInterrupt-打断状态:true 21:13:49.813[Thread-0]c.TestInterrupt-park... 21:13:49.813[Thread-0]c.TestInterrupt-打断状态:true 21:13:49.813[Thread-0]c.TestInterrupt-park... 21:13:49.813[Thread-0]c.TestInterrupt-打断状态:true ``` > 提示 > 可以使用 Thread.interrupted() 清除打断状态 ## 不推荐的方法 还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁 - stop() 停止线程运行 - suspend() 挂起(暂停)线程运行 - resume() 恢复线程运行 ## 主线程与守护线程 默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守 护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。 ```java public static void main(String[]args){ log.debug("开始运行..."); Thread t1=new Thread(()->{ log.debug("开始运行..."); sleep(2); log.debug("运行结束..."); },"daemon"); // 设置该线程为守护线程 t1.setDaemon(true); t1.start(); sleep(1); log.debug("运行结束..."); } ``` 输出 ```java 08:26:38.123[main]c.TestDaemon-开始运行... 08:26:38.213[daemon]c.TestDaemon-开始运行... 08:26:39.215[main]c.TestDaemon-运行结束... ``` > 注意 > 垃圾回收器线程就是一种守护线程 > Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 > 待它们处理完当前请求 ## 五种状态 这是从 操作系统 层面来描述的 ![img_4.png](README.assets/1img_4.png) - 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联 - 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行 - 【运行状态】指获取了 CPU 时间片运行中的状态 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换 - 【阻塞状态】 - 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】 - 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】 - 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们 - 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态 ### 六种状态 这是从 Java API 层面来描述的 根据 Thread.State 枚举,分为六种状态 ![img_5.png](README.assets/1img_5.png) - NEW 线程刚被创建,但是还没有调用 start() 方法 - RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行) - BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述 - TERMINATED 当线程代码运行结束 ```java Object lock=new Object(); Thread t1=new Thread(()->{ try{ Thread.sleep(500); synchronized (lock){ } while(!Thread.currentThread().isInterrupted()); }catch(Exception e){ e.printStackTrace(); } },"t1"); Thread t2=new Thread(()->{ synchronized (lock){ while(!Thread.interrupted()); } try{ t1.join(); }catch(Exception e){ e.printStackTrace(); } },"t1"); t2.setDaemon(true); t2.start(); log.info(t1.getState()); t1.start(); log.info(t1.getState()); Thread.sleep(100); log.info(t1.getState()); Thread.sleep(500); log.info(t1.getState()); t2.interrupt(); Thread.sleep(100); log.info(t2.getState()); t1.interrupt(); t1.join(); log.info(t1.getState()); //输出结果 [INFO]2021-04-20 21:23:02,530[main]---->NEW//初始状态 [INFO]2021-04-20 21:23:02,530[main]---->RUNNABLE//运行状态 或 可运行 [INFO]2021-04-20 21:23:02,631[main]---->TIMED_WAITING//睡眠等待状态 [INFO]2021-04-20 21:23:03,132[main]---->BLOCKED//阻塞状态 [INFO]2021-04-20 21:23:03,233[main]---->WAITING//等待状态 [INFO]2021-04-20 21:23:03,233[main]---->TERMINATED//终止状态 ``` ## 本章小结 本章的重点在于掌握 - 线程创建 - 线程重要 api,如 start,run,sleep,join,interrupt 等 - 线程状态 - 应用方面 - 异步调用:主线程执行期间,其它线程异步执行耗时操作 - 提高效率:并行计算,缩短运算时间 - 同步等待:join - 统筹规划:合理使用线程,得到最优效果 - 原理方面 - 线程运行流程:栈、栈帧、上下文切换、程序计数器 - Thread 两种创建方式 的源码 - 模式方面 - 终止模式之两阶段终止 # 共享模型之管程 ## 共享带来的问题 ### 小故事 - 老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快 - ![img_2.png](README.assets/img_2.png) - 小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用 - 但小南不能一天24小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操作),有时还需要一根烟,没烟时思路全无(wait)这些情况统称为(阻塞) - ![img_4.png](README.assets/img_4.png) - 在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算 - 另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平 - 于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ] - 这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然 - 最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了一个笔记本(主存),把一些中间结果先记在本上 - 计算流程是这样的 - ![img_5.png](README.assets/img_5.png) - 但是由于分时系统,有一天还是发生了事故 - 小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果 - 老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1...] 不甘心地到一边待着去了(上下文切换) - 老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本 - 这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写入了笔记本 - ![img_6.png](README.assets/img_6.png) - 小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0 ### Java 的体现 两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗? ```java static int counter=0; public static void main(String[]args)throws InterruptedException{ Thread t1=new Thread(()->{ for(int i=0;i< 5000;i++){ counter++; } },"t1"); Thread t2=new Thread(()->{ for(int i=0;i< 5000;i++){ counter--; } },"t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("{}",counter); } ``` 多次运行后发现结果并不是0,而是一直在变化的随机值 ### 问题分析 以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理 解,必须从字节码来进行分析 例如对于 `i++` 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令: ```java getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 iadd // 自增 putstatic i // 将修改后的值存入静态变量i ``` 而对应 `i--` 也是类似: ```java getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 isub // 自减 putstatic i // 将修改后的值存入静态变量i ``` 而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换: ![img_8.png](README.assets/img_8.png) 如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题: ![img_9.png](README.assets/img_9.png) 但多线程下这 8 行代码可能交错运行: - 出现负数的情况: ![img_10.png](README.assets/img_10.png) - 出现正数的情况: ![img_11.png](README.assets/img_11.png) ### 临界区 Critical Section - 一个程序运行多个线程本身是没有问题的 - 问题出在多个线程访问共享资源 - 多个线程读共享资源其实也没有问题 - 在多个线程对共享资源读写操作时发生指令交错,就会出现问题 - 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区 例如,下面代码中的临界区 ```java static int counter=0; static void increment() // 临界区 { counter++; } static void decrement() // 临界区 { counter--; } ``` ### 竞态条件 Race Condition 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件 ## synchronized 解决方案 ### 应用之互斥 为了避免临界区的竞态条件发生,有多种手段可以达到目的。 - 阻塞式的解决方案:synchronized,Lock - 非阻塞式的解决方案:原子变量 本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换 > 注意 > 虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的: > >- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码 >- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点 ### synchronized 语法 ```java synchronized(对象) // 线程1, 线程2(blocked) { 临界区 } ``` 解决 ```java static int counter=0; static final Object room=new Object(); public static void main(String[]args)throws InterruptedException{ Thread t1=new Thread(()->{ for(int i=0;i< 5000;i++){ synchronized (room){ counter++; } } },"t1"); Thread t2=new Thread(()->{ for(int i=0;i< 5000;i++){ synchronized (room){ counter--; } } },"t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("{}",counter); } ``` ![img_12.png](README.assets/img_12.png) 你可以做这样的类比: - synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人 进行计算,线程 t1,t2 想象成两个人 - 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码 - 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切 换,阻塞住了 - 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦), 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才 能开门进入 - 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥 匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码 用图来表示 ![img_13.png](README.assets/img_13.png) ### 思考 synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切 换所打断。 为了加深理解,请思考下面的问题 - 如果把 `synchronized(obj)` 放在 for 循环的外面,如何理解?-- 原子性 - 如果 t1 `synchronized(obj1)` 而 t2 `synchronized(obj2)` 会怎样运作?-- 锁对象 - 如果 t1 `synchronized(obj)`而 t2 没有加会怎么样?如何理解?-- 锁对象 ### 面向对象改进 把需要保护的共享变量放入一个类 ```java public class Test3 { public static void main(String[] args) throws InterruptedException { Room room = new Room(); Thread t1 = new Thread(() -> { for (int j = 0; j < 5000; j++) { room.increment(); } }, "t1"); Thread t2 = new Thread(() -> { for (int j = 0; j < 5000; j++) { room.decrement(); } }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("count: {}", room.get()); } } class Room { int value = 0; public void increment() { synchronized (this) { value++; } } public void decrement() { synchronized (this) { value--; } } public int get() { synchronized (this) { return value; } } } ``` ## 方法上的 synchronized ```java class Test { public synchronized void test() { } } 等价于 class Test { public void test() { synchronized (this) { } } } //********************************************************** class Test { public synchronized static void test() { } } 等价于 class Test { public static void test() { synchronized (Test.class) { } } } ``` ### 不加 synchronized 的方法 不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的) ### 所谓的“线程八锁” 其实就是考察 synchronized 锁住的是哪个对象: 情况1:12 或 21 ```java public class Test4 { public static void main(String[] args) { Number n1 = new Number(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n1.b(); }).start(); } } @Slf4j(topic = "c.Number") class Number { public synchronized void a() { log.debug("1"); } public synchronized void b() { log.debug("2"); } } ``` 情况2:1s后12,或 2 1s后 1 ```java public class Test5 { public static void main(String[] args) { Number1 n1 = new Number1(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n1.b(); }).start(); } } @Slf4j class Number1 { public synchronized void a() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("1"); } public synchronized void b() { log.debug("2"); } } ``` 情况3:3 1s 12 或 23 1s 1 或 32 1s 1 ```java public class Test6 { public static void main(String[] args) { Number2 n1 = new Number2(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n1.b(); }).start(); new Thread(() -> { n1.c(); }).start(); } } @Slf4j class Number2 { public synchronized void a() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("1"); } public synchronized void b() { log.debug("2"); } public void c() { log.debug("3"); } } ``` 情况4:2 1s 后 1 ```java public class Test7 { public static void main(String[] args) { Number3 n1 = new Number3(); Number3 n2 = new Number3(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n2.b(); }).start(); } } @Slf4j class Number3 { public synchronized void a() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("1"); } public synchronized void b() { log.debug("2"); } } ``` 情况5:2 1s 后 1 ```java public class Test8 { public static void main(String[] args) { Number4 n1 = new Number4(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n1.b(); }).start(); } } @Slf4j class Number4 { public static synchronized void a() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("1"); } public synchronized void b() { log.debug("2"); } } ``` 情况6:1s 后12, 或 2 1s后 1 ```java public class Test9 { public static void main(String[] args) { Number5 n1 = new Number5(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n1.b(); }).start(); } } @Slf4j class Number5 { public static synchronized void a() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("1"); } public static synchronized void b() { log.debug("2"); } } ``` 情况7:2 1s 后 1 ```java public class Test10 { public static void main(String[] args) { Number6 n1 = new Number6(); Number6 n2 = new Number6(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n2.b(); }).start(); } } @Slf4j class Number6 { public static synchronized void a() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("1"); } public synchronized void b() { log.debug("2"); } } ``` 情况8:1s 后12, 或 2 1s后 1 ```java public class Test11 { public static void main(String[] args) { Number7 n1 = new Number7(); Number7 n2 = new Number7(); new Thread(() -> { n1.a(); }).start(); new Thread(() -> { n2.b(); }).start(); } } @Slf4j class Number7 { public static synchronized void a() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("1"); } public static synchronized void b() { log.debug("2"); } } ``` ## 变量的线程安全分析 ### 成员变量和静态变量是否线程安全? - 如果它们没有共享,则线程安全 - 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况 - 如果只有读操作,则线程安全 - 如果有读写操作,则这段代码是临界区,需要考虑线程安全 ### 局部变量是否线程安全? - 局部变量是线程安全的 - 但局部变量引用的对象则未必 - 如果该对象没有逃离方法的作用访问,它是线程安全的 - 如果该对象逃离方法的作用范围,需要考虑线程安全 ### 变量线程安全分析 ```java public static void test1(){ int i=10; i++; } ``` 每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享 ```java public static void test1(); descriptor:()V flags:ACC_PUBLIC,ACC_STATIC Code: stack=1,locals=1,args_size=0 0:bipush 10 2:istore_0 3:iinc 0,1 6:return LineNumberTable: line 10:0 line 11:3 line 12:6 LocalVariableTable: Start Length Slot Name Signature 3 4 0 i I ``` ![img_15.png](README.assets/img_15.png) 局部变量的引用稍有不同 先看一个成员变量的例子: ```java class ThreadUnsafe { ArrayList list = new ArrayList<>(); public void method1(int loopNumber) { for (int i = 0; i < loopNumber; i++) { // { 临界区, 会产生竞态条件 method2(); method3(); // } 临界区 } } private void method2() { list.add("1"); } private void method3() { list.remove(0); } } ``` 其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错: ```java Exception in thread"Thread1"java.lang.IndexOutOfBoundsException:Index:0,Size:0 at java.util.ArrayList.rangeCheck(ArrayList.java:657) at java.util.ArrayList.remove(ArrayList.java:496) at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) at java.lang.Thread.run(Thread.java:748) ``` 分析: - 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量 - method3 与 method2 分析相同 ![img_16.png](README.assets/img_16.png) 将 list 修改为局部变量 ```java class ThreadSafe { public final void method1(int loopNumber) { ArrayList list = new ArrayList<>(); for (int i = 0; i < loopNumber; i++) { method2(list); method3(list); } } private void method2(ArrayList list) { list.add("1"); } private void method3(ArrayList list) { list.remove(0); } } ``` 那么就不会有上述问题了 分析: - list 是局部变量,每个线程调用时会创建其不同实例,没有共享 - 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象 - method3 的参数分析与 method2 相同 ![img_17.png](README.assets/img_17.png) 方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题? - 情况1:有其它线程调用 method2 和 method3 - 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即 ```java class ThreadSafe { public final void method1(int loopNumber) { ArrayList list = new ArrayList<>(); for (int i = 0; i < loopNumber; i++) { method2(list); method3(list); } } public void method2(ArrayList list) { list.add("1"); } public void method3(ArrayList list) { list.remove(0); } } class ThreadSafeSubClass extends ThreadSafe { @Override public void method3(ArrayList list) { new Thread(() -> { list.remove(0); }).start(); } } ``` > 从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】 ### 常见线程安全类 - String - Integer - StringBuffer - Random - Vector - Hashtable - java.util.concurrent 包下的类 ==这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为它们的每方法是原子的但注意它们多个方法的组合不是原子的,见后面分析== ```java Hashtable table=new Hashtable(); new Thread(()->{ table.put("key","value1"); }).start(); new Thread(()->{ table.put("key","value2"); }).start(); ``` #### 线程安全类方法的组合 分析下面代码是否线程安全 ```java Hashtable table = new Hashtable(); // 线程1,线程2 if( table.get("key") == null) { table.put("key", value); } ``` ![image-20210421133220561](E:\MyProjects\concurrent-programing\README.assets\image-20210421133220561.png) #### 不可变类线程安全性 String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的 有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安 全的呢? 因为String 的replace,substring 等方法都是通过重新创建一个string对象,即只是地址引用。 ## 练习 卖票练习、转账练习见代码 ## Monitor 概念 ### Java 对象头 以 32 位虚拟机为例 普通对象: ![image-20210421133554891](E:\MyProjects\concurrent-programing\README.assets\image-20210421133554891.png) 数组对象: ![image-20210421133608179](E:\MyProjects\concurrent-programing\README.assets\image-20210421133608179.png) 其中 Mark Word 结构为: ![image-20210421133620038](E:\MyProjects\concurrent-programing\README.assets\image-20210421133620038.png) 64 位虚拟机 Mark Word: ![image-20210421133638712](E:\MyProjects\concurrent-programing\README.assets\image-20210421133638712.png) > [参考资料](https://stackoverflow.com/questions/26357186/what-is-in-java-object-header) ### 原理之 Monitor(锁) Monitor 被翻译为监视器或管程 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针 Monitor 结构如下 ![image-20210421200232687](E:\MyProjects\concurrent-programing\README.assets\image-20210421200232687.png) - 刚开始 Monitor 中 Owner 为 null - 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 个 Owner - 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED - Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的 - 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析 >注意: >synchronized 必须是进入同一个对象的 monitor 才有上述的效果 >不加 synchronized 的对象不会关联监视器,不遵从以上规则 ### synchronized 原理 ```java static final Object lock = new Object(); static int counter = 0; public static void main(String[] args) { synchronized (lock) { counter++; } } ``` 对应的字节码为 ```java public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: getstatic #2 // <- lock引用 (synchronized开始) 3: dup 4: astore_1 // lock引用 -> slot 1 5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针 6: getstatic #3 // <- i 9: iconst_1 // 准备常数 1 10: iadd // +1 11: putstatic #3 // -> i 14: aload_1 // <- lock引用 15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList 16: goto 24 19: astore_2 // e -> slot 2 20: aload_1 // <- lock引用 21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList 22: aload_2 // <- slot 2 (e) 23: athrow // throw e 24: return Exception table: from to target type 6 16 19 any 19 22 19 any LineNumberTable: line 8: 0 line 9: 6 line 10: 14 line 11: 24 LocalVariableTable: Start Length Slot Name Signature 0 25 0 args [Ljava/lang/String; StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 19 locals = [ class "[Ljava/lang/String;", class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 ``` >注意 >方法级别的 synchronized 不会在字节码指令中有所体现 ## 原理之 synchronized 进阶 ### 轻量级锁 轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以 使用轻量级锁来优化。 轻量级锁对使用者是透明的,即语法仍然是 `synchronized` 假设有两个方法同步块,利用同一个对象加锁 ```java static final Object obj = new Object(); public static void method1() { synchronized( obj ) { // 同步块 A method2(); } } public static void method2() { synchronized( obj ) { // 同步块 B } } ``` - 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word ![image-20210421200827642](E:\MyProjects\concurrent-programing\README.assets\image-20210421200827642.png) - 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录 ![image-20210421200855936](E:\MyProjects\concurrent-programing\README.assets\image-20210421200855936.png) - 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下 ![image-20210421200928193](E:\MyProjects\concurrent-programing\README.assets\image-20210421200928193.png) - 如果 cas 失败,有两种情况 - 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程 - 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数 ![image-20210421200958184](E:\MyProjects\concurrent-programing\README.assets\image-20210421200958184.png) - 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一 ![image-20210421201017080](E:\MyProjects\concurrent-programing\README.assets\image-20210421201017080.png) - 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头 - 成功,则解锁成功 - 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程 ### 锁膨胀 如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。 ```java static Object obj = new Object(); public static void method1() { synchronized( obj ) { // 同步块 } } ``` - 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁 ![image-20210421201118700](E:\MyProjects\concurrent-programing\README.assets\image-20210421201118700.png) - 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程 - 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址 - 然后自己进入 Monitor 的 EntryList BLOCKED ![image-20210421201135628](E:\MyProjects\concurrent-programing\README.assets\image-20210421201135628.png) - 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁 流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程 ### 自旋优化 重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步 块,释放了锁),这时当前线程就可以避免阻塞。 自旋重试成功的情况: ![image-20210421201209665](E:\MyProjects\concurrent-programing\README.assets\image-20210421201209665.png) 自旋重试失败的情况: ![image-20210421201230916](E:\MyProjects\concurrent-programing\README.assets\image-20210421201230916.png) - 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。 - 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。 - Java 7 之后不能控制是否开启自旋功能 ### 偏向锁 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。 Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有 如: ```java static final Object obj = new Object(); public static void m1() { synchronized( obj ) { // 同步块 A m2(); } } public static void m2() { synchronized( obj ) { // 同步块 B m3(); } } public static void m3() { synchronized( obj ) { // 同步块 C } } ``` ![image-20210421201401700](E:\MyProjects\concurrent-programing\README.assets\image-20210421201401700.png) ![image-20210421201409451](E:\MyProjects\concurrent-programing\README.assets\image-20210421201409451.png) #### 偏向状态 回忆一下对象头格式 ![image-20210421201435080](E:\MyProjects\concurrent-programing\README.assets\image-20210421201435080.png) 一个对象创建时: - 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0 - 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 - XX:BiasedLockingStartupDelay=0 来禁用延迟 - 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、 age 都为 0,第一次用到 hashcode 时才会赋值 1) 测试延迟特性 2) 测试偏向锁 ```java class Dog {} ``` 利用 jol 第三方工具来查看对象头信息(注意这里我扩展了 jol 让它输出更为简洁) ```java public static void main(String[] args) throws IOException { Dog d = new Dog(); ClassLayout classLayout = ClassLayout.parseInstance(d); new Thread(() -> { log.debug("synchronized 前"); System.out.println(classLayout.toPrintableSimple(true)); synchronized (d) { log.debug("synchronized 中"); System.out.println(classLayout.toPrintableSimple(true)); } log.debug("synchronized 后"); System.out.println(classLayout.toPrintableSimple(true)); }, "t1").start(); } ``` 输出 ```java 11:08:58.117 c.TestBiased [t1] - synchronized 前 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 11:08:58.121 c.TestBiased [t1] - synchronized 中 00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101 11:08:58.121 c.TestBiased [t1] - synchronized 后 00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101 ``` >注意 >处于偏向锁的对象解锁后,线程 id 仍存储于对象头中 3)测试禁用 在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁 输出: ```java 11:13:10.018 c.TestBiased [t1] - synchronized 前 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 11:13:10.021 c.TestBiased [t1] - synchronized 中 00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000 11:13:10.021 c.TestBiased [t1] - synchronized 后 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 ``` 4) 测试 hashCode - 正常状态对象一开始是没有 hashCode 的,第一次调用才生成 #### 撤销 - 调用对象 hashCode 调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被 撤销 - 轻量级锁会在锁记录中记录 hashCode - 重量级锁会在 Monitor 中记录 hashCode 在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking 输出: ```java 11:22:10.386 c.TestBiased [main] - 调用 hashCode:1778535015 11:22:10.391 c.TestBiased [t1] - synchronized 前 00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001 11:22:10.393 c.TestBiased [t1] - synchronized 中 00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000 11:22:10.393 c.TestBiased [t1] - synchronized 后 00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001 ``` #### 撤销 - 其它线程使用对象 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁 ```java private static void test2() throws InterruptedException { Dog d = new Dog(); Thread t1 = new Thread(() -> { synchronized (d) { log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); } synchronized (TestBiased.class) { TestBiased.class.notify(); } // 如果不用 wait/notify 使用 join 必须打开下面的注释 // 因为:t1 线程不能结束,否则底层线程可能被 jvm 重用作为 t2 线程,底层线程 id 是一样的 /*try { System.in.read(); } catch (IOException e) { e.printStackTrace(); }*/ }, "t1"); t1.start(); Thread t2 = new Thread(() -> { synchronized (TestBiased.class) { try { TestBiased.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); synchronized (d) { log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); } log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); }, "t2"); t2.start(); } ``` 输出 ```java [t1] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101 [t2] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101 [t2] - 00000000 00000000 00000000 00000000 00011111 10110101 11110000 01000000 [t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 ``` #### 撤销 - 调用 wait/notify ```java public static void main(String[] args) throws InterruptedException { Dog d = new Dog(); Thread t1 = new Thread(() -> { log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); synchronized (d) { log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); try { d.wait(); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); } }, "t1"); t1.start(); new Thread(() -> { try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (d) { log.debug("notify"); d.notify(); } }, "t2").start(); } ``` 输出 ```java [t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 [t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101 [t2] - notify [t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010 ``` #### 批量重偏向 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象 的 Thread ID 当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至 加锁线程 ```java private static void test3() throws InterruptedException { Vector list = new Vector<>(); Thread t1 = new Thread(() -> { for (int i = 0; i < 30; i++) { Dog d = new Dog(); list.add(d); synchronized (d) { log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } } synchronized (list) { list.notify(); } }, "t1"); t1.start(); Thread t2 = new Thread(() -> { synchronized (list) { try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("===============> "); for (int i = 0; i < 30; i++) { Dog d = list.get(i); log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); synchronized (d) { log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } }, "t2"); t2.start(); } ``` 输出: ```java [t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - ===============> [t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 1 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 2 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 3 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 3 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 4 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 5 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 6 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 6 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 7 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 7 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 8 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 9 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 9 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 10 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 10 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 11 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 11 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 12 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 12 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 13 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 13 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 14 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 14 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 15 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 15 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 16 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 16 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 17 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 17 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 18 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 ``` #### 批量撤销 当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象 都会变为不可偏向的,新建的对象也是不可偏向的 ```java static Thread t1,t2,t3; private static void test4() throws InterruptedException { Vector list = new Vector<>(); int loopNumber = 39; t1 = new Thread(() -> { for (int i = 0; i < loopNumber; i++) { Dog d = new Dog(); list.add(d); synchronized (d) { log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } } LockSupport.unpark(t2); }, "t1"); t1.start(); t2 = new Thread(() -> { LockSupport.park(); log.debug("===============> "); for (int i = 0; i < loopNumber; i++) { Dog d = list.get(i); log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); synchronized (d) { log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } LockSupport.unpark(t3); }, "t2"); t2.start(); t3 = new Thread(() -> { LockSupport.park(); log.debug("===============> "); for (int i = 0; i < loopNumber; i++) { Dog d = list.get(i); log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); synchronized (d) { log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } }, "t3"); t3.start(); t3.join(); log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true)); } ``` >参考资料 >https://github.com/farmerjohngit/myblog/issues/12 >https://www.cnblogs.com/LemonFive/p/11246086.html >https://www.cnblogs.com/LemonFive/p/11248248.html >偏向锁论文 ### 锁消除 ```java @Fork(1) @BenchmarkMode(Mode.AverageTime) @Warmup(iterations=3) @Measurement(iterations=5) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class MyBenchmark { static int x = 0; @Benchmark public void a() throws Exception { x++; } @Benchmark public void b() throws Exception { Object o = new Object(); synchronized (o) { x++; } } } ``` ```java //java -jar benchmarks.jar Benchmark Mode Samples Score Score error Units c.i.MyBenchmark.a avgt 5 1.542 0.056 ns/op c.i.MyBenchmark.b avgt 5 1.518 0.091 ns/op //java -XX:-EliminateLocks -jar benchmarks.jar Benchmark Mode Samples Score Score error Units c.i.MyBenchmark.a avgt 5 1.507 0.108 ns/op c.i.MyBenchmark.b avgt 5 16.976 1.572 ns/op ``` ### 锁粗化 对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。 ## wait notify ### wait notify 原理 ![image-20210422090945347](E:\MyProjects\concurrent-programing\README.assets\image-20210422090945347.png) - Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态 - BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片 - BLOCKED 线程会在 Owner 线程释放锁时唤醒 - WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争 ### API 介绍 - `obj.wait() `让进入 object 监视器的线程到 waitSet 等待 - `obj.notify() `在 object 上正在 waitSet 等待的线程中挑一个唤醒 - `obj.notifyAll()`让 object 上正在 waitSet 等待的线程全部唤醒 它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法 ```java final static Object obj = new Object(); public static void main(String[] args) throws InterruptedException { new Thread(() -> { synchronized (obj) { log.debug("执行...."); try { obj.wait(); // 让线程在obj上一直等待下去 } catch (InterruptedException e) { e.printStackTrace(); } log.debug("其它代码...."); } }).start(); new Thread(() -> { synchronized (obj) { log.debug("执行...."); try { obj.wait(); // 让线程在obj上一直等待下去 } catch (InterruptedException e) { e.printStackTrace(); } log.debug("其它代码...."); } }).start(); // 主线程两秒后执行 sleep(2000); log.debug("唤醒 obj 上其它线程"); synchronized (obj) { obj.notify(); // 唤醒obj上一个线程 // obj.notifyAll(); // 唤醒obj上所有等待线程 } } ``` notify 的一种结果 ```java 20:00:53.096 [Thread-0] c.TestWaitNotify - 执行.... 20:00:53.099 [Thread-1] c.TestWaitNotify - 执行.... 20:00:55.096 [main] c.TestWaitNotify - 唤醒 obj 上其它线程 20:00:55.096 [Thread-0] c.TestWaitNotify - 其它代码.... ``` notifyAll 的结果 ```java 19:58:15.457 [Thread-0] c.TestWaitNotify - 执行.... 19:58:15.460 [Thread-1] c.TestWaitNotify - 执行.... 19:58:17.456 [main] c.TestWaitNotify - 唤醒 obj 上其它线程 19:58:17.456 [Thread-1] c.TestWaitNotify - 其它代码.... 19:58:17.456 [Thread-0] c.TestWaitNotify - 其它代码.... ``` `wait() `方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止 `wait(long n) `有时限的等待, 到 n 毫秒后结束等待,或是被 notify ## wait notify 的正确姿势 开始之前先看看 **`sleep(long n)` 和` wait(long n) `的区别** 1. sleep 是 Thread 方法,而 wait 是 Object 的方法 2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用 3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁 4. 它们状态 TIMED_WAITING ### 实现 - 在使用wait notify的时候,应该在所有需要等待的地方使用如下结构,然后每次唤醒都要全部唤醒。 - 这时满足条件的就都会被唤醒,而不满足条件的就会继续等待,直到下一个程序将他们唤醒。 - 这样就可以达到唤醒指定线程的目的 ```java synchronized(lock) { while(条件不成立) { lock.wait(); } // 干活 } //另一个线程 synchronized(lock) { lock.notifyAll(); } ``` ### 同步模式之保护性暂停 #### 定义 即 Guarded Suspension,用在一个线程等待另一个线程的执行结果 要点 - 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject - 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者) - JDK 中,join 的实现、Future 的实现,采用的就是此模式 - 因为要等待另一方的结果,因此归类到同步模式 ![image-20210422191906605](E:\MyProjects\concurrent-programing\README.assets\image-20210422191906605.png) #### 实现 ```java public class GuardedSuspension { public static void main(String[] args) { Guarded guarded = new Guarded(); //线程1等待线程2的下载结果 new Thread(()->{ log.info("等待结果"); Object o = guarded.get(10000); log.info(String.valueOf(o)); },"t1").start(); new Thread(()->{ //模拟下载操作 for (int i = 0; i < 5; i++) { try { log.info(i+"\t正在下载......"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } guarded.complete(i==4?"ok!!!!!":null); } },"t2").start(); } } class Guarded { private long id; //结果 private Object response; public Guarded() { } public Guarded(long id) { this.id = id; } public long getId() { return id; } public Object get(){ return get(0); } /** * 获取结果 * @param timeout 表示等待的超时时间。 当timeout为0时表示没有超时限制,即一直等待 * @return 返回获取的结果 若等待超时则返回的为null */ public Object get(long timeout){ long begin = System.currentTimeMillis(); long passedTime = 0; synchronized (this){ while (response==null&&(passedTime 0 正在下载...... [INFO ] 19:20:47 [t1] -----> 等待结果 [INFO ] 19:20:48 [t2] -----> 1 正在下载...... [INFO ] 19:20:49 [t2] -----> 2 正在下载...... [INFO ] 19:20:50 [t2] -----> 3 正在下载...... [INFO ] 19:20:51 [t2] -----> 4 正在下载...... [INFO ] 19:20:52 [t1] -----> ok!!!!! ``` #### 原理之 join 下面时Thread中join的源码,其设计模式也是参考上面的保护性暂停模式。 ```java public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } } ``` #### 多任务版 GuardedObject 图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右 侧的 t1,t3,t5 就好比邮递员 如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类, 这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理 ![image-20210422192426555](E:\MyProjects\concurrent-programing\README.assets\image-20210422192426555.png) Guarded类参考上面的实现。 中间解耦类: ```java public class MailBox { private static final Map boxes = new ConcurrentHashMap<>(); private static long id = 1; //产生唯一id private static synchronized long generateId() { return id++; } public static Guarded createGuarded() { Guarded guarded = new Guarded(generateId()); boxes.put(guarded.getId(), guarded); return guarded; } public static Guarded getGuarded(long id){ return boxes.remove(id); } public static Set getIds() { return boxes.keySet(); } } ``` 业务相关类和测试类: ```java public class MultiGuardedSuspension { public static void main(String[] args) throws Exception { for (int i = 1; i <= 3; i++) { new Consumer("消费者"+i).start(); } Thread.sleep(1000); for (Long id : MailBox.getIds()) { new Producer("生产者"+id,id,id+"🆗okkkkkkkkkkk!!!!!").start(); System.in.read(); } } } @Slf4j(topic = "Consumer") class Consumer extends Thread{ private final Guarded guarded; public Consumer(String name) { super(name); guarded = MailBox.createGuarded(); } public long getId() { return guarded.getId(); } @Override public void run() { log.info("消费 id:{}",guarded.getId()); Object o = guarded.get(); log.info("消费结果:{}", o); } } @Slf4j(topic = "Producer") class Producer extends Thread{ private final long id; private final Object content; public Producer(String name,long id, Object content) { super(name); this.id = id; this.content = content; } @Override public void run() { Guarded guarded = MailBox.getGuarded(id); log.info("给id:{}生产",id); guarded.complete(content); log.info("----------生产完成----------"); } } ``` 某次运行结果 ```java [INFO ] 19:25:59 [消费者1] -----> 消费 id:1 [INFO ] 19:25:59 [消费者2] -----> 消费 id:2 [INFO ] 19:25:59 [消费者3] -----> 消费 id:3 [INFO ] 19:26:00 [生产者1] -----> 给id:1生产 [INFO ] 19:26:00 [消费者1] -----> 消费结果:1🆗okkkkkkkkkkk!!!!! [INFO ] 19:26:00 [生产者1] -----> ----------生产完成---------- [INFO ] 19:26:05 [生产者2] -----> 给id:2生产 [INFO ] 19:26:05 [生产者2] -----> ----------生产完成---------- [INFO ] 19:26:05 [消费者2] -----> 消费结果:2🆗okkkkkkkkkkk!!!!! [INFO ] 19:26:05 [生产者3] -----> 给id:3生产 [INFO ] 19:26:05 [消费者3] -----> 消费结果:3🆗okkkkkkkkkkk!!!!! [INFO ] 19:26:05 [生产者3] -----> ----------生产完成---------- ``` ### 异步模式之生产者/消费者 #### 定义 要点 - 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应 - 消费队列可以用来平衡生产和消费的线程资源 - 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据 - 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据 - JDK 中各种阻塞队列,采用的就是这种模式 ![image-20210422192739010](E:\MyProjects\concurrent-programing\README.assets\image-20210422192739010.png) #### 实现 ```java @Slf4j public class ProducerConsumer { public static void main(String[] args) { MessageQueue queue = new MessageQueue(3); for (int i = 1; i <= 10; i++) { int finalI = i; new Thread(() ->{ log.info("生产者{} 开始发送消息", finalI); queue.put(new Message(finalI, "lovlove you hahae yolove you hahau hlove you hahaahalove you haha". substring(finalI,finalI*5))); log.info("生产者{}----->消息发送成功<-----", finalI); }, "生产者" + i).start(); } new Thread(() -> { for (;;) { try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } System.out.println(); log.info("接受到消息{}",queue.take()); } }, "消费者").start(); } } /** * 消息队列类 java线程之间的通讯 */ @Slf4j @AllArgsConstructor class MessageQueue { private final LinkedList list = new LinkedList<>(); private final int capacity; /** * 获取消息 */ public Message take() { synchronized (list) { while (list.isEmpty()) { try { //log.info("队列为空等待发送消息"); list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.notifyAll(); return list.pop(); } } /** * 存放消息 */ public void put(Message message) { synchronized (list) { //检查队列是否已满 while (list.size() == capacity) { try { //log.info("队列已满等待接受消息"); list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.add(message); list.notifyAll(); } } } @Getter @AllArgsConstructor @ToString final class Message { private final int id; private final Object value; } ``` 某次运行结果: ```java [INFO ] 19:28:07 [生产者9] -----> 生产者9 开始发送消息 [INFO ] 19:28:07 [生产者9] -----> 生产者9----->消息发送成功<----- [INFO ] 19:28:07 [生产者3] -----> 生产者3 开始发送消息 [INFO ] 19:28:07 [生产者3] -----> 生产者3----->消息发送成功<----- [INFO ] 19:28:07 [生产者1] -----> 生产者1 开始发送消息 [INFO ] 19:28:07 [生产者1] -----> 生产者1----->消息发送成功<----- [INFO ] 19:28:07 [生产者2] -----> 生产者2 开始发送消息 [INFO ] 19:28:07 [生产者4] -----> 生产者4 开始发送消息 [INFO ] 19:28:07 [生产者10] -----> 生产者10 开始发送消息 [INFO ] 19:28:07 [生产者8] -----> 生产者8 开始发送消息 [INFO ] 19:28:07 [生产者7] -----> 生产者7 开始发送消息 [INFO ] 19:28:07 [生产者6] -----> 生产者6 开始发送消息 [INFO ] 19:28:07 [生产者5] -----> 生产者5 开始发送消息 [INFO ] 19:28:08 [生产者2] -----> 生产者2----->消息发送成功<----- [INFO ] 19:28:08 [消费者] -----> 接受到消息Message(id=9, value=ou hahae yolove you hahau hlove you ) [INFO ] 19:28:09 [生产者5] -----> 生产者5----->消息发送成功<----- [INFO ] 19:28:09 [消费者] -----> 接受到消息Message(id=3, value=love you hah) [INFO ] 19:28:10 [生产者4] -----> 生产者4----->消息发送成功<----- [INFO ] 19:28:10 [消费者] -----> 接受到消息Message(id=1, value=ovlo) [INFO ] 19:28:11 [消费者] -----> 接受到消息Message(id=2, value=vlove yo) [INFO ] 19:28:11 [生产者6] -----> 生产者6----->消息发送成功<----- [INFO ] 19:28:12 [生产者10] -----> 生产者10----->消息发送成功<----- [INFO ] 19:28:12 [消费者] -----> 接受到消息Message(id=5, value=ve you hahae yolove ) [INFO ] 19:28:13 [生产者7] -----> 生产者7----->消息发送成功<----- [INFO ] 19:28:13 [消费者] -----> 接受到消息Message(id=4, value=ove you hahae yo) [INFO ] 19:28:14 [生产者8] -----> 生产者8----->消息发送成功<----- [INFO ] 19:28:14 [消费者] -----> 接受到消息Message(id=6, value=e you hahae yolove you h) [INFO ] 19:28:15 [消费者] -----> 接受到消息Message(id=10, value=u hahae yolove you hahau hlove you hahaa) [INFO ] 19:28:16 [消费者] -----> 接受到消息Message(id=7, value= you hahae yolove you hahau ) [INFO ] 19:28:17 [消费者] -----> 接受到消息Message(id=8, value=you hahae yolove you hahau hlove) ``` ## Park & Unpark ### 基本使用 它们是 LockSupport 类中的方法 ```java // 暂停当前线程 LockSupport.park(); // 恢复某个线程的运行 LockSupport.unpark(暂停线程对象) ``` ### 特点 与 Object 的 wait & notify 相比 - wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必 - park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】 - park & unpark 可以先 unpark,而 wait & notify 不能先 notify ### 原理之 park & unpark 每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex 打个比喻 - 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中 的备用干粮(0 为耗尽,1 为充足) - 调用 park 就是要看需不需要停下来歇息 - 如果备用干粮耗尽,那么钻进帐篷歇息 - 如果备用干粮充足,那么不需停留,继续前进 - 调用 unpark,就好比令干粮充足 - 如果这时线程还在帐篷,就唤醒让他继续前进 - 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进 - 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮 ![image-20210422193132395](E:\MyProjects\concurrent-programing\README.assets\image-20210422193132395.png) 1. 当前线程调用 Unsafe.park() 方法 2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁 3. 线程进入 _cond 条件变量阻塞 4. 设置 _counter = 0 ![image-20210422193212012](E:\MyProjects\concurrent-programing\README.assets\image-20210422193212012.png) 1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1 2. 唤醒 _cond 条件变量中的 Thread_0 3. Thread_0 恢复运行 4. 设置 _counter 为 0 ![image-20210422193230757](E:\MyProjects\concurrent-programing\README.assets\image-20210422193230757.png) 1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1 2. 当前线程调用 Unsafe.park() 方法 3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行 4. 设置 _counter 为 0 ## 重新理解线程状态转换 ![image-20210422193319229](E:\MyProjects\concurrent-programing\README.assets\image-20210422193319229.png) 假设有线程 Thread t ### 情况 1 NEW --> RUNNABLE - 当调用 t.start() 方法时,由 NEW --> RUNNABLE ### 情况 2 RUNNABLE <--> WAITING t 线程用 synchronized(obj) 获取了对象锁后 - 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING - 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时 - 竞争锁成功,t 线程从 WAITING --> RUNNABLE - 竞争锁失败,t 线程从 WAITING --> BLOCKED ### 情况 3 RUNNABLE <--> WAITING - 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING - 注意是当前线程在t 线程对象的监视器上等待 - t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE ### 情况 4 RUNNABLE <--> WAITING - 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING - 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE ### 情况 5 RUNNABLE <--> TIMED_WAITING t 线程用 synchronized(obj) 获取了对象锁后 - 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING - t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时 - 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE - 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED ### 情况 6 RUNNABLE <--> TIMED_WAITING - 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING - 注意是当前线程在t 线程对象的监视器上等待 - 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE ### 情况 7 RUNNABLE <--> TIMED_WAITING - 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING - 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE ### 情况 8 RUNNABLE <--> TIMED_WAITING - 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE --> TIMED_WAITING - 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE ### 情况 9 RUNNABLE <--> BLOCKED - t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED - 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争 成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED ### 情况 10 RUNNABLE <--> TERMINATED 当前线程所有代码运行完毕,进入TERMINATED ## 多把锁 ### 多把不相干的锁 一间大屋子有两个功能:睡觉、学习,互不相干。 现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低 解决方法是准备多个房间(多个对象锁) 将锁的粒度细分 - 好处,是可以增强并发度 - 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁 ## 活跃性 ### 死锁 有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁 `t1 线程` 获得 `A对象` 锁,接下来想获取 `B对象`的锁` t2 线程 `获得 `B对象` 锁,接下来想获取 `A对象`的锁 例: ```java public static void main(String[] args) { Object A = new Object(); Object B = new Object(); Thread t1 = new Thread(() -> { synchronized (A) { log.debug("lock A"); sleep(1); synchronized (B) { log.debug("lock B"); log.debug("操作..."); } } }, "t1"); Thread t2 = new Thread(() -> { synchronized (B) { log.debug("lock B"); sleep(0.5); synchronized (A) { log.debug("lock A"); log.debug("操作..."); } } }, "t2"); t1.start(); t2.start(); } ``` 结果 12:22:06.962 [t2] c.TestDeadLock - lock B 12:22:06.962 [t1] c.TestDeadLock - lock A ### 定位死锁 - 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁: - 避免死锁要注意加锁顺序 - 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查 ### 哲学家就餐问题 ![image-20210422194720361](E:\MyProjects\concurrent-programing\README.assets\image-20210422194720361.png) 有五位哲学家,围坐在圆桌旁。 - 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。 - 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。 - 如果筷子被身边的人拿着,自己就得等待 ```java public class PhilosopherDinner { public static void main(String[] args) { Chopstick c1 = new Chopstick("1"); Chopstick c2 = new Chopstick("2"); Chopstick c3 = new Chopstick("3"); Chopstick c4 = new Chopstick("4"); Chopstick c5 = new Chopstick("5"); new Philosopher("苏格拉底", c1, c2).start(); new Philosopher("柏拉图", c2, c3).start(); new Philosopher("亚里士多德", c3, c4).start(); new Philosopher("赫拉克利特", c4, c5).start(); new Philosopher("阿基米德", c5, c1).start(); } } @Slf4j class Philosopher extends Thread { Chopstick left; Chopstick right; public Philosopher(String name, Chopstick left, Chopstick right) { super(name); this.left = left; this.right = right; } private void eat() { log.debug("eating..."); Sleeper.sleep(1); } @Override public void run() { while (true) { // 获得左手筷子 synchronized (left) { // 获得右手筷子 synchronized (right) { // 吃饭 eat(); } // 放下右手筷子 } // 放下左手筷子 } } } class Chopstick { String name; public Chopstick(String name) { this.name = name; } @Override public String toString() { return "筷子{" + name + '}'; } } ``` 执行不多会,就执行不下去了 ```java [DEBUG] 19:49:23 [苏格拉底] -----> eating... [DEBUG] 19:49:24 [阿基米德] -----> eating... [DEBUG] 19:49:25 [赫拉克利特] -----> eating... [DEBUG] 19:49:26 [亚里士多德] -----> eating... [DEBUG] 19:49:27 [柏拉图] -----> eating... // 卡在这里, 不向下运行,这时可用jconsole来发现死锁 ``` 这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情 况 ### 活锁 活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如 ```java @Slf4j public class AliveLock { static volatile int count = 10; static final Object lock = new Object(); public static void main(String[] args) { new Thread(() -> { // 期望减到 0 退出循环 while (count > 0) { sleep(0.2); count--; log.debug("count: {}", count); } }, "t1").start(); new Thread(() -> { // 期望超过 20 退出循环 while (count < 20) { sleep(0.2); count++; log.debug("count: {}", count); } }, "t2").start(); } } ``` ### 饥饿 很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题 下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题 ![image-20210422195245194](E:\MyProjects\concurrent-programing\README.assets\image-20210422195245194.png) 顺序加锁的解决方案 ![image-20210422195256787](E:\MyProjects\concurrent-programing\README.assets\image-20210422195256787.png) ## ReentrantLock 相对于 synchronized 它具备如下特点 - 可中断 - 可以设置超时时间 - 可以设置为公平锁 - 支持多个条件变量 与 synchronized 一样,都支持可重入 ### 基本语法 ```java // 获取锁 reentrantLock.lock(); try { // 临界区 } finally { // 释放锁 reentrantLock.unlock(); } ``` ### 可重入 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住 ```java private static final ReentrantLock lock=new ReentrantLock(); public static void main(String[] args){ lock.lock(); try{ m1(); }finally { lock.unlock(); } } public static void m1(){ lock.lock(); try{ m2(); }finally { lock.unlock(); } } public static void m2(){ lock.lock(); try{ log.info("进入m2"); }finally { lock.unlock(); } } ``` ### 可打断 ```java private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { Thread t1 = new Thread(() -> { try { log.info("尝试获取锁"); lock.lockInterruptibly(); log.info("获取锁"); while (true) { sleep(1); } } catch (InterruptedException e) { log.info("被打断"); e.printStackTrace(); } catch (Exception e) { if (e.getCause() instanceof InterruptedException) { log.info("被打断"); } e.printStackTrace(); } finally { lock.unlock(); } }); //lock.lock(); t1.start(); sleep(1); t1.interrupt(); } ``` > 注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断 ### 锁超时 ```java private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { Thread t1 = new Thread(() -> { log.info("尝试获取锁"); try { if (lock.tryLock(2, TimeUnit.SECONDS)){ try { log.info("获取到锁"); } finally { lock.unlock(); } }else { log.info("获取不到锁"); } } catch (InterruptedException e) { e.printStackTrace(); } }); lock.lock(); t1.start(); sleep(1); //t1.interrupt(); } ``` ### 使用 tryLock 解决哲学家就餐问题 见代码实现 ### 公平锁 ReentrantLock 默认是不公平的 ```java ReentrantLock lock = new ReentrantLock(false); lock.lock(); for (int i = 0; i < 500; i++) { new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + " running..."); } finally { lock.unlock(); } }, "t" + i).start(); } // 1s 之后去争抢锁 Thread.sleep(1000); new Thread(() -> { System.out.println(Thread.currentThread().getName() + " start..."); lock.lock(); try { System.out.println(Thread.currentThread().getName() + " running..."); } finally { lock.unlock(); } }, "强行插入").start(); lock.unlock(); ``` 强行插入,有机会在中间输出 ```java //注意:该实验不一定总能复现 t39 running... t40 running... t41 running... t42 running... t43 running... 强行插入 start... 强行插入 running... t44 running... t45 running... t46 running... t47 running... t49 running... ``` 改为公平锁后 ```java ReentrantLock lock = new ReentrantLock(true); ``` 强行插入,总是在最后输出 ```java t465 running... t464 running... t477 running... t442 running... t468 running... t493 running... t482 running... t485 running... t481 running... 强行插入 running... ``` 公平锁一般没有必要,会降低并发度,后面分析原理时会讲解 ### 条件变量 synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待 ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比 - synchronized 是那些不满足条件的线程都在一间休息室等消息 - 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤 醒 使用要点: - await 前需要获得锁 - await 执行后,会释放锁,进入 conditionObject 等待 - await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁 - 竞争 lock 锁成功后,从 await 后继续执行 ```java //创建新的条件变量 final Condition condition1 = lock.newCondition(); Runnable runnable = () -> { lock.lock(); try { //进入休息室等待 log.info("准备等待"); condition1.await(); log.info("被唤醒"); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } }; Thread thread = new Thread(runnable); thread.start(); Sleeper.sleep(5); lock.lock(); condition1.signal(); lock.unlock(); ``` ## 同步模式之顺序控制 ### 固定运行顺序 比如,必须先 2 后 1 打印 #### wait notify 版 ```java private static final Object LOCK=new Object(); private static boolean t2Run=false; public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (LOCK){ while (!t2Run){ try { LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.info("1"); } }, "t1"); Thread t2 = new Thread(() -> { synchronized (LOCK){ log.info("2"); t2Run=true; LOCK.notifyAll(); } }, "t2"); t1.start(); t2.start(); } ``` #### Park Unpark 版 可以看到,实现上很麻烦: - 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该 wait - 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决 此问题 - 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个 可以使用 LockSupport 类的 park 和 unpark 来简化上面的题目: ```java public static void main(String[] args) { Thread t1 = new Thread(() -> { LockSupport.park(true); log.info("1"); }, "t1"); Thread t2 = new Thread(() -> { log.info("2"); LockSupport.unpark(t1); }, "t2"); t1.start(); t2.start(); } ``` park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓。并且是以线程为单位进行『暂停』和『恢复』, 不需要『同步对象』和『运行标记』 ### 交替输出 线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现 #### wait notify 版 ```java private static final Object LOCK = new Object(); private static int turn = 0; public static void main(String[] args) { Thread t1 = new Thread(() -> print("A", 0), "T1"); Thread t2 = new Thread(() -> print("B", 1), "T2"); Thread t3 = new Thread(() -> print("C", 2), "T3"); t1.start(); t2.start(); t3.start(); } private static void print(String str, int turned) { for (int k = 0; k < 3; k++) { synchronized (LOCK) { while (turn % 3 != turned) { try { LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.info(str); turn++; LOCK.notifyAll(); } } } ``` #### Park Unpark 版 详细见代码-->com.freedy.concurrent.Monitors.patterns.SequenceControlAlternatelyParkUnpark #### Lock 条件变量版 详细见代码-->com.freedy.concurrent.Monitors.patterns.SequenceControlAlternatelyReentrant ## 本章小结 本章我们需要重点掌握的是 - 分析多线程访问共享资源时,哪些代码片段属于临界区 - 使用 synchronized 互斥解决临界区的线程安全问题 - 掌握 synchronized 锁对象语法 - 掌握 synchronzied 加载成员方法和静态方法语法 - 掌握 wait/notify 同步方法 - 使用 lock 互斥解决临界区的线程安全问题 - 掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量 - 学会分析变量的线程安全性、掌握常见线程安全类的使用 - 了解线程活跃性问题:死锁、活锁、饥饿 - 应用方面 - 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果 - 同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果 - 原理方面 - monitor、synchronized 、wait/notify 原理 - synchronized 进阶原理 - park & unpark 原理 - 模式方面 - 同步模式之保护性暂停 - 异步模式之生产者消费者 - 同步模式之顺序控制