Java 并发 (1) -- 多线程基础

文章目录

1. 简介

1. 并发与并行的区别

  1. 并发偏重于多个任务交替执行,而且多个任务之间可能是串行执行的或者轮转执行,是指两个或多个事件在同一时间间隔内发生,并且是指同一实体上的多个事件
  2. 并行则是真正意义上的 “同时执行”,是指两个或者多个事件在同一时刻发生,是在不同实体上的多个事件

从严格意义上来说,并行的多任务是真的同时执行,而对于并发来说,这个过程只是交替的,一会执行任务 A,一会执行任务 B,系统会不停地在两者之间切换。只不过对于外部观察者来说,即使多个任务之间是串行并发的,也会造成多任务间并行执行的错觉

实际上,如果系统内只有一个 CPU,而使用多进程或者多线程任务,那么真实环境中这些任务不可能是真实并行的,毕竟一个 CPU 一次只能执行一条指令,在这种情况下多进程或者多线程其实就是并发的,而不是并行的(操作系统会不停地切换任务)。真正的并行也只可能出现在拥有多个 CPU 的系统中(比如多核 CPU)

2. 进程和线程

讲区别之前,先讲一下他们各自的定义:

  1. 线程:线程是一个独立执行的调用序列,是一个对象,是 CPU 调度和分派的最小单元,是进程内多个可以同时执行的任务,线程也叫轻量级进程或轻权进程;在一个进程里可以创建多个线程,这些线程都拥有各自的程序计数器 、堆栈和局部变量等属性,并且同一个进程内的线程在同一时刻共享一些系统资源(比如文件句柄等),也能访问同一个进程所创建的对象资源(内存资源);在 Java 中每个程序都至少启动两个线程,一个是作为 Java 虚拟机启动参数运行在主类 main 方法中的线程,另一个是垃圾收集线程;在 Java 虚拟机初始化过程中也可能启动其他的后台线程,这种线程的数目和种类因 JVM 的实现而异;所有用户级线程都是显式被构造并在主线程或者其他用户线程中被启动;使用多线程而不是多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程
  2. 进程:进程是程序运行和资源分配的基本单位;是操作系统结构的基础;程序是指令 、数据及其组织形式的描述,而进程则是程序的实体;进程是的,或者说是正在被执行的;进程在执行过程中拥有独立的内存单元;进程控制块 (Process Control Block, PCB) 描述了进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作

他们的区别:(6 不同 + 1 相同)

  1. 进程是资源分配的最小单位,而线程是 CPU 调度的最小单位
  2. 创建进程或撤销进程,系统都要为它分配或回收资源,操作系统开销远大于创建或撤销线程时的开销
  3. 不同进程地址空间相互独立,同一进程内的线程共享同一地址空间。一个进程的线程在另一个进程内是不可见的
  4. 进程间是不会相互影响的,但一个线程挂掉就有可能导致整个进程挂掉
  5. 多进程是指操作系统同时运行多个任务(程序)
  6. 多线程是指在同一程序中有多个顺序流在执行
  7. 线程和进程一样分为五个阶段:新建、就绪、运行、阻塞、终止

3. 守护线程是什么

Java 中的线程分为两种:守护线程和用户线程;Daemon 线程是一种支持型线程,它主要被用于程序中的后台调度以及支持性工作,是为其他线程提供服务的;Thread 对象拥有一个守护标识属性,即 daemon,这个属性无法在构造方法中被赋值,但是可以在线程启动之前通过 Thread.setDaemon(bool on)设置该属性,当布尔类型的入参 on 为 true 时,则表示将该线程设置为守护线程,反之则表示用户线程。Thread.setDaemon() 必须在 Thread.start() 之前调用,否则运行时会抛出 IllegalThreadStateException 异常;dameon 的默认值为父线程的 daemon 属性值,也就是说,父线程如果为用户线程,子线程默认也是用户线程,父线程如果是守护线程,子线程默认也是守护线程;当一个 Java 虚拟机中不存在非守护线程的时候,不管 Daemon 线程处于什么状态,Java 虚拟机都将会自动退出,但是需要注意的是虚拟机退出时 Daemon 线程中的 finally 代码块并不一定会执行,还有就是当程序中所有的非守护线程都已经终止时,此时调用 setDaemon() 方法可能会导致虚拟机粗暴的终止线程并退出

举例有哪些守护线程的应用:服务守护进程、编译守护进程、windows 下的监听 Ctrl+break 的守护进程、Finalizer 守护进程、引用处理守护进程、GC 守护进程等等

4. 死锁 、活锁和饥饿

  1. 死锁应该是最糟糕的一种情况了(当然,其他几种情况也好不到哪里去),死锁是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去

    产生死锁有 4 个必要条件:

    1. 互斥条件:比如说某个进程在某一时间内独占资源。
    2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    3. 不可剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
    4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
  2. 活锁是指任务或者执行者没有被阻塞,但由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败

    活锁与死锁的区别在于,处于活锁的实体是在不断的改变状态的, 而处于死锁的实体表现为等待;活锁有可能自行解开,而死锁则不能

  3. 饥饿是指某一个或者多个线程因为种种原因无法获得所要的资源,导致一直无法执行

    Java 中导致饥饿有 3 个原因:

    1. 高优先级线程吞噬所有的低优先级线程的 CPU 时间
    2. 线程被永久堵塞在一个等待进入同步块的状态,其实就是其他线程总是能在它之前持续地对该同步块进行访问
    3. 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait() 方法)

5. Java 中用到的线程调度算法有哪些

计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得 CPU 的使用权才能执行指令,所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,Java 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权.

有两种调度模型:分时调度模型和抢占式调度模型。

分时调度模型是指让所有的线程轮流获得 CPU 的使用权,并且平均分配每个线程占用的 CPU 时间片

Java 虚拟机采用抢占式调度模型,指优先让运行池中优先级高的线程占用 CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用 CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU

6. 线程组是什么

  1. 我们可以把线程归属到某个线程组中,线程组可以包含多个线程以及线程组,线程和线程组之间组成了父子关系,类似一个树形结构

  2. 使用线程组可以方便管理线程,线程组通过 ThreadGroup 类实现

  3. 线程组允许同时把 Thread 的某些基本功能应用到一组线程中。不过其中有一些基本功能已经被废弃了,剩下的也很少使用

  4. 另外呢,从线程安全性的角度来看,ThreadGroup API 非常弱。因为如果你要得到一个线程组中的活动线程列表,你必须调用 enumerate() 方法,它有一个数组参数,并且数组的容量必须足够大,以便容纳所有的活动线程。activeCount() 方法是用来返回一个线程组中活动线程数量的,但是,一旦这个数组进行了分配,并传递给了 enumerate() 方法,那么这个数组的大小就固定为原先得到的活动线程数,但是如果线程数增加了,但是数组又不够长度去容纳这些额外增加的线程,所以当我们调用 enumerate() 方法的时候会忽略掉无法在数组中容纳的线程,这样就会引发安全问题。还有就是列出线程组汇总子组的 API 也有类似的缺陷。虽然通过增加新的方法,这些问题有可能得到修正,但是他们目前还没有被修正,因为线程组已经过时了,所以实际上也没有必要去修正,如果在开发过程中需要用到处理线程的逻辑组,可以使用线程池

  5. 但是需要注意的是,线程组和线程池是两个不同的概念,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建 、销毁线程的开销

7. 多线程的上线文切换

  1. 多线程会共同使用同一组计算机上的 CPU,当线程数大于给程序分配的 CPU 数量时,为了让各个线程都有执行的机会,就需要轮转使用 CPU 。在运行一个线程的过程中转去运行另外一个线程,这个过程就叫做线程上下文切换
  2. 由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存当前线程的运行状态,以便下次重新切换回来时能够继续以切换之前的状态运行。
    1. 线程切换时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值
    2. 另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值是多少,因此需要记录 CPU 寄存器的状态
  3. 所以一般来说,线程上下文切换过程中会记录程序计数器 、CPU 寄存器状态等数据

8. 创建线程的几种方式

创建线程有四种方式:

  1. 继承 Thread 类
  2. 实现 Runnable 接口
  3. 实现 Callable 接口并结合使用 FutureTask
  4. 线程池创建,例如 实现 Callable 接口并结合使用 Future

9. Runnable 与 Callable 的区别

  1. Runnable 接口中的 run() 方法的返回值是 void,它做的事情只是纯粹地去执行 run() 方法中的代码而已;

  2. Callable 接口中的 call() 方法是有返回值的,是一个泛型,和 Future 或 FutureTask 配合可以用来获取异步执行的结果

10. 线程的状态(生命周期)

