1.概述

2. 线程与进程

2.1 进程与进程

  • 进程就可以视为程序的一个实例,大部分程序都可以运行多个实例进程(例如记事本,浏览器等),部分只可以运行一个实例进程(例如360安全卫士)
  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作 为线程的容器(这里感觉要学了计算机组成原理之后会更有感觉吧!)

二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集. 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂 同一台计算机的进程通信称为 IPC(Inter-process communication) 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP, 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

2.2 并行与并发

并发

在单核 cpu 下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。一般会将这种线程轮流使用 CPU 的做法称为并发(concurrent)

并行

多核 cpu下,每个核(core) 都可以调度运行线程,这时候线程可以是并行的,不同的线程同时使用不同的cpu在执行。

同步和异步的概念

  • 以调用方的角度讲,如果需要等待结果返回才能继续运行的话就是同步,如果不需要等待就是异步
  • 多线程可以使方法的执行变成异步的,比如说读取磁盘文件时,假设读取操作花费了5秒,如果没有线程的调度机制,这么cpu只能等5秒,啥都不能做。


3. java线程

3.1 创建和运行线程

方法一,直接使用 Thread

  1. // 构造方法的参数是给线程指定名字,,推荐给线程起个名字
  2. Thread t1 = new Thread("t1") {
  3. @Override
  4. // run 方法内实现了要执行的任务
  5. public void run() {
  6. log.debug("hello");
  7. }
  8. };
  9. t1.start();

方法二,使用 Runnable 配合 Thread (推荐)

  1. // 创建任务对象
  2. Runnable task2 = new Runnable() {
  3. @Override
  4. public void run() {
  5. log.debug("hello");
  6. }
  7. };
  8. // 参数1 是任务对象; 参数2 是线程名字,推荐给线程起个名字
  9. Thread t2 = new Thread(task2, "t2");
  10. t2.start();
  11. // 使用lambda表达式简化
  12. Runnable r1 = () -> log.debug("hello"););
  13. new Thread(r1).start();
  14. // 或者
  15. new Thread(() -> {
  16. log.debug("hello");
  17. }).start();

方法三,FutureTask 配合 Callable

FutureTask 是对 Runnable 的一个扩展, 由于Runnable的run方法返回值是void, 不能很好地在线程之间传递数据, 因此诞生了FutureTask. FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

3.2 Thread的常见方法

JAVA 并发编程 - 图1

3.2.1 start 与 run

直接调用 run() 是在主线程中执行了 run(),没有启动新的线程 使用 start() 是启动新的线程,通过新的线程间接执行 run()方法 中的代码

3.2.2 sleep 与 yield

sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出 InterruptedException异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
  3. 睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)
  4. 建议用 TimeUnit 的 sleep() 代替 Thread 的 sleep()来获得更好的可读性

    yield

  5. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程

  6. 具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法,但是也没有用)

    小结

    yield使cpu调用其它线程,但是cpu可能会再分配时间片给该线程;而sleep需要等过了休眠时间之后才有可能被分配cpu时间片

3.2.4 join

在主线程中调用t1.join,则主线程会等待t1线程执行完之后再继续执行 Test10.java

  1. static int r = 0;
  2. public static void test1() throws InterruptedException {
  3. log.debug("开始");
  4. Thread t1 = new Thread(() -> {
  5. log.debug("开始");
  6. sleep(1);
  7. log.debug("结束");
  8. r = 10;
  9. },"t1");
  10. t1.start();
  11. t1.join();
  12. log.debug("结果为:{}", r); // 结果为:10
  13. log.debug("结束");
  14. }

小结

  • 调用 join() 会使等待线程状态切换到WAITING状态
  • join()方法传入一个等待时间的参数, 当等待超时时, 则不会继续等待.

3.2.5 sleep,yiled,wait,join 对比

关于join的原理和这几个方法的对比:看这里

  • sleep 不释放锁、释放cpu
  • wait 释放锁、释放cpu
  • join 释放锁、抢占cpu
  • yiled 不释放锁、释放cpu

补充:

  1. sleep,join,yield,interrupted是Thread类中的方法
  2. wait/notify是object中的方法

3.2.6 interrupt 方法详解

情况一、打断 sleep,wait,join 的线程

sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态, 被打断时效果一样, 以sleep为例:

  1. public static void main(String[] args) throws InterruptedException {
  2. Thread t1 = new Thread(() -> {
  3. log.debug("线程任务执行");
  4. try {
  5. Thread.sleep(10000); // wait, join
  6. } catch (InterruptedException e) {
  7. log.debug("被打断");
  8. //e.printStackTrace();
  9. }
  10. });
  11. t1.start();
  12. Thread.sleep(500);
  13. t1.interrupt();
  14. log.debug("t1是否被打断?{}",t1.isInterrupted());
  15. }
  16. // 输出:
  17. "t1是否被打断? false"

打断 sleep 的线程, 被打断的进程会报InterruptedException异常, 并且会清空中断状态,即中断状态会被清除。那么线程是否被中断过可以通过异常来判断。

情况二、打断正常运行的线程

打断正常运行的线程, 线程并不会暂停, 被打断线程的中断状态会置为true, 可以判断Thread.currentThread().isInterrupted();的值来手动停止线程.

  1. public static void main(String[] args) throws InterruptedException {
  2. Thread t1 = new Thread(() -> {
  3. while(true) {
  4. boolean interrupted = Thread.currentThread().isInterrupted();
  5. if(interrupted) {
  6. log.debug("被打断了, 退出循环");
  7. break;
  8. }
  9. }
  10. }, "t1");
  11. t1.start();
  12. Thread.sleep(1000);
  13. log.debug("interrupt");
  14. t1.interrupt();
  15. }

终止模式之两阶段终止模式

Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会.

  1. @Slf4j(topic = "c.TwoPhaseTermination")
  2. class TwoPhaseTermination{
  3. private Thread thread;
  4. public TwoPhaseTermination(){
  5. thread = new Thread(()->{
  6. log.debug("开始");
  7. while (true) {
  8. if (Thread.currentThread().isInterrupted()){
  9. log.debug("料理后事");
  10. break;
  11. }
  12. try {
  13. Thread.sleep(1000);
  14. log.debug("执行监控...");
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. // 重设打断标记
  18. Thread.currentThread().interrupt();
  19. }
  20. }
  21. }, "t1");
  22. }
  23. public void start(){
  24. thread.start();
  25. }
  26. public void stop(){
  27. thread.interrupt();
  28. }
  29. }

情况三、打断park线程

  1. public static void main(String[] args) throws InterruptedException {
  2. Thread t1 = new Thread(()->{
  3. log.debug("park...");
  4. LockSupport.park(); // 线程会停在这里, 状态为WAITING
  5. log.debug("unpack...");
  6. log.debug("打断标记:{}",Thread.currentThread().isInterrupted()); // 打断标记:true
  7. },"t1");
  8. t1.start();
  9. Thread.sleep(1000);
  10. log.debug("t1在park时的状态 : {}", t1.getState()); // t1在park时的状态 : WAITING
  11. t1.interrupt();
  12. }

3.3 守护线程

默认情况下,java进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。普通线程t1可以调用t1.setDeamon(true); 方法变成守护线程

垃圾回收器线程就是一种守护线程,Tomcat 中的 Acceptor 和 Poller 线程也都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求

3.4 线程的状态

3.4.1 五种状态

五种状态的划分主要是从操作系统的层面进行划分的
JAVA 并发编程 - 图2

  1. 初始状态,仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();,还未与操作系统线程关联
  2. 可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
  3. 运行状态,指线程获取了CPU时间片,正在运行
    1. 当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换
  4. 阻塞状态
    1. 如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
    2. 等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    3. 与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
  5. 终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态


3.4.3 六种状态

这是从 Java API 层面来描述的,我们主要研究的就是这种。

JAVA 并发编程 - 图3

  1. NEW 跟五种状态里的初始状态是一个意思
  2. RUNNABLE 是当调用了 start() 方法之后的状态,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【io阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  3. BLOCKEDWAITINGTIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述

4. 共享模型之管程

临界区的概念:一段代码内如果存在对共享资源的多线程读写操作,那么称这段代码为临界区
竞态条件的概念 : 多个线程在临界区执行,那么由于代码指令的执行不确定而导致的结果问题,称为竞态条件

为了避免临界区中的竞态条件发生,由多种手段可以达到

  • 阻塞式解决方案:synchronized ,Lock
  • 非阻塞式解决方案:原子变量

4.1 阻塞式解决方案之synchronized

使用synchronized来进行解决,即俗称的对象锁,它采用互斥的方式让同一时刻至多只有一个线程持有对象锁,其他线程如果想获取这个锁就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换.

  1. class Number{
  2. private static int count;
  3. // 静态方法锁住的是Number.class对象
  4. public static synchronized void a(){
  5. count++;
  6. System.out.println(count);
  7. }
  8. // 成员方法锁住的是this
  9. public synchronized void b(){
  10. count--;
  11. System.out.println(count);
  12. }
  13. }

