微体系-多端全栈项目实战:商业级代驾全流程落地无密

91 阅读17分钟

复制uri下崽ZY超清: https://www.97yrbl.com/t-1463.html

download:百度网盘

前言

在 java 中,线程由 Thread 类表示,用户创建线程的唯一方式是创建 Thread 类的一个实例,每一个线程都和这样的一个实例关联。在相应的 Thread 实例上调用 start() 方法将启动一个线程。

如果没有正确使用同步,线程表现出来的现象将会是令人疑惑的、违反直觉的。这个章节将描述多线程编程的语义问题,包括一系列的规则,这些规则定义了在多线程环境中线程对共享内存中值的修改是否对其他线程立即可见

java编程语言内存模型定义了统一的内存模型用于屏蔽不同的硬件架构,在没有歧义的情况下,下面将用内存模型表示这个概念。

这些语义没有规定多线程的程序在 JVM 的实现上应该怎么执行,而是限定了一系列规则,由 JVM 厂商来满足这些规则,即不管 JVM 的执行策略是什么,表现出来的行为必须是可被接受的。

操作系统有自己的内存模型,C/C++ 这些语言直接使用的就是操作系统的内存模型,而 Java 为了屏蔽各个系统的差异,定义了自己的统一的内存模型。简单说,Java 开发者不再关心每个 CPU 核心有自己的内存,然后共享主内存。而是把关注点转移到:每个线程都有自己的工作内存,所有线程共享主内存。

17.1 同步(synchronization)

Java 提供了多种线程之间通信的机制,其中最基本的就是使用同步 (synchronization),其使用监视器 (monitor) 来实现。java中的每个对象都关联了一个监视器,线程可以对其进行加锁和解锁操作。

在同一时间,只有一个线程可以拿到对象上的监视器锁。如果其他线程在锁被占用期间试图去获取锁,那么将会被阻塞直到成功获取到锁。同时,监视器锁可以重入,也就是说如果线程 t 拿到了锁,那么线程 t 可以在解锁之前重复获取锁;每次解锁操作会反转一次加锁产生的效果。

synchronized 有以下两种使用方式:

  • synchronized 代码块。synchronized(object)在对某个对象上执行加锁时,会尝试在该对象的监视器上进行加锁操作,只有成功获取锁之后,线程才会继续往下执行。线程获取到了监视器锁后,将继续执行synchronized 代码块中的代码,如果代码块执行完成,或者抛出了异常,线程将会自动对该对象上的监视器执行解锁操作。
  • synchronized 作用于方法,称为同步方法。同步方法被调用时,会自动执行加锁操作,只有加锁成功,方法体才会得到执行。如果被 synchronized 修饰的方法是实例方法,那么这个实例的监视器会被锁定。如果是 static 方法,线程会锁住相应的 Class 对象的监视器。方法体执行完成或者异常退出后,会自动执行解锁操作。

Java语言规范既不要求阻止死锁的发生,也不要求检测到死锁的发生。如果线程要在多个对象上执行加锁操作,那么就应该使用传统的方法来避免死锁的发生,如果有必要的话,需要创建更高层次的不会产生死锁的加锁原语。

java 还提供了其他的一些同步机制,比如对 volatile 变量的读写、使用 java.util.concurrent 包中的同步工具类等。

同步这一节说了 Java 并发编程中最基础的 synchronized 这个关键字,大家一定要理解 synchronize 的锁是什么,它的锁是基于 Java 对象的监视器 monitor,所以任何对象都可以用来做锁。有兴趣的读者可以去了解相关知识,包括偏向锁、轻量级锁、重量级锁等。

小知识点:对 Class 对象加锁、对对象加锁,它们之间不构成同步。synchronized 作用于静态方法时是对 Class 对象加锁,作用于实例方法时是对实例加锁。

面试中经常会问到一个类中的两个 synchronized static 方法之间是否构成同步?构成同步。

17.2 等待集合 和 唤醒(Wait Sets and Notification)

每个 java 对象,都关联了一个监视器,也关联了一个等待集合。等待集合是一个线程集合。

当对象被创建出来时,它的等待集合是空的,对于向等待集合中添加或者移除线程的操作都是原子的,以下几个操作可以操纵这个等待集合:Object.wait, Object.notify, Object.notifyAll。

等待集合也可能受到线程的中断状态的影响,也受到线程中处理中断的方法的影响。另外,sleep 方法和 join 方法可以感知到线程的 wait 和 notify。

这里概括得比较简略,没看懂的读者没关系,继续往下看就是了。

这节要讲Java线程的相关知识,主要包括:

  • Thread 中的 sleep、join、interrupt
  • 继承自 Object 的 wait、notify、notifyAll
  • 还有 Java 的中断,这个概念也很重要

17.2.1 等待 (Wait)