Java 中的线程状态是通过 Thread.State 枚举类来定义的,该枚举类定义了 6 种线程状态,每一种线程状态都是一个枚举实例

  1. NEW(新建):指线程的初始状态,此时还没调用 start() 方法
  2. RUNNABLE(运行):Java 线程中的将就绪状态和运行状态统称为运行状态
  3. BLOCKED(阻塞):线程在 RUNNING 状态下被同步阻塞或 IO 阻塞时,会进入阻塞状态
  4. WAITING(等待):线程在 RUNNING 状态下调用了 wait()、join() 方法时,会进入等待状态,直到 run()执行结束或者其他线程调用了 notify()、notifyAll() 方法,线程会从等待状态进入 RUNNABLE 状态
  5. TIME_WAITING(定时等待):线程在 RUNNING 状态下调用了 wait(seccond)、join(seccond)、sleep(seccond) 方法时,会进入超时等待状态,直到睡眠时间期限已满,然后线程会从超时等待状态进入 RUNNABLE 状态
  6. TERMINATED(终止):表示当前线程已经执行完毕

在这里插入图片描述

11. sleep() 和 wait() 的区别

  1. sleep():

    1. sleep() 的作用是让当前线程休眠,让出 CPU 时间片(即不占用 CPU),但仍然持有对象锁,然后当前线程会从 “运行状态” 进入到 “阻塞状态”。等待完指定的睡眠时间后,它会由 “阻塞状态” 变成 “就绪状态”,然后等待 CPU 的调度执行
    2. 当 睡眠时间 = 0 时, 即 sleep(0),如果线程调度器的可运行队列中有大于或等于当前线程优先级的就绪线程存在,操作系统会将当前线程从处理器上移除,调度并运行其他优先级高的就绪线程;如果可运行队列中的没有就绪线程或所有就绪线程的优先级均低于当前线程优先级,那么当前线程会继续执行, 就像没有调用 sleep(0) 一样
    3. 当 timeout > 0 时,会引发线程上下文切换:调用线程会从线程调度器的可运行队列中被移除一段指定的时间,因为通常情况下,系统的时间精度 为 10 ms,那么指定的睡眠时间少于 10 ms 但大于 0 ms ,均会向上求值为 10 ms。
    4. switchToThread() 方法指如果当前有其他就绪线程在线程调度器的可运行队列中,始终会让出一个时间切片给这些就绪线程,而不管就绪线程的优先级的高低与否

    wait():让当前线程进入等待状态,同时会让当前线程释放它所持有的对象锁

  2. sleep() 方法可以在任何地方使用,而 wait() 方法则只能在同步方法或同步块中使用

  3. sleep() 是 Thread 类的方法,而 wait() 是 Object 类中的方法

12. 为什么 notify() 、wait() 等方法定义在了 Object 类中,而不是 Thread 类中

Object 中的 wait(), notify() 等函数,和 synchronized 一样,会对 “对象的同步锁” 进行操作。wait() 会使 “当前线程” 等待,并需要释放它所持有的 “同步锁”,否则其它线程就会因为获取不到该 “同步锁” 而无法运行

线程调用 wait() 之后,会释放它所持有的 “同步锁”;然后 notify() 或 notifyAll() 方法依据该释放的 “同步锁” 可以唤醒等待线程

唤醒线程只有在获取 “该对象的同步锁”,并且调用 notify() 或 notifyAll() 方法之后,才能唤醒等待线程。虽然,等待线程被唤醒;但是,它不能立刻执行,因为唤醒线程还持有 “该对象的同步锁”。必须等到唤醒线程释放了 “对象的同步锁” 之后,等待线程才有可能获取到 “对象的同步锁” 进而继续运行

总之,notify(), wait() 依赖于 “同步锁”,而 “同步锁” 是对象持有的,并且每个对象有且仅有一个!这大概就是为什么 notify(), wait() 等函数定义在 Object 类,而不是 Thread 类中的原因吧

因为这个 notify(), wait() 除了作为线程同步的工具外,其实还提供了线程间的通信机制,这有点类似Java 内存模型中说的,各线程的工作内存是无法互相访问的,线程间的通信需要依赖主内存来进行
.
而我觉得这个对象锁就有点像这个主内存的作用,它就像是两个线程间通信的一个桥梁,线程a 里面调用了这个对象的 wait 方法进入阻塞,线程b 里面调用了这个对象的 notify 方法来唤醒 因为释放了这个对象锁而进入等待的线程

13. run() 与 start() 的区别

  1. start():它的作用是启动一个新线程,新线程启动后会执行相应的 run() 方法,start() 方法不能被重复调用

    run():和普通的成员方法一样,可以被重复调用。单独调用 run() 的话,仅仅是在当前线程中执行 run() 中的代码,而不会启动新线程

  2. start() 实际上是通过一个本地方法 start0() 启动线程的。而 start0() 会新运行一个线程,新线程会调用 run()方法

    run()的源码里面通过一个 target 属性来关联一个具体的线程对象,最后通过这个 target 调用了我们重写的 run()

14. 线程中断

  1. interrupt() 只是改变中断状态而已,它不会中断一个正在运行的线程。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程就得以退出阻塞的状态。更确切的说,如果线程被 Object.wait, Thread.join 和 Thread.sleep 三种方法之一阻塞,此时调用该线程的 interrupt() 方法,那么该线程将抛出一个 InterruptedException 中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态(即该线程的阻塞状态将会被清除)。如果线程没有被阻塞,这时调用 interrupt() 将不起作用,直到执行到 wait(),sleep(),join() 时,才马上会抛出 InterruptedException 异常

  2. interrupted() 用于检测当前线程是否被中断,如果调用成功返回 true,中断状态会被清除即重置为false,由于它是静态方法,因此不能在特定的线程上使用,如果该方法被调用两次,则第二次一般是返回 false,如果线程不存活,则返回 false

  3. isinterrupted() 用于检测调用该方法的线程是否被中断,中断状态不会被清除。线程一旦被中断,该方法返回true,而一旦遇到 sleep() 等方法抛出异常,并清除中断状态,此时方法将返回 false

2. 精讲

1. 必须知道的几个概念

1. 同步和异步

  1. 同步(Synchronous)和异步(Asynchronous)通常来形容一次方法调用

  2. 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为

  3. 异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中 “真实” 地执行。整个过程中,不会阻碍调用者的工作

  4. 对于调用者来说,异步调用似乎是一瞬间就完成的。如果异步调用需要返回结果,那么当这个异步调用真实完成时,则会通知调用者

    打个比方,比如购物,如果你去商场买空调,当你到了商场看重了一款空调,你就向售货员下单。售货员去仓库帮你调配物品。这天你热的是在不行了,就催着商家赶紧给你送货,于是你就在商店里面候着他们,直到商家把你和空调一起送回家,一次愉快的购物就结束了。这就是同步调用。

    不过,如果我们赶时髦,就坐在家里打开电脑,在电脑上订购了一台空调。当你完成网上支付的时候,对你来说购物过程已经结束了。虽然空调还没有送到家,但是你的任务已经完成了。商家接到你的订单后,就会加紧送货,当然这一切已经跟你无关了。你已经支付完成,想干什么就能去干什么,出去溜几圈都不成问题,等送货上门的时候,接到商家的电话,回家一趟签收就完事了。这就是异步调用

2. 并发和并行

  1. 并发(Concurrency)和并行(Parallelism)是两个非常容易被混淆的概念。他们都可以表示两个或者多个任务一起执行,但是侧重点有所不同。

    1. 并发偏重于多个任务交替执行,而且多个任务之间可能是串行执行的或者轮转执行,是指两个或多个事件在同一时间间隔发生,是在同一实体上的多个事件

    2. 并行是真正意义上的 “同时执行”,是指两个或者多个事件在同一时刻发生,是在不同实体上的多个事件
      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OklCCYNJ-1588751987783)(4. Java 并发(1)- Java 多线程基础.assets/1578364091589.png)]
      大家排队在一个咖啡机上接咖啡,交替执行,是并发;两台咖啡机上面接咖啡,是并行

  2. 从严格意义上来说,并行的多任务是真的同时执行,而对于并发来说,这个过程只是交替的,一会执行任务 A,一会执行任务 B,系统会不停地在两者之间切换。只不过对于外部观察者来说,即使多个任务之间是串行并发的,也会造成多任务间并行执行的错觉

  3. 总结

    1. 并发说的是在一个时间段内,多件事情在这个时间段内交替执行
    2. 并行说的是多件事情在同一个时刻同事发生
      .

    实际上,如果系统内只有一个 CPU,而使用多进程或者多线程任务,那么真实环境中这些任务不可能是真实并行的,毕竟一个 CPU 一次只能执行一条指令,在这种情况下多进程或者多线程就是并发的,而不是并行的(操作系统会不停地切换任务)。真实的并行也只可能出现在拥有多个 CPU 的系统中(比如多核 CPU)

3. 阻塞和非阻塞

阻塞(Blocking)和非阻塞(non - Blocking)通常用来形容很多线程间的相互影响。比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中等待。等待会导致线程挂起,这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他阻塞在这个临界区上的线程就都无法工作。

非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行,所有的线程都会尝试不断向前执行

4. 死锁 、饥饿和活锁