4.2 常见线程安全类

  1. String
  2. Integer
  3. StringBuffer
  4. Random
  5. Vector
  6. Hashtable
  7. java.util.concurrent 包下的类

4.3 Monitor

Java 对象头

以 32 位虚拟机为例,普通对象的对象头结构如下,其中的Klass Word为指针,指向对应的Class对象;
JAVA 并发编程 - 图4
数组对象JAVA 并发编程 - 图5
其中 Mark Word 结构为JAVA 并发编程 - 图6

Monitor 原理(锁原理)

Monitor被翻译为监视器或者说管程
每个java对象都可以关联一个Monitor,如果使用synchronized给对象上锁(重量级),该对象头的Mark Word中就被设置为指向Monitor对象的指针

JAVA 并发编程 - 图7

  • 刚开始时Monitor中的Owner为null
  • 当Thread-2 执行synchronized(obj){}代码时就会将Monitor的所有者Owner 设置为 Thread-2,上锁成功,Monitor中同一时刻只能有一个Owner
  • 当Thread-2 占据锁时,如果线程Thread-3,Thread-4也来执行synchronized(obj){}代码,就会进入EntryList中变成BLOCKED状态
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析

synchronized 原理进阶

轻量级锁

轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用者是透明的,即语法仍然是synchronized,假设有两个方法同步块,利用同一个对象加锁

  1. static final Object obj = new Object();
  2. public static void method1() {
  3. synchronized( obj ) {
  4. // 同步块 A
  5. method2();
  6. }
  7. }
  8. public static void method2() {
  9. synchronized( obj ) {
  10. // 同步块 B
  11. }
  12. }
  1. 每次指向到synchronized代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的Mark Word和对象引用reference

JAVA 并发编程 - 图8

  1. 让锁记录中的Object reference指向对象,并且尝试用cas(compare and swap)替换Object对象的Mark Word ,将Mark Word 的值存入锁记录中

JAVA 并发编程 - 图9

  1. 如果cas替换成功,那么对象的对象头储存的就是锁记录的地址和状态01,如下所示

JAVA 并发编程 - 图10

  1. 如果cas失败,有两种情况
    1. 如果是其它线程已经持有了该Object的轻量级锁,那么表示有竞争,将进入锁膨胀阶段
    2. 如果是自己的线程已经执行了synchronized进行加锁,那么那么再添加一条 Lock Record 作为重入的计数

JAVA 并发编程 - 图11

  1. 当线程退出synchronized代码块的时候,如果获取的是取值为 null 的锁记录 ,表示有重入,这时重置锁记录,表示重入计数减一

JAVA 并发编程 - 图12

  1. 当线程退出synchronized代码块的时候,如果获取的锁记录取值不为 null,那么使用cas将Mark Word的值恢复给对象
    1. 成功则解锁成功
    2. 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

如果在尝试加轻量级锁的过程中,cas操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。

  1. 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
  2. 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    1. 即为对象申请Monitor锁,让Object指向重量级锁地址,然后自己进入Monitor 的EntryList 变成BLOCKED状态
    2. 当Thread-0 退出synchronized同步块时,使用cas将Mark Word的值恢复给对象头,失败,那么会进入重量级锁的解锁过程,即按照Monitor的地址找到Monitor对象,将Owner设置为null,唤醒EntryList 中的Thread-1线程

JAVA 并发编程 - 图13

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换就获得了锁
JAVA 并发编程 - 图14

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才有意义。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能


偏向锁

在轻量级的锁中,我们可以发现,如果同一个线程对同一个对象进行重入锁时,也需要执行CAS操作,这是有点耗时滴,那么java6开始引入了偏向锁的东东,只有第一次使用CAS时将对象的Mark Word头设置为入锁线程ID,之后这个入锁线程再进行重入锁时,发现线程ID是自己的,那么就不用再进行CAS了
**JAVA 并发编程 - 图15

偏向状态

JAVA 并发编程 - 图16
一个对象的创建过程

  1. 如果开启了偏向锁(默认是开启的),那么对象刚创建之后,Mark Word 最后三位的值101,并且这是它的Thread,epoch,age都是0,在加锁的时候进行设置这些的值.
  2. 偏向锁默认是延迟的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:-XX:BiasedLockingStartupDelay=0来禁用延迟
  3. 注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
  4. 实验Test18.java,加上虚拟机参数-XX:BiasedLockingStartupDelay=0进行测试