等待操作由以下几个方法引发:wait(),wait(long millisecs),wait(long millisecs, int nanosecs)。在后面两个重载方法中,如果参数为 0,即 wait(0)、wait(0, 0) 和 wait() 是等效的。

如果调用 wait 方法时没有抛出 InterruptedException 异常,则表示正常返回。

前方高能,请读者保持高度精神集中。

我们在线程 t 中对对象 m 调用 m.wait() 方法,n 代表加锁编号,同时还没有相匹配的解锁操作,则下面的其中之一会发生:

  • 如果 n 等于 0(如线程 t 没有持有对象 m 的锁),那么会抛出 IllegalMonitorStateException 异常。

注意,如果没有获取到监视器锁,wait 方法是会抛异常的,而且注意这个异常是IllegalMonitorStateException异常。这是重要知识点,要考。

  • 如果线程 t 调用的是 m.wait(millisecs) 或m.wait(millisecs, nanosecs),形参millisecs 不能为负数,nanosecs 取值应为 [0, 999999],否则会抛出IllegalArgumentException 异常。
  • 如果线程 t 被中断,此时中断状态为 true,则 wait 方法将抛出 InterruptedException 异常,并将中断状态设置为 false。

中断,如果读者不了解这个概念,可以参考我在 AQS(二) 中的介绍,这是非常重要的知识。

  • 否则,下面的操作会顺序发生:

注意:到这里的时候,wait 参数是正常的,同时 t 没有被中断,并且线程 t 已经拿到了 m 的监视器锁。

1.线程 t 会加入到对象 m 的等待集合中,执行 加锁编号 n 对应的解锁操作

这里也非常关键,前面说了,wait 方法的调用必须是线程获取到了对象的监视器锁,而到这里会进行解锁操作。切记切记。。。

 public Object object = new Object();
 void thread1() {
     synchronized (object) { // 获取监视器锁
         try {
             object.wait(); // 这里会解锁,这里会解锁,这里会解锁
             // 顺便提一下,只是解了object上的监视器锁,如果这个线程还持有其他对象的监视器锁,这个时候是不会释放的。
         } catch (InterruptedException e) {
             // do somethings
         }
     }
 }

2.线程 t 不会执行任何进一步的指令,直到它从 m 的等待集合中移出(也就是等待唤醒)。在发生以下操作的时候,线程 t 会从 m 的等待集合中移出,然后在之后的某个时间点恢复,并继续执行之后的指令。

并不是说线程移出等待队列就马上往下执行,这个线程还需要重新获取锁才行,这里也很关键,请往后看17.2.4中我写的两个简单的例子。

  • 在 m上执行了 notify 操作,而且线程 t 被选中从等待集合中移除。
  • 在 m 上执行了 notifyAll 操作,那么线程 t 会从等待集合中移除。
  • 线程 t 发生了 interrupt 操作。
  • 如果线程 t 是调用 wait(millisecs) 或者 wait(millisecs, nanosecs) 方法进入等待集合的,那么过了millisecs 毫秒或者 (millisecs*1000000+nanosecs) 纳秒后,线程 t也会从等待集合中移出。
  • JVM 的“假唤醒”,虽然这是不鼓励的,但是这种操作是被允许的,这样 JVM 能实现将线程从等待集合中移出,而不必等待具体的移出指令。

注意,良好的 Java 编码习惯是,只在循环中使用 wait 方法,这个循环等待某些条件来退出循环。

个人理解wait方法是这么用的:

 synchronized(m) {
     while(!canExit) {
       m.wait(10); // 等待10ms; 当然中断也是常用的
       canExit = something();  // 判断是否可以退出循环
     }
 }
 // 2 个知识点:
 // 1. 必须先获取到对象上的监视器锁
 // 2. wait 有可能被假唤醒

每个线程在一系列 可能导致它从等待集合中移出的事件 中必须决定一个顺序。这个顺序不必要和其他顺序一致,但是线程必须表现为它是按照那个顺序发生的。

例如,线程 t 现在在 m 的等待集合中,不管是线程 t 中断还是 m 的 notify 方法被调用,这些操作事件肯定存在一个顺序。如果线程 t 的中断先发生,那么 t 会因为 InterruptedException 异常而从 wait 方法中返回,同时 m 的等待集合中的其他线程(如果有的话)会收到这个通知。如果 m 的 notify 先发生,那么 t 会正常从 wait 方法返回,且不会改变中断状态。

我们考虑这个场景:

线程 1 和线程 2 此时都 wait 了,线程 3 调用了 :

synchronized (object) {
    thread1.interrupt(); //1
    object.notify();  //2
}

本来我以为上面的情况 线程1 一定是抛出 InterruptedException,线程2 是正常返回的。感谢评论留言的 xupeng.zhang,我的这个想法是错误的,完全有可能线程1正常返回(即使其中断状态是true),线程2 一直 wait。