死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)都属于多线程的活跃性问题。如果发现上述几种情况,那么相关线程就不再活跃,也就是说它可能很难再继续往下执行了

  1. 死锁应该是最糟糕的一种情况了(当然,其他几种情况也好不到哪里去),是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去

    产生死锁的必要条件:

    1. 互斥条件:所谓互斥就是进程在某一时间内独占资源。
    2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    3. 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
    4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
      .

    如下图显示了一个死锁的发生:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gMwHPRnw-1588751987785)(4. Java 并发(1)- Java 多线程基础.assets/640-1578364781707.webp)]
    A、B、C、D 四辆小车都在这种情况下都无法继续行驶了。他们彼此之间相互占用了其他车辆的车道,如果大家都不愿意释放自己的车道,那么这个状况将永远持续下去,谁都不可能通过,死锁是一个很严重的并且应该避免和实时小心的问题

    例子:
    在这里插入图片描述

    thread1 持有 com.jvm.visualvm.Demo4$Obj1 的锁,等待获取 com.jvm.visualvm.Demo4$Obj2 的锁

    thread2 持有 com.jvm.visualvm.Demo4$Obj2 的锁,等待获取 com.jvm.visualvm.Demo4$Obj1 的锁,两个线程相互等待获取对方持有的锁,出现死锁

  2. 饥饿是指某一个或者多个线程因为种种原因无法获得所要的资源,导致一直无法执行。

    Java 中导致饥饿的原因:

    1. 高优先级线程吞噬所有的低优先级线程的 CPU 时间。
    2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
    3. 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait() 方法),因为其他线程总是被持续地获得唤醒
      .

    比如它的优先级可能太低,而高优先级的线程不断抢占它需要的资源,导致低优先级线程无法工作。在自然界中,母鸡给雏鸟喂食很容易出现这种情况:由于雏鸟很多,食物有限,雏鸟之间的食物竞争可能非常厉害,经常抢不到事务的雏鸟有可能被饿死。线程的饥饿非常类似这种情况。

    此外,某一个线程一直占着关键资源不放,导致其他需要这个资源的线程无法正常执行,这种情况也是饥饿的一种。与死锁相比,饥饿还是有可能在未来一段时间内解决的。比如,高优先级的线程已经完成任务,不再疯狂执行

    例子:

    在这里插入图片描述

    使用 jstack 命令查看线程堆栈信息后发现,主线程在 32 行处于等待中,线程池中的工作线程在 25 行处于等待中,等待获取结果。由于线程池是一个线程,AnotherCallable 得不到执行,而被饿死,最终导致了程序死锁的现象

  3. 活锁是一种非常有趣的情况。指任务或者执行者没有被阻塞,但由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败

    不知道大家是否遇到过这么一种场景,当你要做电梯下楼时,电梯到了,门开了,这时你正准备出去。但很不巧的是,门外一个人挡着你的去路,他想进来。于是,你很礼貌地靠左走,礼让对方。同时,对方也非常礼貌的靠右走,希望礼让你。结果,你们俩就又撞上了。于是乎,你们都意识到了问题,希望尽快避让对方,你立即向右边走,同时,他立即向左边走。结果,又撞上了!

    不过介于人类的智慧,我相信这个动作重复两三次后,你应该可以顺利解决这个问题。因为这个时候,大家都会本能地对视,进行交流,保证这种情况不再发生。

    但如果这种情况发生在两个线程之间可能就不那么幸运了。如果线程智力不够。且都秉承着 “谦让” 的原则,主动将资源释放给他人使用,那么久会导致资源不断地在两个线程间跳动,而没有一个线程可以同时拿到所有资源正常执行。这种情况就是活锁

2. 多线程基础

1. 线程是什么

1. 线程的定义
  1. 线程是一个独立执行的调用序列,是一个对象,是 CPU 调度和分派的最小单元,是进程内允许多个同时进行的任务,线程也叫轻量级进程或轻权进程
  2. 在一个进程里可以创建多个线程,这些线程都拥有各自的程序计数器 、堆栈和局部变量等属性,并且同一个进程内的线程在同一时刻共享一些系统资源(比如文件句柄等),也能访问同一个进程所创建的对象资源(内存资源),java.lang.Thread 对象负责统计和控制这种行为
  3. 在 Java 中每个程序都至少启动两个线程,一个是作为 Java 虚拟机启动参数运行在主类 main 方法中的线程,另一个是垃圾收集线程
  4. 在 Java 虚拟机初始化过程中也可能启动其他的后台线程,这种线程的数目和种类因 JVM 的实现而异
  5. 所有用户级线程都是显式被构造并在主线程或者其他用户线程中被启动
  6. 使用多线程而不是多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程
2. 进程的定义
  1. 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动
  2. 是系统进行资源分配和调度的基本单位
  3. 是操作系统结构的基础
  4. 程序是指令 、数据及其组织形式的描述,而进程则是程序的实体
  5. 进程是的,或者说是正在被执行的
  6. 进程具有四个特性:
    1. 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的
    2. 并发性:任何进程都可以同其他进程一起并发执行
    3. 独立性:进程是系统进行资源分配和调度的一个独立单位
    4. 结构性:进程由程序 、数据和进程控制块三部分组成
3. 线程与进程的区别
  1. 进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含 1- n 个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程会一直运行,直到所有的非守护线程都结束运行后才能结束,进程是资源分配的最小单位

  2. 线程:同一进程内的所有线程共享该进程的代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小,线程是 CPU 调度的最小单位

  3. 线程和进程一样分为五个阶段:新建、就绪、运行、阻塞、终止

  4. 多进程是指操作系统能同时运行多个任务(程序)

  5. 多线程是指在同一程序中有多个顺序流在执行