synchronized原理图

JAVA 并发编程 - 图17

4.4 Wait/Notify

Wait/Notify原理


JAVA 并发编程 - 图18

  1. 锁对象调用wait方法(obj.wait),就会使当前线程进入 WaitSet 中,变为 WAITING 状态。
  2. 处于BLOCKED和 WAITING 状态的线程都为阻塞状态,CPU 都不会分给他们时间片。但是有所区别:
    • BLOCKED 状态的线程是在竞争对象时,发现 Monitor 的 Owner 已经是别的线程了,此时就会进入 EntryList 中,并处于 BLOCKED 状态
    • WAITING 状态的线程是获得了对象的锁,但是自身因为某些原因需要进入阻塞状态时,锁对象调用了 wait 方法而进入了 WaitSet 中,处于 WAITING 状态
  3. BLOCKED 状态的线程会在锁被释放的时候被唤醒,但是处于 WAITING 状态的线程只有被锁对象调用了 notify 方法(obj.notify/obj.notifyAll),才会被唤醒。

注:只有当对象加锁以后,才能调用 wait 和 notify 方法

API介绍

下面的三个方法都是Object中的方法; 通过锁对象来调用

  • wait(): 让获得对象锁的线程到waitSet中一直等待
  • wait(long n) : 当该等待线程没有被notify, 等待时间到了之后, 也会自动唤醒
  • notify(): 让获得对象锁的线程, 使用锁对象调用notify去waitSet的等待线程中挑一个唤醒
  • notifyAll() : 让获得对象锁的线程, 使用锁对象调用notifyAll去唤醒waitSet中所有的等待线程