3.线程 t 执行编号为 n 的加锁操作

回去看 2 说了什么,线程刚刚从等待集合中移出,然后这里需要重新获取监视器锁才能继续往下执行。

4.如果线程 t 在 2 的时候由于中断而从 m 的等待集合中移出,那么它的中断状态会重置为 false,同时 wait 方法会抛出 InterruptedException 异常。

这一节主要在讲线程进出等待集合的各种情况,同时,最好要知道中断是怎么用的,中断的状态重置发生于什么时候。

这里的 1,2,3,4 的发生顺序非常关键,大家可以仔细再看看是不是完全理解了,之后的几个小节还会更具体地阐述这个,参考代码请看 17.2.4 小节我写的简单的例子。

17.2.2 通知(Notification)

通知操作发生于调用 notify 和 notifyAll 方法。

我们在线程 t 中对对象 m 调用 m.notify() 或 m.notifyAll() 方法,n 代表加锁编号,同时对应的解锁操作没有执行,则下面的其中之一会发生:

  • 如果 n 等于 0,抛出 IllegalMonitorStateException 异常,因为线程 t 还没有获取到对象 m 上的锁。

这一点很关键,只有获取到了对象上的监视器锁的线程才可以正常调用 notify,前面我们也说过,调用 wait 方法的时候也要先获取锁

  • 如果 n 大于 0,而且这是一个 notify 操作,如果 m 的等待集合不为空,那么等待集合中的线程 u 被选中从等待集合中移出。

对于哪个线程会被选中而被移出,虚拟机没有提供任何保证,从等待集合中将线程 u 移出,可以让线程 u 得以恢复。注意,恢复之后的线程 u 如果对 m 进行加锁操作将不会成功,直到线程 t 完全释放锁之后。

因为线程 t 这个时候还持有 m 的锁。这个知识点在 17.2.4 节我还会重点说。这里记住,被 notify 的线程在唤醒后是需要重新获取监视器锁的。

  • 如果 n 大于 0,而且这是一个 notifyAll 操作,那么等待集合中的所有线程都将从等待集合中移出,然后恢复。

注意,这些线程恢复后,只有一个线程可以锁住监视器。

本小节结束,通知操作相对来说还是很简单的吧。

17.2.3 中断(Interruptions)

中断发生于 Thread.interrupt 方法的调用。

令线程 t 调用线程 u 上的方法 u.interrupt(),其中 t 和 u 可以是同一个线程,这个操作会将 u 的中断状态设置为 true。

顺便说说中断状态吧,初学者肯定以为 thread.interrupt() 方法是用来暂停线程的,主要是和它对应中文翻译的“中断”有关。中断在并发中是常用的手段,请大家一定好好掌握。可以将中断理解为线程的状态,它的特殊之处在于设置了中断状态为 true 后,这几个方法会感知到:

  • wait(), wait(long), wait(long, int), join(), join(long), join(long, int), sleep(long), sleep(long, int)这些方法都有一个共同之处,方法签名上都有throws InterruptedException,这个就是用来响应中断状态修改的。
  • 如果线程阻塞在 InterruptibleChannel 类的 IO 操作中,那么这个 channel 会被关闭。
  • 如果线程阻塞在一个 Selector 中,那么 select 方法会立即返回。

如果线程阻塞在以上3种情况中,那么当线程感知到中断状态后(此线程的 interrupt() 方法被调用),会将中断状态重新设置为 false,然后执行相应的操作(通常就是跳到 catch 异常处)。

如果不是以上3种情况,那么,线程的 interrupt() 方法被调用,会将线程的中断状态设置为 true。

当然,除了这几个方法,我知道的是 LockSupport 中的 park 方法也能自动感知到线程被中断,当然,它不会重置中断状态为 false。我们说了,只有上面的几种情况会在感知到中断后先重置中断状态为 false,然后再继续执行。

另外,如果有一个对象 m,而且线程 u 此时在 m 的等待集合中,那么 u 将会从 m 的等待集合中移出。这会让 u 从 wait 操作中恢复过来,u 此时需要获取 m 的监视器锁,获取完锁以后,发现线程 u 处于中断状态,此时会抛出 InterruptedException 异常。

这里的流程:t 设置 u 的中断状态 => u 线程恢复 => u 获取 m 的监视器锁 => 获取锁以后,抛出 InterruptedException 异常。

这个流程在前面 wait 的小节已经讲过了,这也是很多人都不了解的知识点。如果还不懂,可以看下一小节的结束,我的两个简单的例子。

一个小细节:u 被中断,wait 方法返回,并不会立即抛出 InterruptedException 异常,而是在重新获取监视器锁之后才会抛出异常。