4. 创建线程的 4 种方式
  • 继承 Thread 类

    @Test
    public void test1() {
        class A extends Thread {
            @Override
            public void run() {
                System.out.println("A run");
            }
        }
        A a = new A();
        a.start();
    }
    
  • 实现 Runable 接口

    @Test
    public void test2() {
        class B implements Runnable {
    
            @Override
            public void run() {
                System.out.println("B run");
            }
        }
        B b = new B();
        //Runable 实现类需要由 Thread 类包装后才能执行
        new Thread(b).start();
    }
    
  • 实现 Callable 接口

    • 并与 Future、线程池结合使用,例子在线程池创建那里

    • 并与 FutureTask 结合使用

      @Test
      public void test3() {
          Callable callable = new Callable() {
              int sum = 0;
              @Override
              public Object call() throws Exception {
                  for (int i = 0;i < 5;i ++) {
                      sum += i;
                  }
                  return sum;
              }
          };
          //这里要用FutureTask,否则不能加入Thread构造方法
          FutureTask futureTask = new FutureTask(callable);
          new Thread(futureTask).start();
          try {
              System.out.println(futureTask.get());
          } catch (InterruptedException e) {
              e.printStackTrace();
          } catch (ExecutionException e) {
              e.printStackTrace();
          }
      }
      
  • 通过线程池创建

    @Test
    public void test4() {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        //execute直接执行线程 
        executorService.execute(new Runnable() { //实现 Runnable 接口的方式
            @Override
            public void run() {
                System.out.println("runnable");
            }
        });
        //submit提交有返回结果的任务,运行完后返回结果。
        Future future = executorService.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "a";
            }
        });
        try {
            System.out.println(future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
    

继承 Thread 类和实现 Runnable 接口创建线程的区别:

实现 Runnable 接口更具优势:

  1. 可以避免 Java 中的单继承问题,Runnable 的可扩展性更好
  2. 可以用于资源共享。如果多个线程都是基于某一个 Runnable 对象建立的,那么他们会共享该 Runnable 对象上的资源

资源共享的示例:

//1.Thread 类的多线程示例
public class MyThread extends Thread {
    
    private int ticket = 10;
    public void run(){
        for(int i = 0; i < 20; i++){
            if(this.ticket > 0){
                System.out.println(this.getName() + " 卖票:ticket" + this.ticket--);
            }
        }
    }

    public static void main(String[] args) {
        // 启动三个线程,每个线程各卖10张票
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

//运行结果
Thread-1 卖票:ticket10
Thread-2 卖票:ticket10
Thread-0 卖票:ticket10
Thread-2 卖票:ticket9
Thread-1 卖票:ticket9
Thread-2 卖票:ticket8
Thread-0 卖票:ticket9
Thread-2 卖票:ticket7
Thread-1 卖票:ticket8
Thread-2 卖票:ticket6
Thread-0 卖票:ticket8
Thread-2 卖票:ticket5
Thread-1 卖票:ticket7
Thread-2 卖票:ticket4
Thread-0 卖票:ticket7
Thread-2 卖票:ticket3
Thread-1 卖票:ticket6
Thread-2 卖票:ticket2
Thread-0 卖票:ticket6
Thread-2 卖票:ticket1
Thread-1 卖票:ticket5
Thread-0 卖票:ticket5
Thread-1 卖票:ticket4
Thread-0 卖票:ticket4
Thread-1 卖票:ticket3
Thread-0 卖票:ticket3
Thread-1 卖票:ticket2
Thread-0 卖票:ticket2
Thread-1 卖票:ticket1
Thread-0 卖票:ticket1

可以看出 MyThread 继承自 Thread,它是自定义线程。主线程 main 创建并启动 3 个 MyThread 子线程。每个子线程都各自卖出了 10 张票

//2.实现 Runnable 接口的多线程示例
public class MyThread implements Runnable {

    private int ticket = 10;
    public void run(){
        for(int i = 0; i < 20; i++){
            if(this.ticket > 0){
                System.out.println(Thread.currentThread().getName() + " 卖票:ticket" + this.ticket--);
            }
        }
    }

    public static void main(String[] args) {
        MyThread mt = new MyThread();
        
        Thread t1 = new Thread(mt);
        Thread t2 = new Thread(mt);
        Thread t3 = new Thread(mt);
        t1.start();
        t2.start();
        t3.start();
    }
}

//运行结果
Thread-0 卖票:ticket10
Thread-1 卖票:ticket9
Thread-2 卖票:ticket8
Thread-1 卖票:ticket6
Thread-0 卖票:ticket7
Thread-1 卖票:ticket4
Thread-2 卖票:ticket5
Thread-1 卖票:ticket2
Thread-0 卖票:ticket3
Thread-2 卖票:ticket1

和上面 MyThread 继承自 Thread 类不同,这里的 MyThread 实现了 Runnable 接口,主线程 main 创建并启动 3 个子线程,而且这 3 个子线程都是基于 mt 这个 Runnable 对象而创建的。运行结果是这 3 个子线程一共卖出了 10 张票。这说明它们是共享了 mt 对象的

注意:

每次 Java 程序至少会启动 2 个线程。一个是 main 线程,另一个是垃圾收集线程。因为每当使用 Java 命令执行一个类的时候,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动的一个进程

5. 线程的生命周期(五种状态)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-63R28AUO-1588751987790)(4. Java 并发(1)- Java 多线程基础.assets/20181201110806310.png)]
第一种状态:新建(New):新建了一个线程对象。例如,Thread thread = new Thread();

第二种状态:可运行状态(Runnable):又叫“就绪状态”。线程新建后,其他线程(比如 main 线程)调用了该对象的 start() 方法,从而来启动该线程。Runnable 状态的线程位于可运行线程池中,等待被线程调度选中,获取 CPU 的使用权;

第三种状态:运行状态(Running):可运行状态(Runnable)的线程获得了 CPU 时间片(timeslice),执行代码。需要注意的是,线程只能从就绪状态进入到运行状态;

第四种状态:阻塞状态(Block):阻塞状态是指线程因为某种原因放弃了 CPU 使用权,即让出了 CPU TimeSlice,暂时停止运行。只有当线程重新进入可运行状态,才有机会再次获得 CPU TimeSlice 进而转到运行状态。阻塞的情况有三种:

  1. 等待阻塞:运行(Running)的线程执行 wait() 方法,JVM 会把该线程放入等待队列(waiting queue)中;

  2. 同步阻塞:运行(Running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程先放入锁池(也就是同步队列)(lock pool)中;

  3. 其他阻塞:运行(RunnIng)的线程执行 Thread.sleep() 方法或者 Thread.join() 方法,或者发出 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep() 状态超时时 、join() 后等待的线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态

第五种状态:终止(Terminated)或者死亡(Dead):线程 run() 方法和 main() 方法执行结束,或者因异常退出了 run() 方法,则该线程结束生命周期。死亡的线程是不可再次重生的

当然,Java 中的线程状态是通过 Thread.State 枚举类来定义的,该枚举类定义了六种线程状态,每一种线程状态都是一个枚举实例

代码如下:

public enum State
{
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;

}

在这里插入图片描述
状态转换图如下:
在这里插入图片描述
注意事项:

  1. sleep()yield() 的区别在于: sleep() 可以使优先级低的线程得到执行的机会, 而 yield() 只能使同优先级的线程有执行的机会;

  2. interrupt() 方法不会中断一个正在运行的线程,就是说如果线程正在运行, 调用此方法是没有任何反应的。为什么呢,因为这个方法只是提供给被阻塞的线程,即当线程调用了 Object.wait()Thread.join()Thread.sleep() 三种方法之一的时候,再调用 interrupt() 方法,才可以中断刚才的阻塞而继续去执行线程;

  3. join(0) 表示等待一个线程执行直到它死亡返回主线程, join(1000) 表示主线程等待一个线程 1000 毫秒后回到主线程继续执行;

  4. waite()notify() 必须在 synchronized 函数或 synchronized block 中进行调用。如果在 non-synchronized 函数或 non-synchronized block 中进行调用,虽然能编译通过,但在运行时会发生 IllegalMonitorStateException 异常

6. run() 与 start() 的区别
  1. start():它的作用是启动一个新线程,新线程启动后会执行相应的 run() 方法,start() 方法不能被重复调用
  2. run():和普通的成员方法一样,可以被重复调用。单独调用 run() 的话,仅仅是在当前线程中执行 run() 方法,而不会启动新线程

Thread 类中的 start() 方法源码:

public synchronized void start() {
    // 如果线程不是"就绪状态",则抛出异常!
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    // 将线程添加到ThreadGroup中
    group.add(this);

    boolean started = false;
    try {
        // 通过start0()启动线程
        start0();
        // 设置started标记
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}

start() 实际上是通过本地方法 start0() 启动线程的。而 start0() 会新运行一个线程,新线程会调用 run()方法

private native void start0();

Thread 类中的 run() 方法源码:

public void run() {
    if (target != null) {
        target.run();
    }
}

target 是一个 Runnable 对象或者继承 Thread 类的子类对象(或者更准确地说应该是重写了 run() 所对应的对象)。run() 就是直接调用 target 的 run() 方法,并不会新建一个线程

7. 线程组

我们可以把线程归属到某个线程组中,线程组可以包含多个线程以及线程组,线程和线程组组成了父子关系,是个树形结构,如下图:
在这里插入图片描述
使用线程组可以方便管理线程,线程组提供了一些方法方便方便我们管理线程

1. 创建线程关联线程组

创建线程的时候,可以给线程指定一个线程组,代码如下:
在这里插入图片描述
运行结果:
在这里插入图片描述
activeCount() 方法可以返回线程组中的所有活动线程数,包含下面的所有子孙节点的线程,由于线程组中的线程是动态变化的,这个值只能是一个估算值

2. 为线程组指定父线程组

创建线程组的时候,可以给其指定一个父线程组,也可以不指定,如果不指定父线程组,则父线程组为当前线程的线程组,java API 有 2 个常用的构造方法用来创建线程组:
在这里插入图片描述
第一个构造方法未指定父线程组,看一下内部的实现:
在这里插入图片描述
系统自动获取当前线程的线程组作为默认父线程组

线程组的 list() 方法,可以将线程组中的所有子孙节点信息输出到控制台,用于调试使用,eg:

threadGroud1.list();

控制台输出:
在这里插入图片描述

3. 根线程组

获取根线程组:
在这里插入图片描述
输出结果:
在这里插入图片描述
从上面代码可以看出:主线程的线程组为 main,根线程组为 system

ThreadGroup 的源码:
在这里插入图片描述
发现 ThreadGroup 默认构造方法是 private 的,是由 C 调用的,创建的正是 system 线程组

4. 批量停止线程

调用线程组 interrupt(),会将线程组树下的所有子孙线程中断标志置为 true,可以用来批量中断线程

示例代码:
在这里插入图片描述
输出结果:
在这里插入图片描述
停止线程之后,通过 list() 方法可以看出输出的信息中不包含已结束的线程了

注意事项:

  1. 因为使用线程组有很多安全隐患,所以不推荐使用它,如果有需要,推荐使用线程池
  2. 如果提供的 ThreadGroup 不允许被访问,那么就会抛出一个 SecurityException
8. 守护线程
  1. Java 中的线程分为两种:守护线程(Daemon)和用户线程(User)

  2. Daemon 线程是一种支持型线程,它主要被用于程序中后台调度以及支持性工作,是为其他线程提供服务的,也可以理解为守护线程是 JVM 自动创建的线程(但不一定),用户线程是程序创建的线程

  3. Thread 对象拥有一个守护(daemon)标识属性,这个属性无法在构造方法中被赋值,但是可以在线程启动之前设置该属性,即通过方法 Thread.setDaemon(bool on) 完成设置,on 为 true 则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon() 必须在 Thread.start() 之前调用,否则运行时会抛出 IllegalThreadStateException 异常

  4. dameon 的默认值为为父线程的 daemon,也就是说,父线程如果为用户线程,子线程默认也是用户线程,父线程如果是守护线程,子线程默认也是守护线程,Thread 类中的 init() 方法部分内容如下:
    在这里插入图片描述

  5. 当一个 Java 虚拟机中不存在非 Daemon 线程的时候,不管 Daemon 线程处于什么状态,Java 虚拟机都将会退出。即如果全部的 User Thread 已经撤离,Daemon 没有可服务的线程,那么 JVM 将会退出。比如, JVM 的垃圾回收线程就是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是 Java 虚拟机上仅剩的线程时,Java 虚拟机会自动退出

  6. 但是需要注意的是虚拟机退出时 Daemon 线程中的 finally 代码块并不一定会执行

  7. 当程序中所有的非守护线程都已经终止,调用 setDaemon() 方法可能会导致虚拟机粗暴的终止线程并退出

  8. isDaemon() 方法能够返回该属性的值。守护状态的作用非常有限,即使是后台线程在程序退出的时候也经常需要做一些清理工作

  9. daemon 的发音为”day-mon”,这是系统编程传统的遗留,系统守护进程是一个持续运行的进程

  10. Thread Dump 打印出来的线程信息,含有 daemon 字样的线程即为守护进程,可能会有:服务守护进程、编译守护进程、windows 下的监听 Ctrl+break 的守护进程、Finalizer 守护进程、引用处理守护进程、GC 守护进程

9. 线程的优先级
  • Java 的线程实现基本上都是内核级线程的实现,所以 Java 线程的具体执行还取决于操作系统的特性
  • 每个线程都有一个优先级,分布在 Thread.MIN_PRIORITYThread.MAX_PRIORITY 之间(分别为 1 和 10) 默认情况下,新创建的线程都拥有和创建它的线程相同的优先级。main 方法所关联的初始化线程拥有一个默认的优先级,这个优先级是 Thread.NORM_PRIORITY (为 5)
  • 线程的当前优先级可以通过 getPriority() 方法获得。 线程的优先级可以通过 setPriority() 方法来动态的修改,一个线程的最高优先级由其所在的线程组限定
10. 一些线程启动的注意事项
  • 调用 start() 方法会触发 Thread 实例以一个新的线程启动其 run() 方法
  • 当一个线程正常地运行结束或者抛出某种未检测的异常(比如,运行时异常 、错误或者他们的子类),线程就会终止
  • 当线程终止之后,是不能被重新启动的。在同一个 Thread 上调用多次 start() 方法会抛出 InvalidThreadStateException 异常
  • 如果线程已经启动但是还没有终止,那么调用 isAlive() 方法就会返回 true。即使线程由于某些原因处于阻塞(Blocked)状态该方法依然返回 true
  • 如果线程已经被取消(Cancelled),那么调用其 isAlive() 在什么时候返回 false 就因各 Java 虚拟机的实现而异了。没有方法可以得知一个处于非活动状态的线程是否已经被启动过

2. 多线程是什么

1. 多线程的定义
  • 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
  • 多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销
  • 多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的

一个多线程示例:

ArrayList<String> list = new ArrayList<>();
//有返回值的线程组将返回值存进集合
for (int i = 0;i < 5;i ++ ) { //创建了五个线程
    int finalI = i;
    Future future = executorService.submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "res" + finalI;
        }
    });
    try {
        list.add((String) future.get());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}
for (String s : list) {
    System.out.println(s);
}
2. 多线程带来的问题

在过去单 CPU 时代,单任务在一个时间点只能执行单一程序。之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程。虽然并不是真正意义上的 “同一时间点”,而是多个任务或进程共享一个 CPU,并交由操作系统来完成多任务间对 CPU 的运行切换,以使得每个任务都有机会获得一定的 CPU 时间片运行。

随着多任务对软件开发者带来的新挑战,程序不在能假设独占所有的 CPU 时间 、所有的内存和其他计算机资源。一个好的程序榜样是在其不再使用这些资源时对其进行释放,以使得其他程序能有机会使用这些资源。

再后来发展到多线程技术,使得在一个程序内部能拥有多个线程并行执行。一个线程的执行可以被认为是一个 CPU 在执行该程序。当一个程序运行在多线程下,就好像有多个 CPU 在同时执行该程序。

多线程比多任务更加有挑战。多线程是在同一个程序内部并行执行,因此会对相同的内存空间进行并发读写操作。这可能是在单线程程序中从来不会遇到的问题。其中的一些错误也未必会在单 CPU 机器上出现,因为两个线程从来不会得到真正的并行执行。然而,更现代的计算机伴随着多核 CPU 的出现,也就意味着不同的线程能被不同的 CPU 核真正意义地并行执行。

如果一个线程在读一个内存时,另一个线程正向该内存进行写操作,那进行读操作的那个线程将获得什么结果呢?是写操作之前旧的值?还是写操作成功之后的新值?或是一半新一半旧的值?或者,如果是两个线程同时写同一个内存,在操作完成后将会是什么结果呢?是第一个线程写入的值?还是第二个线程写入的值?还是两个线程写入的一个混合值?因此如果没有合适的预防措施,任何结果都是可能的。而且这种行为的发生甚至不能预测,所以结果也是不确定的

Java 是最先支持多线程的开发的语言之一,Java 从一开始就支持了多线程功能,因此 Java 开发者能常遇到上面描述的问题场景。

3. 多线程的好处
1. 资源利用率更好

.在一个程序中,有很多的操作是非常耗时的,如数据库读写操作 、IO 操作等,如果使用单线程,那么程序就必须等待这些操作执行完成之后才能执行其他操作。使用多线程的话,就可以在将耗时任务放在后台继续执行的同时,执行其他操作

想象一下,一个应用程序需要从本地文件系统中读取和处理文件的情景。比方说,从磁盘读取一个文件需要 5 秒,处理一个文件需要 2 秒。处理两个文件则需要:
在这里插入图片描述

可以发现,从磁盘中读取文件的时候,大部分的 CPU 时间用于等待磁盘去读取数据。在这段时间里,CPU 非常的空闲。它可以做一些别的事情。通过改变操作的顺序,就能够更好的使用 CPU 资源。看下面的顺序:
在这里插入图片描述

CPU 等待第一个文件被读取完,然后开始读取第二个文件。当第二文件在被读取的时候,CPU 会同时去处理第一个文件。记住,在等待磁盘读取文件的时候,CPU 大部分时间是空闲的。

总的说来,CPU 能够在等待 IO 的时候做一些其他的事情。这个 IO 不一定就是磁盘 IO,它也可以是网络的 IO,或者用户输入。通常情况下,网络和磁盘的 IO 比 CPU 和内存的 IO 慢得多

2. 程序设计更简单

在单线程应用程序中,如果你想编写程序手动处理上面所提到的读取和处理的顺序,你必须记录每个文件读取和处理的状态。

而使用多线程时,你可以启动两个线程,每个线程处理一个文件的读取和操作。线程会在等待磁盘读取文件的过程中被阻塞。在等待的时候,其他的线程能够使用 CPU 去处理已经读取完的文件。其结果就是,磁盘总是在繁忙地读取不同的文件到内存中。这会带来磁盘和 CPU 利用率的提升。而且每个线程只需要记录一个文件,因此这种方式也很容易编程实现。

3. 响应速度更快

将一个单线程应用程序变成多线程应用程序的另一个常见的目的是实现一个响应更快的应用程序。设想一个服务器应用,它在某一个端口监听进来的请求。当一个请求到来时,它去处理这个请求,然后再回去监听。

服务器的流程如下所述:

while(server is active){    
    listen for request;  // 监听 
    process request;     // 处理请求    
}

如果一个请求需要占用大量的时间来处理,在这段时间内新的客户端就无法发送请求给服务端。只有当服务器在监听的时候,请求才能被接收。

另一种设计是,监听线程把请求传递给工作者线程(worker thread),然后立刻返回去监听。而工作者线程则能够处理这个请求并发送一个回复给客户端。这种设计如下所述:

while(server is active){    
    listen for request;              // 监听
    hand request to worker thread;   // 将请求转交给工作者线程处理 
}

这种方式,可以让服务端线程迅速地返回去监听。因此,更多的客户端能够发送请求给服务端,也就是说这个服务的响应变得更快了

桌面应用也是同样如此,如果你点击一个按钮开始运行一个耗时的任务,这个线程既要执行任务又要更新窗口和按钮,那么在任务执行的过程中,这个应用程序看起来好像没有反应一样。相反,任务可以传递给工作者线程(word thread)。当工作者线程在繁忙地处理任务的时候,窗口线程可以自由地响应其他用户的请求。当工作者线程完成任务的时候,它发送信号给窗口线程。窗口线程便可以更新应用程序窗口,并显示任务的结果。对用户而言,这种具有工作者线程设计的程序显得响应速度更快

4. 多线程的代价

从一个单线程的应用到一个多线程的应用并不仅仅带来好处,它也会有一些代价。不要仅仅为了使用多线程而使用多线程。而应该明确只有当使用多线程时所带来的好处比所付出的代价大的时候,才使用多线程。如果存在疑问,应该尝试测量一下应用程序的性能和响应能力,而不只是猜测

1. 设计更复杂

虽然有一些多线程应用程序比单线程的应用程序要简单,但其他的一般都更复杂。在多线程访问共享数据的时候,这部分代码需要特别的注意。线程之间的交互往往非常复杂。不正确的线程同步产生的错误非常难以被发现,并且难以修复

2. 线程上文切换的开销

当 CPU 从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为上下文切换(context switch)。CPU 会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。上下文切换并不廉价。如果没有必要,应该减少上下文切换的发生

3. 增加资源消耗

除了 CPU,使用太多线程,也是很耗系统资源的,因为线程需要开辟内存来维持它本地的堆栈,更多线程也就需要更多内存。它也需要占用操作系统中一些资源来管理线程。我们可以尝试编写一个程序,让它创建 100 个线程,这些线程什么事情都不做,只是在等待,然后看看这个程序在运行的时候占用了多少内存

5. 竞态条件和临界区

在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。

多线程同时执行下面的代码可能会出错:

public class Counter{
    protected long count = 0;
    public void add(long value){
        this.count = this.count + value;
    }
}

假如现在,线程 A 和线程 B 同时执行同一个 Counter 对象的 add() 方法,我们无法知道操作系统何时会在两个线程之间切换。JVM 并不是将这段代码视为单条指令来执行的,而是按照下面的顺序:
在这里插入图片描述
假设现在有这样一种场景:观察线程 A 和 B 交错执行会发生什么:
在这里插入图片描述
两个线程分别加了 2 和 3 到 count 变量上,两个线程执行结束后,count 变量的值本应该要等于 5。然而由于两个线程是交叉执行的,两个线程从内存中读出的初始值都是 0,然后各自加了 2 和 3,并分别写回内存。最终的值并不是期望的 5,而是最后写回内存的那个线程的值,上面例子中最后写回内存的是线程 A,但实际中也可能是线程 B。如果没有采用合适的同步机制,线程间的交叉执行情况就无法预料

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。上面的 add() 方法就是一个临界区,它会产生竞态条件。在临界区中使用适当的同步就可以避免竞态条件

1. 临界区详解

临界区用来表示一种公共资源或者说共享数据,可以被多个线程使用,但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源就必须等待。

比如,一个办公室里有一台打印机,打印机一次只能执行一个任务。如果小王和小明同时需要打印文件,很明显,如果小王先发了打印任务,打印机就开始打印小王的文件,小明的任务就只能等待小王打印结束后才能打印,这里的打印机就是一个临界区的例子。

在并行程序中,临界区资源是保护的对象(即需要加同步机制之类的),如果意外出现打印机同时执行两个任务的情况(多线程情况下不加安全机制),那么最有可能的结果就是打印出来的文件是损坏的文件,它既不是小王想要的,也不是小明想要的

6. 线程安全与共享资源

允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。当多个线程同时更新共享资源时会引发竞态条件。因此,了解 Java 线程执行时共享了什么资源很重要

1. 基础类型的局部变量

局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。下面是基础类型的局部变量的一个例子:

public void someMethod(){   
    long threadSafeInt = 0;    
    threadSafeInt++;
}

2. 引用类型的局部变量

引用值仍然存储在线程自己的栈中,不会被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。如果在某个方法中创建的对象不会逃逸出该方法(即该对象不会被其它方法获得,也不会被非局部变量引用到),那么它就是线程安全的。实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。下面是一个线程安全的局部引用样例:

public void someMethod(){ //每次调用这个方法都会生成不同的 localObject 对象
    LocalObject localObject = new LocalObject(); 
    localObject.callMethod(); 
    method2(localObject);//每次调用someMethod时,传入的localObject对象都是不同的
}
 
public void method2(LocalObject localObject){
    localObject.setValue("value");  //还是localObject对象,没有其他对象
}

样例中 LocalObject 对象没有被方法返回,也没有被传递给 someMethod() 方法外的对象。每个执行 someMethod() 的线程都会创建自己的 LocalObject 对象,并赋值给 localObject 引用。因此,这里的 LocalObject 是线程安全的。事实上,整个 someMethod() 都是线程安全的。即使将 LocalObject 作为参数传给同一个类的其它方法或其它类的方法时,它仍然是线程安全的。当然,如果 LocalObject 通过某些方法被传给了别的线程,那它就不再是线程安全的了

3. 对象

对象本身存储在堆上,如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。

例如:

public class NotThreadSafe(){ 
    StringBuilder builder = new StringBuilder(); 
    public void add(String text){
        this.builder.append(text);
    }
}

如果两个线程同时调用同一个NotThreadSafe实例上的 add() 方法,就会有竞态条件问题。例如:

NotThreadSafe sharedInstance = new NotThreadSafe();
new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start(); 

public class MyRunnable implements Runnable{ 
    NotThreadSafe instance = null;    
    public MyRunnable(NotThreadSafe instance){    
        this.instance = instance;
    } 
    public void run(){
        this.instance.add("some text");
    }
}

注意两个 MyRunnable 共享了同一个 NotThreadSafe 对象。因此,当它们调用 add() 方法时会造成竞态条件。当然,如果这两个线程在不同的 NotThreadSafe 实例上调用 add() 方法,就不会导致竞态条件。下面是稍微修改后的例子:

new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();

现在两个线程都有自己单独的 NotThreadSafe 对象,调用 add() 方法时互不干扰,因此不会有竞态条件问题。所以非线程安全的对象仍可以通过某种方式来消除竞态条件

4. 线程控制逃逸规则

线程控制逃逸规则可以帮助你判断代码中对某些资源的访问是否是线程安全的。

如果一个资源的创建 、使用 、销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。

资源可以是对象 、数组 、文件 、数据库连接 、套接字等等。Java 中你无需主动销毁对象,所谓 “销毁” 指不再有引用指向对象

即使对象本身线程安全,但如果该对象中包含其他资源(文件 、数据库连接),整个应用也许就不再是线程安全的了。比如 2 个线程都创建了各自的数据库连接,每个连接自身是线程安全的,但它们所连接到的同一个数据库也许不是线程安全的。比如,2 个线程执行如下代码:
在这里插入图片描述
如果两个线程同时执行,而且碰巧检查的是同一个记录,那么两个线程最终可能都插入了记录:
在这里插入图片描述
同样的问题也会发生在文件或其他共享资源上。因此,区分某个线程控制的对象是资源本身,还是仅仅到某个资源的引用很重要

7. 线程安全与不可变性

当多个线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了写操作,才会产生竞态条件。多个线程同时读同一个资源不会产生竞态条件

我们可以通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。如下示例:

public class ImmutableValue{ 
    
    private int value = 0; 
    public ImmuttableValue(int value){
        this.value = value;   // 通过构造函数给value赋值
    }    
    
    public int getValue(){
        return this.value;
    }
}

请注意 ImmutableValue 类的成员变量 value 是通过构造函数赋值的,并且在类中没有 set() 方法。这意味着一旦 ImmutableValue 实例被创建,value 变量就不能再被修改,这就是不可变性。但你可以通过 getValue() 方法读取这个变量的值。
在这里插入图片描述
如果你需要对 ImmutableValue 类的实例进行操作,可以通过得到 value 变量后创建一个新的实例来实现,下面是一个对 value 变量进行加法操作的示例:

public class ImmutableValue{ 

    private int value = 0; 
    public ImmutableValue(int value){
        this.value = value;
    } 

    public int getValue(){
        return this.value;
    } 

    public ImmutableValue add(int valueToAdd){        
        return new ImmutableValue(this.value + valueToAdd);
    }
}

请注意 add() 方法以加法操作的结果作为一个新的 ImmutableValue 类实例返回,而不是直接对它自己的 value 变量进行操作。

**引用可能不是线程安全的!**重要的是要记住,即使一个对象是线程安全的不可变对象,指向这个对象的引用也可能不是线程安全的。看这个例子:

//可以参考引用类型的局部变量:currentValue 这个引用是被共享的,故线程不安全
public void Calculator{
 
    private ImmutableValue currentValue = null;
 
    public ImmutableValue getValue(){
        return currentValue;//ImmutableValue不可变,但该引用指向的对象可以被其他方法获得
    }
 
    public void setValue(ImmutableValue newValue){
        this.currentValue = newValue; //ImmutableValue不可变,但该引用可以被其他方法修改
    }
 
    public void add(int newValue){
        this.currentValue = this.currentValue.add(newValue);
    }
}

Calculator 类持有一个指向 ImmutableValue 实例的引用。注意,通过 setValue() 方法和 add() 方法可能会改变这个引用。因此,即使 Calculator 类内部使用了一个不可变对象,但 Calculator 类本身还是可变的,因此 Calculator 类不是线程安全的。换句话说:ImmutableValue 类是线程安全的,但使用它的类不是。当尝试通过不可变性去获得线程安全时,这点是需要牢记的。

要使 Calculator 类实现线程安全,需要将 getValue()setValue()add() 方法都声明为同步方法

3. 重点知识

1. 等待/唤醒机制

等待/唤醒机制需要在同步函数或同步代码块中使用

一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是 “生产者”,后者是 “消费者”,在功能层面实现了解耦

Object 中的三个方法:

  1. wait():让当前线程进入等待状态,同时,wait() 也会让当前线程释放它所持有的锁
  2. notify() 和 notifyAll():则是唤醒当前对象上的等待线程
    1. notify() 是唤醒单个线程
    2. notifyAll() 是唤醒所有的线程

详细:

  1. notify():唤醒在此对象监视器上等待的单个线程,使其从 wait() 方法返回,而返回的前提是该线程获取到了对象的锁(被唤醒后会先进入同步队列争锁,获得锁后才会返回方法调用点继续执行)
  2. notifyAll():唤醒在此对象监视器上等待的所有线程;
  3. wait():让当前线程处于 “等待(阻塞)状态”,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,当前线程被唤醒(进入 “就绪状态”)。需要注意的是:调用 wait() 方法后,会释放对象的锁;
  4. wait(long timeout):让当前线程处于 “等待(阻塞)状态”,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者超过指定的时间量,当前线程被唤醒(进入 “就绪状态”)。
  5. wait(long timeout, int nanos):让当前线程处于 “等待(阻塞)状态”,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量(timeout + nanos),当前线程被唤醒(进入 “就绪状态”)。其实就是对于超时时间更细粒度的控制;

等待唤醒机制的含义:

等待/唤醒机制是指一个线程 A 调用了对象 O 的 wait() 方法进入等待状态,而另一个线程 B 调用了对象 O 的 notify() 或者 notifyAll() 方法,线程 A 收到通知后从对象 O 的 wait() 方法返回,进而执行后续操作

两个线程通过对象 O 来完成交互,而对象上的 wait()notify/notifyAll() 的关系就如同开关信号一样,用来完成 等待方和通知方之间 的交互工作

示例:
在这里插入图片描述

任务:

  • 当 input 发现 Resource 中没有数据时,开始输入,输入完成后,叫 output 来输出;如果发现有数据,就wait()
  • 当 output 发现 Resource 中没有数据时,就 wait() ;当发现有数据时,就输出,然后,唤醒 input 来输入数据

这个问题的关键在于,如何实现交替进行

public class Resource{

    private String name;
    private String sex;
    private boolean flag = false;

    public synchronized void set(String name, String sex){	
        // 如果flag为true,证明Resource还没有输出,则进入等待状态
        if(flag){
            try {
                wait();   // 被this对象调用, 等待消费者消费
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 设置成员变量
        this.name = name;
        this.sex = sex;
        System.out.println("输入的是:The name is : " + name + "&& The sex is :" + sex);
        // 设置之后,Resource中有值,相当于生产出了新的Resource对象,将flag设置为true
        flag = true;
        // 唤醒output线程,进行数据的写出,即消费
        this.notify();
    }

    public synchronized void get(){
        // 如果没有了Resource对象,则进入等待状态
        if(!flag){
            try {
                wait();   // 等待生产者生产
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // out线程将数据输出
        System.out.println("输出的是:The name is : " + name + "&& The sex is :" + sex);
        // 改变标记,以便input线程输入数据
        flag = false;
        // 唤醒input线程,进行数据的写入,即生产
        this.notify();
    }
}

public class Input implements Runnable {

    private Resource res;

    public Input(Resource res){
        this.res = res;
    }

    @Override
    public void run() {
        int count = 0;
        while(true){
            if(count == 0){
                res.set("Tom", "man");   // 生产数据
            }else{
                res.set("Lily", "woman");
            }
            // 在两个数据之间进行切换
            count = (count + 1) % 2;
        }
    }
}

public class Output implements Runnable {

    private Resource res;

    public Output(Resource res){
        this.res = res;
    }

    @Override
    public void run() {
        while(true){
            res.get();  // 消费数据
        }
    }
}

public class ResourceTest {

    public static void main(String[] args) {

        // 资源对象
        Resource res = new Resource();

        // 任务对象,同一个res
        Input in = new Input(res);
        Output out = new Output(res);

        // 线程对象
        Thread t1 = new Thread(in);   // 输入线程
        Thread t2 = new Thread(out);  // 输出线程

        // 开启线程
        t1.start();
        t2.start();
    }
}

在这里插入图片描述
上面的这个例子是典型的生产者和消费者案例。只不过是单生产者和单消费者案例。

注意:

  1. 调用 wait() 、notify()和notifyAll() 时需要先对调用对象加锁;

  2. 调用 wait() 方法后,线程状态由 RUNNING 变为 WAITING,并将当前线程放置到对象的等待队列;

  3. notify()notifyAll() 方法调用后,等待线程依旧不会从 wait() 返回,需要调用 notify()notifyAll() 的线程释放锁之后,等待线程才有机会wait() 返回;

  4. notify() 方法将等待队列中的一个等待线程从等待队列中移动到同步队列中,而 notifyAll() 方法则是将等待队列中所有的线程全部移动到同步队列,被移动的线程状态由 WAITING 变为 BLOCKED

  5. wait() 方法返回的前提是获得了调用对象的锁

  6. 等待/唤醒机制依赖于同步机制,其目的就是确保等待线程从 wait() 方法返回时能够感知到通知线程对变量做出的修改

1. 为什么 notify() 、wait() 等方法定义在了 Object 类中,而不是 Thread 类中呢?

Object 中的 wait(), notify() 等函数,和 synchronized 一样,会对 “对象的同步锁” 进行操作。wait() 会使 “当前线程” 等待,因为线程进入等待状态,所以线程应该释放它所持有的 “同步锁”,否则其它线程就会因为获取不到该 “同步锁” 而无法运行

线程调用 wait() 之后,会释放它所持有的 “同步锁”;而且,根据前面的介绍,我们知道:等待线程可以被notify() 或 notifyAll() 唤醒。现在,请思考一个问题:notify() 是依据什么唤醒等待线程的?或者说,wait() 等待线程和 notify() 之间是通过什么关联起来的?答案是:依据 “对象的同步锁”

负责唤醒等待线程的那个线程(我们称为 “唤醒线程”),它只有在获取 “该对象的同步锁” (这里的同步锁必须和等待线程的同步锁是同一个),并且调用 notify() 或 notifyAll() 方法之后,才能唤醒等待线程。虽然,等待线程被唤醒;但是,它不能立刻执行,因为唤醒线程还持有 “该对象的同步锁”。必须等到唤醒线程释放了 “对象的同步锁” 之后,等待线程才有可能获取到 “对象的同步锁” 进而继续运行

总之,notify(), wait() 依赖于 “同步锁”,而 “同步锁” 是对象持有的,并且每个对象有且仅有一个!这就是为什么 notify(), wait() 等函数定义在 Object 类,而不是 Thread 类中的原因

2. 线程中断

线程中断是线程的标志位属性。而不是真正终止线程,和线程的状态无关。线程中断过程表示一个运行中的线程,通过其他线程(例如 main 线程)调用了该线程的 interrupt() 方法,使得该线程中断标志位属性改变。

深入思考下,线程中断不是去中断了线程,恰恰是用来通知该线程应该被中断了。具体是一个标志位属性。至于到底该线程生命周期是终止,还是继续运行,由线程根据标志位属性自行处理

  • interrupt(): 方法只是改变中断状态而已,它不会中断一个正在运行的线程。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程就得以退出阻塞的状态。更确切的说,如果线程被 Object.wait, Thread.join 和 Thread.sleep 三种方法之一阻塞,此时调用该线程的 interrupt() 方法,那么该线程将抛出一个 InterruptedException 中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态(即该线程的阻塞状态将会被清除)。如果线程没有被阻塞,这时调用 interrupt() 将不起作用,直到执行到 wait(),sleep(),join() 时,才马上会抛出 InterruptedException 异常

    public class InterruptTest {
        public static void main(String[] args) {
            for(int i=0;i<5;i++){
                Thread thread=new Thread( new InterruptThread());
                thread.start();
                thread.interrupt();
            }
        }
    
        static class InterruptThread implements Runnable  {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    System.out.println("线程处于阻塞状态时,中断线程,就会抛出异常。");
                    e.printStackTrace();
                }
            }
        }
    }
    
  • interrupted():检测当前线程是否被中断,如果调用成功(返回 true),中断状态会被清除(即重置为false),由于它是静态方法,因此不能在特定的线程上使用,只能报告调用它的线程的中断状态,如果该方法被调用两次,则第二次一般是返回 false,如果线程不存活,则返回 false

    public class Test31 {
        public static void main(String[] args) {
            System.out.println("1: " + Thread.interrupted());//注意:不能在特定的线程上使用
            Thread.currentThread().interrupt(); 
            System.out.println("2: " + Thread.interrupted());
            System.out.println("3: " + Thread.interrupted());
        }
    }
    
    //结果
    1false
    2true
    3false
    
  • isInterrupted():检测调用该方法的线程是否被中断,中断状态不会被清除。线程一旦被中断,该方法返回true,而一旦遇到 sleep() 等方法他将抛出异常,并清除中断状态,此时方法将返回 false

    public class Test30 {
        public static void main(String[] args) {
            Thread t = Thread.currentThread();
            System.out.println("1: " + t.isInterrupted());
            t.interrupt();
            System.out.println("2: " + t.isInterrupted());
            System.out.println("3: " + t.isInterrupted());
            try {
                Thread.sleep(2000); //这里抛出异常,所以后面的代码不会执行
                System.out.println("not interrted..."); //不会执行
            } catch (Exception e) { //捕获到异常后执行,并执行catch块之后的非catch块代码
                System.out.println("interrupted...");
                System.out.println("4: " + t.isInterrupted());
            }
            System.out.println("5: " + t.isInterrupted());
        }
    }
    
    //结果
    1false
    2true
    3true
    interrupted...
    4false
    5:fasle
    
    

3. 线程终止

比如在 IDEA 中强制关闭程序,立即停止程序,不给程序释放资源等操作,肯定是不正确的。线程终止也存在类似的问题,所以需要考虑如何终止线程?

上面提到的中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个 boolean 变量来控制是否需要停止任务并终止该线程

案例如下代码所示:

public class ThreadSafeStop {

    public static void main(String[] args) throws Exception {
        Runner run1 = new Runner();
        Thread countThread = new Thread(run1, "CountThread");
        countThread.start();
        // 睡眠1秒后,通知CountThread中断,并终止线程
        TimeUnit.SECONDS.sleep(1);
        countThread.interrupt();

        Runner run2 = new Runner();
        countThread = new Thread(run2,"CountThread");
        countThread.start();
        // 睡眠 1 秒,然后设置线程停止状态,并终止线程
        TimeUnit.SECONDS.sleep(1);
        run2.stopSafely();
    }

    // Runner:静态内部类
    private static class Runner implements Runnable{
        
        private long i;

        // 线程状态变量
        private volatile boolean on = true;

        @Override
        public void run() {
            while(on && !Thread.currentThread().isInterrupted()){
                // 线程执行的具体逻辑
                i++;
            }
            System.out.println("Count i = " + i);
        }

        public void stopSafely(){
            on = false;
        }
    }
}

从上面代码可以看出,通过 while(on && !Thread.currentThread().isInterrupted()) 代码来实现线程是否跳出执行逻辑,并终止。但是疑问点就来了,为啥需要on和isInterrupted()两项一起呢?用其中一个方式不就行了吗?答案是:

  1. 线程成员变量 on 通过 volatile 关键字修饰,达到线程之间可见,从而实现线程的终止。但当线程状态为被阻塞状态(sleep、wait、join 等状态)时,对成员变量操作也阻塞,进而无法执行安全终止线程;
  2. 为了处理上面的问题,引入了 isInterrupted() 去解决阻塞状态下的线程安全终止;
  3. 两者结合是真的没问题了吗?不是的,如果是网络 io 阻塞,比如一个 websocket 一直再等待响应,那么直接使用底层的 close()

4. 线程上下文切换

  1. 多线程会共同使用一组计算机上的 CPU,当线程数大于给程序分配的 CPU 数量时,为了让各个线程都有执行的机会,就需要轮转使用 CPU 。在运行一个线程的过程中转去运行另外一个线程,这个就叫做线程上下文切换(对于进程也是类似)
  2. 由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存当前线程的运行状态,以便下次重新切换回来时能够继续以切换之前的状态运行。举个简单的例子:比如一个线程 A 正在读取一个文件的内容,正读到文件的一半,此时需要暂停线程 A,转去执行线程 B,当再次切换回来继续执行线程 A 的时候,我们并不希望线程 A 又从文件的开头开始读取,所以这时就需要记录切换时线程 A 的执行状态
    1. 线程切换时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值
    2. 另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值是多少,因此需要记录 CPU 寄存器的状态
  3. 所以一般来说,线程上下文切换过程中会记录程序计数器 、CPU 寄存器状态等数据

5. 线程休眠

  1. sleep() 的作用是让当前线程休眠,让出 CPU 时间片(即不占用 CPU),但仍然持有对象锁,然后当前线程会从 “运行状态” 进入到 “休眠(阻塞)状态”。等线程重新被唤醒时,它会由 “阻塞状态” 变成 “就绪状态”,从而等待 CPU 的调度执行

  2. 当 timeout = 0, 即 sleep(0),如果线程调度器的可运行队列中有大于或等于当前线程优先级的就绪线程存在,操作系统会将当前线程从处理器上移除,调度并运行其他优先级高的就绪线程;如果可运行队列中的没有就绪线程或所有就绪线程的优先级均低于当前线程优先级,那么当前线程会继续执行, 就像没有调用 sleep(0) 一样

  3. 当 timeout > 0 时,如:sleep(1),会引发线程上下文切换:调用线程会从线程调度器的可运行队列中被移除一段时间,这个时间段 约等于 timeout 所指定的时间长度。为什么说约等于呢?是因为睡眠时间单位为毫秒,这与系统的时间精度有关。通常情况下,系统的时间精度 为 10 ms,那么指定任意少于 10 ms 但大于 0 ms 的睡眠时间,均会向上求值为 10 ms。

  4. 调用 switchToThread() 方法,如果当前有其他就绪线程在线程调度器的可运行队列中,始终会让出一个时间切片给这些就绪线程,而不管就绪线程的优先级的高低与否

  5. 所以,如果我们想让当前线程真正睡眠一下子,最好是调用 sleep(timeOut > 0)switchToThread()

6. 线程让步

yield():执行此方法会向系统线程调度器(Schelduler)发出一个暗示,告诉其当前 Java 线程打算放弃对 CPU 的使用,但该暗示有可能被调度器忽略。使用该方法,可以防止线程对 CPU 的过度使用,提高系统性能

  • Java 线程中的 Thread.yield( ) 方法,译为线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己 CPU 执行权让出来,让自己或者其它的线程获取,注意是让自己或者其他线程获取,并不是单纯的让给其他线程。
  • yield() 的作用是让步。它能让当前线程由 “运行状态” 进入到 “就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用 yield() 之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到 “运行状态” 继续运行
public class Thread_Yield extends Thread {

    public Thread_Yield(String name){
        super(name);
    }

    public synchronized void run(){
        for(int i = 0; i < 10; i++){
            System.out.printf("%s [%d]:%d\n", this.getName(), this.getPriority(), i);
            // i整除4时,调用yield()
            if(i % 4 == 0){
                Thread.yield();
            }
        }
    }

    public static void main(String[] args) {

        Thread_Yield t1 = new Thread_Yield("t1");
        Thread_Yield t2 = new Thread_Yield("t2");
        t1.start();
        t2.start();
    }
}

在这里插入图片描述
“线程 t1” 在能被 4 整数的时候,并没有切换到 “线程 t2”。这表明,yield() 虽然可以让线程由 “运行状态” 进入到 “就绪状态”;但是,它不一定会让其它线程获取 CPU 执行权(即,其它线程进入到 “运行状态”),即使这个 “其它线程” 与当前调用 yield() 的线程具有相同的优先级

7. join 方法

join() & join(time):A 线程调用 B 线程的 join() 方法,将会使 A 等待 B 执行,直到 B 线程终止。如果传入time 参数,将会使 A 等待 B 执行 time 的时间,如果 time 时间到达或者 B 提前执行完毕,将会切换回 A 线程,继续执行 A 线程

  1. 在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程将早于子线程结束。这时,如果主线程想等子线程执行完成才结束,比如子线程处理一个数据,主线程想要获得这个数据中的值,就要用到 join() 方法了。方法 join() 的作用是等待线程对象销毁
  2. join() 方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。在 A 线程中调用了 B 线程的 join() 方法时,表示只有当 B 线程执行完毕后,A 线程才能继续执行。
  3. join() 方法中如果传入参数,则表示:如果 A 线程中调用了 B 线程的 join(10),则 A 线程会等待 B 线程执行 10 毫秒,10 毫秒过后,A、B 线程并行执行。
  4. 需要注意的是,JDK 规定,join(0) 的意思不是 A 线程等待 B 线程 0 秒,而是 A 线程等待 B 线程无限时间,直到 B 线程执行完毕,即 join(0) 等价于 join()。(其实 join() 底层中调用的就是 join(0) )
  5. join() 方法必须在线程 start() 方法调用之后调用才有意义。这个也很容易理解:如果一个线程都没有 start,那它也就无法同步了
// 主线程
public class Father extends Thread {
    public void run() {
        Son s = new Son();
        s.start();
        s.join();
        ...
    }
}
// 子线程
public class Son extends Thread {
    public void run() {
        ...
    }
}

上面的有两个类 Father (主线程类)和 Son (子线程类)。因为 Son 是在 Father 中创建并启动的,所以,Father 是主线程类,Son 是子线程类

在 Father 主线程中,通过 new Son() 新建 “子线程 s”。接着通过 s.start() 启动 “子线程 s”,并且调用 s.join()。在调用 s.join() 之后,Father 主线程会一直等待,直到 “子线程 s” 运行完毕;在 “子线程 s” 运行完毕之后,Father 主线程才能接着运行。 这也就是我们所说的 join() 的作用:让主线程会等待子线程结束之后才能继续运行

JDK 1.7 中 join() 源码:

public final void join() throws InterruptedException {
    join(0);
}

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");
    }

    // 当millis等于0的时候,判断子线程是否是活的
    if (millis == 0) {
        while (isAlive()) {
            wait(0);  // 如果是活的,就无限等待下去
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;   
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值