它们都是线程之间进行协作的手段, 都属于Object对象的方法, 必须获得此对象的锁, 才能调用这些方法

  1. @Slf4j(topic = "c.WaitNotifyTest")
  2. public class WaitNotifyTest {
  3. static final Object obj = new Object();
  4. public static void main(String[] args) throws Exception {
  5. new Thread(() -> {
  6. synchronized (obj) {
  7. log.debug("执行...");
  8. try {
  9. // 只有获得锁对象之后, 才能调用wait/notify
  10. obj.wait(); // 此时t1线程进入WaitSet等待
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. log.debug("其它代码...");
  15. }
  16. }, "t1").start();
  17. new Thread(() -> {
  18. synchronized (obj) {
  19. log.debug("执行...");
  20. try {
  21. obj.wait(); // 此时t2线程进入WaitSet等待
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. }
  25. log.debug("其它代码...");
  26. }
  27. }, "t2").start();
  28. // 让主线程等两秒在执行,为了`唤醒`,不睡的话,那两个线程还没进入waitSet,主线程就开始唤醒了
  29. Thread.sleep(1000);
  30. log.debug("唤醒waitSet中的线程!");
  31. // 只有获得锁对象之后, 才能调用wait/notify
  32. synchronized (obj) {
  33. // obj.notify(); // 唤醒waitset中的一个线程
  34. obj.notifyAll(); // 唤醒waitset中的全部等待线程
  35. }
  36. }
  37. }
  38. 13:01:36.176 guizy.WaitNotifyTest [t1] - 执行...
  39. 13:01:36.178 guizy.WaitNotifyTest [t2] - 执行...
  40. 13:01:37.175 guizy.WaitNotifyTest [main] - 唤醒waitSet中的线程!
  41. 13:01:37.175 guizy.WaitNotifyTest [t2] - 其它代码...
  42. 13:01:37.175 guizy.WaitNotifyTest [t1] - 其它代码...

Sleep(long n) 和 Wait(long n)的区别 (重点)

不同点

  1. Sleep是Thread类的静态方法,Wait是Object的方法,Object又是所有类的父类,所以所有类都有Wait方法。
  2. Sleep在阻塞的时候不会释放锁,而Wait在阻塞的时候会释放锁 (不释放锁的话, 其他线程就无法唤醒该线程了)
  3. Sleep方法不需要与synchronized一起使用,而Wait方法需要与synchronized一起使用(wait/notify等方法, 必须要使用对象锁来调用)

相同点

  • 阻塞状态都为TIMED_WAITING (限时等待)

优雅地使用 wait/notify

  1. synchronized (lock) {
  2. while(//不满足条件,一直等待,避免虚假唤醒) {
  3. lock.wait();
  4. }
  5. //满足条件后再运行
  6. }
  7. synchronized (lock) {
  8. //唤醒所有等待线程
  9. lock.notifyAll();
  10. }

例如:

  1. @Slf4j(topic = "guizy.WaitNotifyTest")
  2. public class Main {
  3. static final Object room = new Object();
  4. static boolean hasCigarette = false;
  5. static boolean hasTakeout = false;
  6. public static void main(String[] args) {
  7. new Thread(() -> {
  8. synchronized (room) {
  9. log.debug("有烟没?[{}]", hasCigarette);
  10. while (!hasCigarette) {
  11. log.debug("没烟,先歇会!");
  12. try {
  13. room.wait(); // 此时进入到waitset等待集合, 同时会释放锁
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. log.debug("有烟没?[{}]", hasCigarette);
  19. if (hasCigarette) {
  20. log.debug("可以开始干活了");
  21. }
  22. }
  23. }, "小南").start();
  24. new Thread(() -> {
  25. synchronized (room) {
  26. log.debug("外卖送到没?[{}]", hasTakeout);
  27. while (!hasTakeout) {
  28. log.debug("没外卖,先歇会!");
  29. try {
  30. room.wait();
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. log.debug("外卖送到没?[{}]", hasTakeout);
  36. if (hasTakeout) {
  37. log.debug("可以开始干活了");
  38. } else {
  39. log.debug("没干成活...");
  40. }
  41. }
  42. }, "小女").start();
  43. Sleeper.sleep(1);
  44. new Thread(() -> {
  45. synchronized (room) {
  46. hasTakeout = true;
  47. log.debug("外卖到了噢!");
  48. room.notifyAll();
  49. }
  50. }, "送外卖的").start();
  51. }
  52. }
  53. 11:19:25.275 guizy.WaitNotifyTest [小南] - 有烟没?[false]
  54. 11:19:25.282 guizy.WaitNotifyTest [小南] - 没烟,先歇会!
  55. 11:19:25.282 guizy.WaitNotifyTest [小女] - 外卖送到没?[false]
  56. 11:19:25.283 guizy.WaitNotifyTest [小女] - 没外卖,先歇会!
  57. 11:19:26.287 guizy.WaitNotifyTest [送外卖的] - 外卖到了噢!
  58. 11:19:26.287 guizy.WaitNotifyTest [小女] - 外卖送到没?[true]
  59. 11:19:26.287 guizy.WaitNotifyTest [小女] - 可以开始干活了
  60. 11:19:26.288 guizy.WaitNotifyTest [小南] - 没烟,先歇会!

4.5 park与unpark

两个都是LockSupport类的静态方法
park : 暂停当前线程, 线程进入WAIT状态
unpark : 恢复当前线程

一个现象 : 先调用unpark(), 再调用park(), 则线程不会暂停

4.6 线程状态转换

JAVA 并发编程 - 图19

  1. 情况一:NEW –> RUNNABLE
    当调用了 t.start() 方法时,由 NEW –> RUNNABLE
  2. 情况二: RUNNABLE <–> WAITING
    • 当调用了t 线程用 synchronized(obj) 获取了对象锁后,调用 obj.wait() 方法时,t 线程从 RUNNABLE –> WAITING
    • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时,会在 WaitSet 等待队列中出现锁竞争,非公平竞争
      • 竞争锁成功,t 线程从 WAITING –> RUNNABLE
      • 竞争锁失败,t 线程从 WAITING –> BLOCKED

**

  1. 情况三:RUNNABLE <–> WAITING

    • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE –> WAITING
      • 注意是当前线程在 t 线程对象的监视器上等待
    • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING –> RUNNABLE
  2. 情况四: RUNNABLE <–> WAITING

    • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE –> WAITING
    • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING –> RUNNABLE
  3. 情况五: 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
  1. 情况六:RUNNABLE <–> TIMED_WAITING
    • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE –> TIMED_WAITING, 注意是当前线程在 t 线程对象的监视器上等待
    • 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING –> RUNNABLE
  2. 情况七:RUNNABLE <–> TIMED_WAITING
    • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE –> TIMED_WAITING
    • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING –> RUNNABLE
  3. 情况八:RUNNABLE <–> TIMED_WAITING
    • 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE –> TIMED_WAITING
    • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE
  4. 情况九:RUNNABLE <–> BLOCKED
    • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE –> BLOCKED
    • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争 成功,从 BLOCKED –> RUNNABLE ,其它失败的线程仍然 BLOCKED
  5. 情况十: RUNNABLE <–> TERMINATED
    • 当前线程所有代码运行完毕,进入 TERMINATED

4.7 ReentrantLock

ReentrantLock是java.util.concurrent.locks中的一个可重入锁类。在高竞争条件下有更好的性能,且可以中断。深入剖析ReentrantLock的源码有助于我们了解线程调度,锁实现,中断,信号触发等底层机制,实现更好的并发程序。
和 synchronized 相比具有的的特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁 (先到先得)
  • 支持多个条件变量( 具有多个 WaitSet)
  • synchronized 发生异常会自动释放锁, 不会造成死锁,而ReentrantLock不会主动释放锁

基本语法

  1. // 获取ReentrantLock对象
  2. private ReentrantLock reentrantLock = new ReentrantLock();
  3. // 加锁
  4. reentrantLock.lock();
  5. try {
  6. // 需要执行的代码
  7. }finally {
  8. // 释放锁
  9. reentrantLock.unlock();
  10. }

可打断

使用lockInterruptibly()方法上锁时, 在未能未能获得锁而进入阻塞状态时,可被其他线程打断而放弃竞争锁

  1. private static ReentrantLock reentrantLock = new ReentrantLock();
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread t1 = new Thread(() -> {
  4. try {
  5. reentrantLock.lockInterruptibly();
  6. while (true) {
  7. Thread.sleep(100);
  8. System.out.println(Thread.currentThread().getName() + " : 我睡了");
  9. }
  10. } catch (InterruptedException e) {
  11. System.out.println(Thread.currentThread().getName() + " : 被打断");
  12. // 打断后的处理
  13. } finally {
  14. reentrantLock.unlock();
  15. }
  16. }, "t1");
  17. t1.start();
  18. Thread.sleep(1000);
  19. System.out.println("开始打断");
  20. t1.interrupt();
  21. }

注 : 已经获得了锁的进程也会被打断而放弃锁资源

锁超时

使用 lock.tryLock 方法会返回获取锁是否成功。如果成功则返回 true ,反之则返回 false 。
并且 tryLock 方法可以指定等待时间,参数为:tryLock(long timeout, TimeUnit unit), 其中 timeout 为最长等待时间,TimeUnit 为时间单位

  1. public static void main(String[] args) {
  2. ReentrantLock lock = new ReentrantLock();
  3. Thread t1 = new Thread(() -> {
  4. try {
  5. // 判断获取锁是否成功,最多等待1秒
  6. if(!lock.tryLock(1, TimeUnit.SECONDS)) {
  7. System.out.println("获取失败");
  8. // 获取失败,不再向下执行,直接返回
  9. return;
  10. }
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. // 被打断,不再向下执行,直接返回
  14. return;
  15. }
  16. System.out.println("得到了锁");
  17. // 释放锁
  18. lock.unlock();
  19. });
  20. lock.lock();
  21. try{
  22. t1.start();
  23. // 打断等待
  24. t1.interrupt();
  25. Thread.sleep(3000);
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. } finally {
  29. lock.unlock();
  30. }
  31. }

公平锁

  1. // 默认是不公平锁,需要在创建时指定为公平锁
  2. ReentrantLock lock = new ReentrantLock(true);

4.8 ReadWriteLock

image.png

Lock readLock(); Lock writeLock();

这是个接口,ReentrantReadWriteLock是这个接口的实现类

4.9 线程通信

image.png

五、共享模型之内存

1、Java 内存模型(JMM)

JMM 即 Java Memory Model,它定义了主存(共享内存)、工作内存(线程私有)抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。
JMM 体现在以下几个方面

原子性 - 保证指令不会受到线程上下文切换的影响
可见性 - 保证指令不会受 cpu 缓存的影响
有序性 - 保证指令不会受 cpu 指令并行优化的影响

2、可见性

1)退不出的循环

首先看一段代码:

  1. public static boolean run = true;
  2. public static void main(String[] args) {
  3. Thread t1 = new Thread(() -> {
  4. while(run) {
  5. }
  6. }, "t1");
  7. t1.start();
  8. try {
  9. Thread.sleep(1000);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. log.info("t1 Stop");
  14. run = false;
  15. }

JAVA 并发编程 - 图22

解决方法

  • 使用 volatile (易变关键字)
  • 它可以用来修饰成员变量和静态成员变量(放在主存中的变量),他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
  1. public static volatile boolean run = true; // 保证内存的可见性