实例方法 thread.isInterrupted() 可以知道线程的中断状态。

调用静态方法 Thread.interrupted() 可以返回当前线程的中断状态,同时将中断状态设置为false。

所以说,如果是这个方法调用两次,那么第二次一定会返回 false,因为第一次会重置状态。当然了,前提是两次调用的中间没有发生设置线程中断状态的其他语句。

17.2.4 等待、通知和中断的交互(Interactions of Waits, Notification, and Interruption)

以上的一系列规范能让我们确定 在等待、通知、中断的交互中 有关的几个属性。

如果一个线程在等待期间,同时发生了通知和中断,它将发生:

  • 从 wait 方法中正常返回,同时不改变中断状态(也就是说,调用 Thread.interrupted 方法将会返回 true)
  • 由于抛出了 InterruptedException 异常而从 wait 方法中返回,中断状态设置为 false

线程可能没有重置它的中断状态,同时从 wait 方法中正常返回,即第一种情况。

也就是说,线程是从 notify 被唤醒的,由于发生了中断,所以中断状态为 true

同样的,通知也不能由于中断而丢失。

这个要说的是,线程其实是从中断唤醒的,那么线程醒过来,同时中断状态会被重置为 false。

假设 m 的等待集合为 线程集合 s,并且在另一个线程中调用了 m.notify(), 那么将发生:

  • 至少有集合 s 中的一个线程正常从 wait 方法返回,或者
  • 集合 s 中的所有线程由抛出 InterruptedException 异常而返回。

考虑是否有这个场景:x 被设置了中断状态,notify 选中了集合中的线程 x,那么这次 notify 将唤醒线程 x,其他线程(我们假设还有其他线程在等待)不会有变化。

答案:存在这种场景。因为这种场景是满足上述条件的,而且此时 x 的中断状态是 true。

注意,如果一个线程同时被中断和通知唤醒,同时这个线程通过抛出 InterruptedException 异常从 wait 中返回,那么等待集合中的某个其他线程一定会被通知。

下面我们通过 3 个例子简单分析下 wait、notify、中断 它们的组合使用。

第一个例子展示了 wait 和 notify 操作过程中的监视器锁的 持有、释放 的问题。考虑以下操作:

public class WaitNotify {

    public static void main(String[] args) {

        Object object = new Object();

        new Thread(new Runnable() {
            @Override
            public void run() {

                synchronized (object) {
                    System.out.println("线程1 获取到监视器锁");
                    try {
                        object.wait();
                        System.out.println("线程1 恢复啦。我为什么这么久才恢复,因为notify方法虽然早就发生了,可是我还要获取锁才能继续执行。");
                    } catch (InterruptedException e) {
                        System.out.println("线程1 wait方法抛出了InterruptedException异常");
                    }
                }
            }
        }, "线程1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("线程2 拿到了监视器锁。为什么呢,因为线程1 在 wait 方法的时候会自动释放锁");
                    System.out.println("线程2 执行 notify 操作");
                    object.notify();
                    System.out.println("线程2 执行完了 notify,先休息3秒再说。");
                    try {
                        Thread.sleep(3000);
                        System.out.println("线程2 休息完啦。注意了,调sleep方法和wait方法不一样,不会释放监视器锁");
                    } catch (InterruptedException e) {

                    }
                    System.out.println("线程2 休息够了,结束操作");
                }
            }
        }, "线程2").start();
    }
}

output:
线程1 获取到监视器锁
线程2 拿到了监视器锁。为什么呢,因为线程1 在 wait 方法的时候会自动释放锁
线程2 执行 notify 操作
线程2 执行完了 notify,先休息3秒再说。
线程2 休息完啦。注意了,调sleep方法和wait方法不一样,不会释放监视器锁
线程2 休息够了,结束操作
线程1 恢复啦。我为什么这么久才恢复,因为notify方法虽然早就发生了,可是我还要获取锁才能继续执行。

上面的例子展示了,wait 方法返回后,需要重新获取监视器锁,才可以继续往下执行。

同理,我们稍微修改下以上的程序,看下中断和 wait 之间的交互:

public class WaitNotify {

    public static void main(String[] args) {

        Object object = new Object();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {

                synchronized (object) {
                    System.out.println("线程1 获取到监视器锁");
                    try {
                        object.wait();
                        System.out.println("线程1 恢复啦。我为什么这么久才恢复,因为notify方法虽然早就发生了,可是我还要获取锁才能继续执行。");
                    } catch (InterruptedException e) {
                        System.out.println("线程1 wait方法抛出了InterruptedException异常,即使是异常,我也是要获取到监视器锁了才会抛出");
                    }
                }
            }
        }, "线程1");
        thread1.start();

        new Thread(new Runnable() {
            @Override
            public void run() {