Java 线程 攻略

255 阅读51分钟

攻略大全

1. 粘贴攻略

进程是程序运行资源分配的最小单位

进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、 磁盘 IO 等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程 之间是相互独立的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。

进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一 个进程。显然,程序是死的、静态的,进程是活的、动态的。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。

线程是 CPU 调度的最小单位,必须依赖于进程而存在。

线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的、 能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

1.1 进程的基本原理

在计算机中,CPU是核心的硬件资源,承担了所有的计算任务;内存资源承担了运行时数据的保存任务;外存资源(硬盘等)承担了数据外部永久存储的任务。

其中,计算任务的调度、资源的分配由操作系统来统领。

应用程序以进程的形式运行于操作系统之上,享受操作系统提供的服务。

进程的定义一直以来没有完美的标准。一般来说,一个进程由程序段、数据段和程序控制块三部分组成。进程的大致结构如图所示:

image.png

程序段一般也被称为代码段。代码段是进程的程序指令在内存中的位置,包含需要执行的指令集合。

数据段是进程的操作数据在内存中的位置,包含需要操作的数据集合。

程序控制块(Program Control Block,PCB)包含进程的描述信息和控制信息,是进程存在的唯一标志

PCB主要由四大部分组成:

  • (1)进程的描述信息。

    主要包括:进程ID和进程名称,进程ID是唯一的,代表进程的身份;进程状态,比如运行、就绪、阻塞;进程优先级,是进程调度的重要依据。

  • (2)进程的调度信息。

    主要包括:程序起始地址,程序的第一行指令的内存地址,从这里开始程序的执行;通信信息,进程间通信时的消息队列。

  • (3)进程的资源信息。

    主要包括:内存信息,内存占用情况和内存管理所用的数据结构;I/O设备信息,所用的I/O设备编号及相应的数据结构;文件句柄,所打开文件的信息。

  • (4)进程上下文。

    主要包括执行时各种CPU寄存器的值、当前程序计数器(PC)的值以及各种栈的值等,即进程的环境。在操作系统切换进程时,当前进程被迫让出CPU,当前进程的上下文就保存在PCB结构中,供下次恢复运行时使用。

现代操作系统中,进程是并发执行的,任何进程都可以同其他进程一起执行。在进程内部,代码段和数据段有自己的独立地址空间,不同进程的地址空间是相互隔离的。

Java编写的程序都运行在Java虚拟机(JVM)中,每当使用Java命令启动一个Java应用程序时,就会启动一个JVM进程。在这个JVM进程内部,所有Java程序代码都是以线程来运行的。JVM找到程序的入口点main()方法,然后运行main()方法,这样就产生了一个线程,这个线程被称为主线程。当main()方法结束后,主线程运行完成,JVM进程也随即退出。

1.2 线程的基本原理

为了充分发挥CPU的计算性能,提升CPU硬件资源的利用率,同时弥补进程调度过于笨重产生的问题,进程内部演进出了并发调度的诉求,于是就发明了线程。

线程是指“进程代码段”的一次顺序执行流程。线程是CPU调度的最小单位。一个进程可以有一个或多个线程,各个线程之间共享进程的内存空间、系统资源,进程仍然是操作系统资源分配的最小单位。

Java程序的进程执行过程就是标准的多线程的执行过程。每当使用Java命令执行一个class类时,实际上就是启动了一个JVM进程。理论上,在该进程的内部至少会启动两个线程,一个是main线程,另一个是GC(垃圾回收)线程。实际上,执行一个Java程序后,通过Process Explorer来观察,线程数量远远不止两个,达到了18个之多。

一个标准的线程主要由三部分组成,即线程描述信息、程序计数器(Program Counter,PC)和栈内存,如图示:

image.png

在线程的结构中,线程描述信息即线程的基本信息,主要包括:

  • (1)线程ID(Thread ID,线程标识符)。线程的唯一标识,同一个进程内不同线程的ID不会重叠。
  • (2)线程名称。主要是方便用户识别,用户可以指定线程的名字,如果没有指定,系统就会自动分配一个名称。
  • (3)线程优先级。表示线程调度的优先级,优先级越高,获得CPU的执行机会就越大。
  • (4)线程状态。表示当前线程的执行状态,为新建、就绪、运行、阻塞、结束等状态中的一种。
  • (5)其他。例如是否为守护线程等。

在线程的结构中,栈内存是代码段中局部变量的存储空间,为线程所独立拥有,在线程之间不共享。在JDK 1.8中,每个线程在创建时默认被分配1MB大小的栈内存。栈内存和堆内存不同,栈内存不受垃圾回收器管理。

在Java中,执行程序流程的重要单位是“方法”,而栈内存的分配单位是“栈帧”(或者叫“方法帧”)。方法的每一次执行都需要为其分配一个栈帧(方法帧),栈帧主要保存该方法中的局部变量、方法的返回地址以及其他方法的相关信息。当线程的执行流程进入方法时,JVM就会为方法分配一个对应的栈帧压入栈内存;当线程的执行流程跳出方法时,JVM就从栈内存弹出该方法的栈帧,此时方法帧的局部变量的内存空间就会被回收。

正是由于栈帧(方法帧)的操作是后进先出的模式,这也是标准的栈操作模式,因此存放方法帧的内存也被叫作栈内存。

1.3 进程与线程的区别

(1)线程是“进程代码段”的一次顺序执行流程。一个进程由一个或多个线程组成,一个进程至少有一个线程。

(2)线程是CPU调度的最小单位,进程是操作系统分配资源的最小单位。线程的划分尺度小于进程,使得多线程程序的并发性高。

(3)线程是出于高并发的调度诉求从进程内部演进而来的。线程的出现既充分发挥了CPU的计算性能,又弥补了进程调度过于笨重的问题。

(4)进程之间是相互独立的,但进程内部的各个线程之间并不完全独立。各个线程之间共享进程的方法区内存、堆内存、系统资源(文件句柄、系统信号等)。

(5)切换速度不同:线程上下文切换比进程上下文切换要快得多。所以,有的时候,线程也称为轻量级进程。

1.4 创建线程的4种方法

虽然一个进程有很多个线程,但是在一个CPU内核上,同一时刻只能有一个线程是正在执行的,该线程也被叫作当前线程。

1.4.1 Thread类详解

1.线程ID

属性:private long tid,此属性用于保存线程的ID。这是一个private类型的属性,外部只能使用getId()方法访问线程的ID。

方法:public long getId(),获取线程ID,线程ID由JVM进行管理,在进程内唯一。

2.线程名称

属性:private String name,该属性保存一个Thread线程实例的名字。

方法一:public final String getName(),获取线程名称。

方法二:public final void setName(String name),设置线程名称。

方法三:Thread(String threadName),通过此构造方法给线程设置一个定制化的名字。

3.线程优先级

属性:private int priority,保存一个Thread线程实例的优先级。

方法一:public final int getPriority(),获取线程优先级。

方法二:public final void setPriority(int priority),设置线程优先级。

Java线程的最大优先级值为10,最小值为1,默认值为5。这三个优先级值为三个常量值,在Thread类中使用类常量定义,三个类常量如下:

Image [20].png

4.是否为守护线程

属性:private boolean daemon=false,该属性保存Thread线程实例的守护状态,默认为false,表示是普通的用户线程,而不是守护线程。

方法:public final void setDaemon(boolean on),将线程实例标记为守护线程或用户线程,如果参数值为true,那么将线程实例标记为守护线程。

守护线程是在进程运行时提供某种后台服务的线程,比如垃圾回收(GC)线程。

5.线程的状态

属性:private int threadStatus,该属性以整数的形式保存线程的状态。

方法:public Thread.State getState(),返回表示当前线程的执行状态,为新建、就绪、运行、阻塞、结束等状态中的一种。

Thread的内部静态枚举类State用于定义Java线程的所有状态,具体如下:

Image [21].png

在Java线程的状态中,就绪状态和运行状态在内部用同一种状态RUNNABLE表示。就绪状态表示线程具备运行条件,正在等待获取CPU时间片;运行状态表示线程已经获取了CPU时间片,CPU正在执行线程代码逻辑。

6.线程的启动和运行

方法一:public void start(),用来启动一个线程,当调用start()方法后,JVM才会开启一个新的线程来执行用户定义的线程代码逻辑,在这个过程中会为相应的线程分配需要的资源。

方法二:public void run(),作为线程代码逻辑的入口方法。run()方法不是由用户程序来调用的,当调用start()方法启动一个线程之后,只要线程获得了CPU执行时间,便进入run()方法体去执行具体的用户线程代码。

总之,这两个方法非常重要,start()方法用于线程的启动,run()方法作为用户代码逻辑的执行入口。

7.取得当前线程

方法:public static Thread currentThread(),该方法是一个非常重要的静态方法,用于获取当前线程的Thread实例对象。

什么是当前线程呢?就是当前在CPU上执行的线程。在没有其他的途径获取当前线程的实例对象的时候,可以通过Thread.currentThread()静态方法获取。

1.4.2 创建线程

1.4.2.1 方法一:继承Thread类创建线程类

(1)需要继承Thread类,创建一个新的线程类。

(2)同时重写run()方法,将需要并发执行的业务代码编写在run()方法中。

1.4.2.2 方法二:实现Runnable接口创建线程目标类

在Thread类的run()方法中,如果target(执行目标)不为空,就执行target属性的run()方法。而target属性是Thread类的一个实例属性,并且target属性的类型为Runnable。

Image [22].png

Thread类有一系列的构造器,其中有多个构造器可以为target属性赋值,这些构造器包括如下两个:

  • (1)public Thread(Runnable target)
  • (2)public Thread(Runnable target,String name)

使用这两个构造器传入target执行目标实例(Runnable实例),就可以直接通过Thread类的run()方法以默认方式实现,达到线程并发执行的目的。在这种场景下,可以不通过继承Thread类实现线程类的创建。

1.Runnable接口 Image [23].png Runnable有且仅有一个抽象方法——void run(),代表被执行的用户业务逻辑的抽象,在使用的时候,将用户业务逻辑编写在Runnable实现类的run()的实现版本中。当Runnable实例传入Thread实例的target属性后,Runnable接口的run()的实现版本将被异步调用。

2.通过实现Runnable接口创建线程类

创建线程的第二种方法就是实现Runnable接口,将需要异步执行的业务逻辑代码放在Runnable实现类的run()方法中,将Runnable实例作为target执行目标传入Thread实例。该方法的具体步骤如下:

  • (1)定义一个新类实现Runnable接口。
  • (2)实现Runnable接口中的run()抽象方法,将线程代码逻辑存放在该run()实现版本中。
  • (3)通过Thread类创建线程对象,将Runnable实例作为实际参数传递给Thread类的构造器,由Thread构造器将该Runnable实例赋值给自己的target执行目标属性。
  • (4)调用Thread实例的start()方法启动线程。
  • (5)线程启动之后,线程的run()方法将被JVM执行,该run()方法将调用target属性的run()方法,从而完成Runnable实现类中业务代码逻辑的并发执行。

通过实现Runnable接口的方式创建的执行目标类,如果需要访问线程的任何属性和方法,必须通过Thread.currentThread()获取当前的线程对象,通过当前线程对象间接访问。

1.4.2.2.1 优雅创建Runnable线程目标类的两种方式

1.通过匿名类优雅地创建Runnable线程目标类

在实现Runnable编写target执行目标类时,如果target实现类是一次性类,可以使用匿名实例的形式。

2.使用Lambda表达式优雅地创建Runnable线程目标类

1.4.2.2.2 通过实现Runnable接口的方式创建线程目标类的优缺点

通过实现Runnable接口的方式创建线程目标类有以下缺点:

  • (1)所创建的类并不是线程类,而是线程的target执行目标类,需要将其实例作为参数传入线程类的构造器,才能创建真正的线程。
  • (2)如果访问当前线程的属性(甚至控制当前线程),不能直接访问Thread的实例方法,必须通过Thread.currentThread()获取当前线程实例,才能访问和控制当前线程。

通过实现Runnable接口的方式创建线程目标类有以下优点:

  • (1)可以避免由于Java单继承带来的局限性。如果异步逻辑所在类已经继承了一个基类,就没有办法再继承Thread类。比如,当一个Dog类继承了Pet类,再要继承Thread类就不行了。所以在已经存在继承关系的情况下,只能使用实现Runnable接口的方式。

  • (2)逻辑和数据更好分离。通过实现Runnable接口的方法创建多线程更加适合同一个资源被多段业务逻辑并行处理的场景。在同一个资源被多个线程逻辑异步、并行处理的场景中,通过实现Runnable接口的方式设计多个target执行目标类可以更加方便、清晰地将执行逻辑和数据存储分离,更好地体现了面向对象的设计思想。

1.4.2.3 方法三:使用Callable和FutureTask创建线程

继承Thread类或者实现Runnable接口来创建线程类,有一个共同的缺陷:不能获取异步执行的结果。

为了解决异步执行的结果问题,Java语言在1.5版本之后提供了一种新的多线程创建方法:通过Callable接口和FutureTask类相结合创建线程。

1.Callable接口

Image [24].png

Callable接口是一个泛型接口,也是一个“函数式接口”。其唯一的抽象方法call()有返回值,返回值的类型为Callable接口的泛型形参类型。call()抽象方法还有一个Exception的异常声明,容许方法的实现版本的内部异常直接抛出,并且可以不予捕获。

Callable接口类似于Runnable。不同的是,Runnable的唯一抽象方法run()没有返回值,也没有受检异常的异常声明。比较而言,Callable接口的call()有返回值,并且声明了受检异常,其功能更强大一些。

2.RunnableFuture接口

RunnableFuture接口实现了两个目标:一是可以作为Thread线程实例的target实例,二是可以获取异步执行的结果。

Image [25].png

通过源代码可以看出:RunnableFuture继承了Runnable接口,从而保证了其实例可以作为Thread线程实例的target目标;同时,RunnableFuture通过继承Future接口,保证了可以获取未来的异步执行结果。

3.Future接口

Image [26].png

Future接口至少提供了三大功能:

(1)能够取消异步执行中的任务。

(2)判断异步任务是否执行完成。

(3)获取异步任务完成后的执行结果。

  • V get():获取异步任务执行的结果。注意,这个方法的调用是阻塞性的。如果异步任务没有执行完成,异步结果获取线程(调用线程)会一直被阻塞,一直阻塞到异步任务执行完成,其异步结果返回给调用线程。

  • V get(Long timeout,TimeUnit unit):设置时限,(调用线程)阻塞性地获取异步任务执行的结果。该方法的调用也是阻塞性的,但是结果获取线程(调用线程)会有一个阻塞时长限制,不会无限制地阻塞和等待,如果其阻塞时间超过设定的timeout时间,该方法将抛出异常,调用线程可捕获此异常。

  • boolean isDone():获取异步任务的执行状态。如果任务执行结束,就返回true。

  • boolean isCancelled():获取异步任务的取消状态。如果任务完成前被取消,就返回true。

  • boolean cancel(boolean mayInterruptRunning):取消异步任务的执行。

4.FutureTask类

Image [27].png

FutureTask类实现了RunnableFuture接口,是真正的在Thread与Callable之间搭桥的类。

Image [28].png

从FutureTask类的UML关系图可以看到:FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承了Runnable接口和Future接口,所以FutureTask既能作为一个Runnable类型的target执行目标直接被Thread执行,又能作为Future异步任务来获取Callable的计算结果。

Image [29].png

callable实例属性用来保存并发执行的Callable<>类型的任务,并且callable实例属性需要在FutureTask实例构造时进行初始化。FutureTask类实现了Runnable接口,在其run()方法的实现版本中会执行callable成员的call()方法。

FutureTask的outcome实例属性用于保存callable成员call()方法的异步执行结果。在FutureTask类的run()方法完成callable成员的call()方法的执行之后,其结果将被保存在outcome实例属性中,供FutureTask类的get()方法获取。

5.使用Callable和FutureTask创建线程的具体步骤

通过FutureTask类和Callable接口的联合使用可以创建能够获取异步执行结果的线程,具体步骤如下:

  • (1)创建一个Callable接口的实现类,并实现其call()方法,编写好异步执行的具体逻辑,可以有返回值。
  • (2)使用Callable实现类的实例构造一个FutureTask实例。
  • (3)使用FutureTask实例作为Thread构造器的target入参,构造新的Thread线程实例。
  • (4)调用Thread实例的start()方法启动新线程,启动新线程的run()方法并发执行。其内部的执行过程为:启动Thread实例的run()方法并发执行后,会执行FutureTask实例的run()方法,最终会并发执行Callable实现类的call()方法。
  • (5)调用FutureTask对象的get()方法阻塞性地获得并发线程的执行结果。

Image [30].png

1.4.2.4 方法四:通过线程池创建线程

1.线程池的创建与执行目标提交

实际上创建一个线程实例在时间成本、资源耗费上都很高,在高并发的场景中,断然不能频繁进行线程实例的创建与销毁,而是需要对已经创建好的线程实例进行复用,这就涉及线程池的技术。Java中提供了一个静态工厂来创建不同的线程池,该静态工厂为Executors工厂类。

ExecutorService是Java提供的一个线程池接口,每次我们在异步执行target目标任务的时候,可以通过ExecutorService线程池实例去提交或者执行。ExecutorService实例负责对池中的线程进行管理和调度,并且可以有效控制最大并发线程数,提高系统资源的使用率,同时提供定时执行、定频执行、单线程、并发数控制等功能。

向ExecutorService线程池提交异步执行target目标任务的常用方法有: Image [31].png

2.线程池的使用实战

ExecutorService线程池的execute(...)与submit(...)方法的区别如下。

(1)接收的参数不一样 submit()可以接收两种入参:无返回值的Runnable类型的target执行目标实例和有返回值的Callable类型的target执行目标实例。而execute()仅仅接收无返回值的target执行目标实例,或者无返回值的Thread实例。

(2)submit()有返回值,而execute()没有 submit()方法在提交异步target执行目标之后会返回Future异步任务实例,以便对target的异步执行过程进行控制,比如取消执行、获取结果等。execute()没有任何返回,target执行目标实例在执行之后没有办法对其异步执行过程进行控制,只能任其执行,直到其执行结束。

1.5 线程的核心原理

1.5.1 线程的调度与时间片

由于CPU的计算频率非常高,每秒计算数十亿次,因此可以将CPU的时间从毫秒的维度进行分段,每一小段叫作一个CPU时间片。对于不同的操作系统、不同的CPU,线程的CPU时间片长度都不同。

目前操作系统中主流的线程调度方式是:基于CPU时间片方式进行线程调度。

线程只有得到CPU时间片才能执行指令,处于执行状态,没有得到时间片的线程处于就绪状态,等待系统分配下一个CPU时间片。由于时间片非常短,在各个线程之间快速地切换,因此表现出来的特征是很多个线程在“同时执行”或者“并发执行”。

线程的调度模型目前主要分为两种:分时调度模型和抢占式调度模型。

(1)分时调度模型:系统平均分配CPU的时间片,所有线程轮流占用CPU,即在时间片调度的分配上所有线程“人人平等”。

Image [32].png

(2)抢占式调度模型:系统按照线程优先级分配CPU时间片。优先级高的线程优先分配CPU时间片,如果所有就绪线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些。

1.5.2 线程的优先级

(1)整体而言,高优先级的线程获得的执行机会更多。

(2)执行机会的获取具有随机性,优先级高的不一定获得的机会多。

在 Java 线程中,通过一个整型成员变量 priority 来控制优先级,优先级的范 围从 1~10,在线程构建的时候可以通过 setPriority(int)方法来修改优先级,默认 优先级是 5,优先级高的线程分配时间片的数量要多于优先级低的线程。

设置线程优先级时,针对频繁阻塞(休眠或者 I/O 操作)的线程需要设置较 高优先级,而偏重计算(需要较多 CPU 时间或者偏运算)的线程则设置较低的 优先级,确保处理器不会被独占。在不同的 JVM 以及操作系统上,线程规划会 存在差异,有些操作系统甚至会忽略对线程优先级的设定。

1.5.3 线程的生命周期

1.NEW

Java源码对NEW状态的说明是:创建成功但是没有调用start()方法启动的Thread线程实例都处于NEW状态。

当然,并不是Thread线程实例的start()方法一经调用,其状态就从NEW状态到RUNNABLE状态,此时并不意味着线程立即获取CPU时间片并且立即执行,中间需要一系列操作系统的内部操作。

2.RUNNABLE

JVM的幕后工作和操作系统的线程调度有关。Java中的线程管理是通过JNI本地调用的方式委托操作系统的线程管理API完成的。当Java线程的Thread实例的start()方法被调用后,操作系统中的对应线程进入的并不是运行状态,而是就绪状态,而Java线程并没有这个就绪状态。操作系统中线程的就绪状态是什么状态的呢?

Image [33].png

一个操作系统线程如果处于就绪状态,就表示“万事俱备,只欠东风”,即该线程已经满足执行条件,但是还不能执行。处于就绪状态的线程需要等待系统的调度,一旦就绪状态被系统选中,获得CPU时间片,线程就开始占用CPU,开始执行线程的代码,这时线程的操作系统状态发生了改变,进入了运行状态。

在操作系统中,处于运行状态的线程在CPU时间片用完之后,又回到就绪状态,等待CPU的下一次调度。就这样,操作系统线程在就绪状态和执行状态之间被系统反复地调度,这种情况会一直持续,直到线程的代码逻辑执行完成或者异常终止。这时线程的操作系统状态又发生了改变,进入线程的最后状态——TERMINATED状态。

就绪状态和运行状态都是操作系统中的线程状态。在Java语言中,并没有细分这两种状态,而是将这两种状态合并成同一种状态——RUNNABLE状态。因此,在Thread.State枚举类中,没有定义线程的就绪状态和运行状态,只是定义了RUNNABLE状态。这就是Java线程状态和操作系统中线程状态不同的地方。

总之,NEW状态的Thread实例调用了start()方法后,线程的状态将变成RUNNABLE状态。尽管如此,线程的run()方法不一定会马上被并发执行,需要在线程获取了CPU时间片之后才真正启动并发执行。

3.BLOCKED

4.WAITING

5.TIME_WAITING

线程处于一种特殊的等待状态,准确地说,线程处于限时等待状态。能让线程处于限时等待状态的操作大致有以下几种:

  • (1)Thread.sleep(int n):使得当前线程进入限时等待状态,等待时间为n毫秒。
  • (2)Object.wait():带时限的抢占对象的monitor锁。
  • (3)Thread.join():带时限的线程合并。
  • (4)LockSupport.parkNanos():让线程等待,时间以纳秒为单位。
  • (5)LockSupport.parkUntil():让线程等待,时间可以灵活设置。

6.TERMINATED 处于RUNNABLE状态的线程在run()方法执行完成之后就变成终止状态TERMINATED了。当然,如果在run()方法执行过程中发生了运行时异常而没有被捕获,run()方法将被异常终止,线程也会变成TERMINATED状态。

1.5.4 使用Jstack工具查看线程状态

Jstack工具是Java虚拟机自带的一种堆栈跟踪工具。Jstack用于生成或导出(DUMP)JVM虚拟机运行实例当前时刻的线程快照。线程快照是当前JVM实例内每一个线程正在执行的方法堆栈的集合,生成或导出线程快照的主要目的是定位线程出现长时间运行、停顿或者阻塞的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。线程出现停顿的时候通过Jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。

Jstack指令所输出的信息中包含以下重要信息: (1)tid:线程实例在JVM进程中的id。 (2)nid:线程实例在操作系统中对应的底层线程的线程id。 (3)prio:线程实例在JVM进程中的优先级。 (4)os_prio:线程实例在操作系统中对应的底层线程的优先级。 (5)线程状态:如runnable、waiting on condition等。

1.6 线程的基本操作

1.6.1 线程名称的设置和获取

关于线程名称有以下几个要点: (1)线程名称一般在启动线程前设置,但也允许为运行的线程设置名称。 (2)允许两个Thread对象有相同的名称,但是应该避免。 (3)如果程序没有为线程指定名称,系统会自动为线程设置名称。

编程规范要求:创建线程或线程池时,需要指定有意义的线程名称,方便出错时回溯。

1.6.2 线程的sleep操作

sleep的作用是让目前正在执行的线程休眠,让CPU去执行其他的任务。从线程状态来说,就是从执行状态变成限时阻塞状态。

sleep()方法会有InterruptException受检异常抛出,如果调用了sleep()方法,就必须进行异常审查,捕获InterruptedException异常,或者再次通过方法声明存在InterruptedException异常。

必须注意的是,sleep函数会在抛出异常时重置中断信号标识位为false。而wait函数,则在抛出异常前重置中断信号标识位为false。

1.6.3 线程的interrupt操作

Java语言提供了stop()方法终止正在运行的线程,但是Java将Thread的stop()方法设置为过时,不建议大家使用。为什么呢?因为使用stop()方法是很危险的,就像突然关闭计算机电源,而不是按正常程序关机。

在程序中,我们是不能随便中断一个线程的,我们无法知道这个线程正运行在什么状态,它可能持有某把锁,强行中断线程可能导致锁不能释放的问题;或者线程可能在操作数据库,强行中断线程可能导致数据不一致的问题。正是由于调用stop()方法来终止线程可能会产生不可预料的结果,因此不推荐调用stop()方法。

一个线程什么时候可以退出呢?当然只有线程自己才能知道。所以,这里介绍一下Thread的interrupt()方法,此方法本质不是用来中断一个线程,而是将线程设置为中断状态。

当我们调用线程的interrupt()方法时,它有两个作用:

  • (1)如果此线程处于阻塞状态(如调用了Object.wait()方法),就会立马退出阻塞,并抛出InterruptedException异常,线程就可以通过捕获InterruptedException来做一定的处理,然后让线程退出。更确切地说,如果线程被Object.wait()、Thread.join()和Thread.sleep()三种方法之一阻塞,此时调用该线程的interrupt()方法,该线程将抛出一个InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早终结被阻塞状态。
  • (2)如果此线程正处于运行之中,线程就不受任何影响,继续运行,仅仅是线程的中断标记被设置为true。所以,程序可以在适当的位置通过调用isInterrupted()方法来查看自己是否被中断,并执行退出操作。

如果线程的interrupt()方法先被调用,然后线程开始调用阻塞方法进入阻塞状态,InterruptedException异常依旧会抛出。如果线程捕获InterruptedException异常后,继续调用阻塞方法,将不再触发InterruptedException异常。

Thread.interrupt()方法并不像Thread.stop()方法那样中止一个正在运行的线程,其作用是设置线程的中断状态位(为true),至于线程是死亡、等待新的任务还是继续运行至下一步,就取决于这个程序本身。线程可以不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。总之,Thread.interrupt()方法只是改变中断状态,不会中断一个正在运行的线程,线程是否停止执行,需要用户程序去监视线程的isInterrupted()状态,并进行相应的处理。

1.6.4 线程的join操作

假设有两个线程A和B。现在线程A在执行过程中对另一个线程B的执行有依赖,具体的依赖为:线程A需要将线程B的执行流程合并到自己的执行流程中(至少表面如此),这就是线程合并,被动方线程B可以叫作被合并线程。

1.线程的join操作的三个版本

Image [34].png

Image [35].png

Image [36].png

调用join()方法的要点: (1)join()方法是实例方法,需要使用被合并线程的句柄(或者指针、变量)去调用,如threadb.join()。执行threadb.join()这行代码的当前线程为合并线程(甲方),进入TIMED_WAITING等待状态,让出CPU。 (2)如果设置了被合并线程的执行时间millis(或者millis+nanos),并不能保证当前线程一定会在millis时间后变为RUNNABLE。 (3)如果主动方合并线程在等待时被中断,就会抛出InterruptedException受检异常。

调用join()方法的语句可以理解为合并点,合并的本质是:线程A需要在合并点等待,一直等到线程B执行完成,或者等待超时。

调用join()方法的优势是比较简单,劣势是join()方法没有办法直接取得乙方线程的执行结果。

Image [37].png

2.join线程的WAITING状态

线程的WAITING(等待)状态表示线程在等待被唤醒。处于WAITING状态的线程不会被分配CPU时间片。执行以下两个操作,当前线程将处于WAITING状态:

  • (1)执行没有时限(timeout)参数的thread.join()调用:在线程合并场景中,若线程A调用B.join()去合入B线程,则在B执行期间线程A处于WAITING状态,一直等线程B执行完成。

  • (2)执行没有时限(timeout)参数的object.wait()调用:指一个拥有object对象锁的线程,进入相应的代码临界区后,调用相应的object的wait()方法去等待其“对象锁”(Object Monitor)上的信号,若“对象锁”上没有信号,则当前线程处于WAITING状态,如图所示。

    Image [38].png

3.join线程的TIMED_WAITING状态

线程的TIMED_WAITING状态表示在等待唤醒。处于TIMED_WAITING状态的线程不会被分配CPU时间片,它们要等待被唤醒,或者直到等待的时限到期。

在线程合入场景中,若线程A在调用B.join()操作时加入了时限参数,则在B执行期间线程A处于TIMED_WAITING状态。若B在等待时限内没有返回,则线程A结束等待TIMED_WAITING状态,恢复成RUNNABLE状态。

1.6.5 线程的yield操作

线程的yield(让步)操作的作用是让目前正在执行的线程放弃当前的执行,让出CPU的执行权限,使得CPU去执行其他的线程。处于让步状态的JVM层面的线程状态仍然是RUNNABLE状态,但是该线程所对应的操作系统层面的线程从状态上来说会从执行状态变成就绪状态。线程在yield时,线程放弃和重占CPU的时间是不确定的,可能是刚刚放弃CPU,马上又获得CPU执行权限,重新开始执行。

yield()方法是Thread类提供的一个静态方法,它可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是让线程转入就绪状态。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次。

yield()方法不会释放锁资源。注意:并不是每个线程都需要这个锁的,而且执行yield( )的线程不一定就会持有锁,我们完全可以在释放锁后再调用 yield 方法。

Image [39].png 线程调用yield之后,操作系统在重新进行线程调度时偏向于将执行机会让给优先级较高的线程。

Thread.yeid()方法有以下特点:

  • (1)yield仅能使一个线程从运行状态转到就绪状态,而不是阻塞状态。
  • (2)yield不能保证使得当前正在运行的线程迅速转换到就绪状态。
  • (3)即使完成了迅速切换,系统通过线程调度机制从所有就绪线程中挑选下一个执行线程时,就绪的线程有可能被选中,也有可能不被选中,其调度的过程受到其他因素(如优先级)的影响。

1.6.6 线程的daemon操作

Java中的线程分为两类:守护线程与用户线程。

守护线程也称为后台线程,专门指在程序进程运行过程中,在后台提供某种通用服务的线程。比如,每启动一个JVM进程,都会在后台运行一系列的GC(垃圾回收)线程,这些GC线程就是守护线程,提供幕后的垃圾回收服务。

1.守护线程的基本操作

(1)实例属性daemon:保存一条Thread线程实例的守护状态,默认为false,表示线程默认为用户线程。

(2)实例方法setDaemon(…):此方法将线程标记为守护线程或者用户线程。setDaemon(true)将线程设置为守护线程,setDaemon(false)将线程设置为用户线程。

(3)实例方法isDaemon():获取线程的守护状态,用于判断该线程是不是守护线程。

Java程序中,main线程也是一条用户线程。 main线程在创建和启动了daemonThread和userThread后,就提前结束了。虽然main线程结束了,但是两条线程还在继续执行,其中就有一条是用户线程,所以进程还不能结束。当剩下的一条用户线程userThread的run()方法执行完成后,userThread线程执行结束。这时,所有的用户线程执行已经完成,JVM进程就随之退出了。在JVM退出时,守护线程daemonThread还远远没有结束,还在死循环的执行中。但是JVM不管这些,强行终止了所有守护线程的执行。

2.守护线程与用户线程的关系

从是否为守护线程的角度,对Java线程进行分类,分为用户线程和守护线程。

守护线程和用户线程的本质区别是:二者与JVM虚拟机进程终止的方向不同。用户线程和JVM进程是主动关系,如果用户线程全部终止,JVM虚拟机进程也随之终止;守护线程和JVM进程是被动关系,如果JVM进程终止,所有的守护线程也随之终止,如图1-14所示。

Image [40].png

换个角度来理解,守护线程提供服务,是守护者,用户线程享受服务,是被守护者。只有用户线程全部终止了,相当于没有了被守护者,守护线程也就没有工作可做了,也就可以全部终止了。当然,用户线程全部终止,JVM进程也就没有继续的必要了。反过来说,只要有一个用户线程没有终止,JVM进程也不会退出。但是在终止维度上,守护线程和JVM进程没有主动关系。也就是说,哪怕是守护线程全部被终止,JVM虚拟机也不一定终止。

3.守护线程的要点

使用守护线程时,有以下几点需要特别注意:

(1)守护线程必须在启动前将其守护状态设置为true,启动之后不能再将用户线程设置为守护线程,否则JVM会抛出一个InterruptedException异常。

具体来说,如果线程为守护线程,就必须在线程实例的start()方法调用之前调用线程实例的setDaemon(true),设置其daemon实例属性值为true。

(2)守护线程存在被JVM强行终止的风险,所以在守护线程中尽量不去访问系统资源,如文件句柄、数据库连接等。守护线程被强行终止时,可能会引发系统资源操作不负责任的中断,从而导致资源不可逆的损坏。

(3)守护线程创建的线程也是守护线程。在守护线程中创建的线程,新的线程都是守护线程。在创建之后,如果通过调用setDaemon(false)将新的线程显式地设置为用户线程,新的线程可以调整成用户线程。

1.6.7 线程状态总结

1.NEW状态

通过new Thread(…)已经创建线程,但尚未调用start()启动线程,该线程处于NEW(新建)状态。虽然前面介绍了4种方式创建线程,但是其中的其他三种方式本质上都是通过newThread()创建线程,仅仅是创建了不同的target执行目标实例(如Runnable实例)。

2.RUNNABLE状态

Java把Ready(就绪)和Running(执行)两种状态合并为一种状态:RUNNABLE(可执行)状态(或者可运行状态)。调用了线程的start()实例方法后,线程就处于就绪状态。此线程获取到CPU时间片后,开始执行run()方法中的业务代码,线程处于执行状态。

(1)就绪状态 就绪状态仅仅表示线程具备运行资格,如果没有被操作系统的调度程序挑选中,线程就永远处于就绪状态。当前线程进入就绪状态的条件大致包括以下几种:

  • 调用线程的start()方法,此线程就会进入就绪状态。
  • 当前线程的执行时间片用完。
  • 线程睡眠(Sleep)操作结束。
  • 对其他线程合入(Join)操作结束。
  • 等待用户输入结束。
  • 线程争抢到对象锁(Object Monitor)。
  • 当前线程调用了yield()方法出让CPU执行权限。

(2)执行状态 线程调度程序从就绪状态的线程中选择一个线程,被选中的线程状态将变成执行状态。这也是线程进入执行状态的唯一方式。

3.BLOCKED状态

处于BLOCKED(阻塞)状态的线程并不会占用CPU资源,以下情况会让线程进入阻塞状态:

(1)线程等待获取锁 等待获取一个锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态。

(2)IO阻塞 线程发起了一个阻塞式IO操作后,如果不具备IO操作的条件,线程就会进入阻塞状态。IO包括磁盘IO、网络IO等。IO阻塞的一个简单例子:线程等待用户输入内容后继续执行。

4.WAITING状态

处于WAITING(无限期等待)状态的线程不会被分配CPU时间片,需要被其他线程显式地唤醒,才会进入就绪状态。线程调用以下3种方法会让自己进入无限等待状态:

  • Object.wait()方法,对应的唤醒方式为:Object.notify()/Object.notifyAll()。
  • Thread.join()方法,对应的唤醒方式为:被合入的线程执行完毕。
  • LockSupport.park()方法,对应的唤醒方式为:LockSupport.unpark(Thread)。

5.TIMED_WAITING状态

处于TIMED_WAITING(限时等待)状态的线程不会被分配CPU时间片,如果指定时间之内没有被唤醒,限时等待的线程会被系统自动唤醒,进入就绪状态。以下3种方法会让线程进入限时等待状态:

  • Thread.sleep(time)方法,对应的唤醒方式为:sleep睡眠时间结束。
  • Object.wait(time)方法,对应的唤醒方式为:调用Object.notify()/Object.notifyAll()主动唤醒,或者限时结束。
  • LockSupport.parkNanos(time)/parkUntil(time)方法,对应的唤醒方式为:线程调用配套的LockSupport.unpark(Thread)方法结束,或者线程停止(park)时限结束。

进入BLOCKED状态、WAITING状态、TIMED_WAITING状态的线程都会让出CPU的使用权;另外,等待或者阻塞状态的线程被唤醒后,进入Ready状态,需要重新获取时间片才能接着运行。

6.TERMINATED状态

线程结束任务之后,将会正常进入TERMINATED(死亡)状态;或者说在线程执行过程中发生了异常(而没有被处理),也会导致线程进入死亡状态。

1.7 线程共享

1.8 线程协作

1.9 高并发编程

1.9.1 CPU核心数与线程数

多核心:也指单芯片多处理器( Chip Multiprocessors,简称 CMP),CMP 是由美国斯坦福大学提出的,其思想是将大规模并行处理器中的 SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个 CPU 同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理。

多线程: Simultaneous Multithreading,简称 SMT。让同一个处理器上的多个线程同步执行并共享处理器的执行资源。

核心数、线程数:目前主流 CPU 都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是 1:1 对应关系,也就是说四核 CPU 一般拥有四个线程。但 Intel 引入超线程技术后,使核心数与线程数形成 1:2 的关系。

image.png

1.9.2 并行与并发

当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少? 离开了单位时间其实是没有意义的。

并发:指应用能够交替执行不同的任务。比如单 CPU 核心下执行多线程并非是同时执行多个任务,只是以人不可能察觉到的速度不断地去切换任务,达到"同时执行效果"的错觉,仅此而已。

并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话, 这两件事情可以同时执行。

两者区别:一个是交替执行,一个是同时执行。

1.9.3 高并发编程的意义

  1. 充分利用CPU的资源。
  2. 加快响应用户的时间。
  3. 可以使你的代码模块化、异步化、简单化。

1.9.4 多线程程序需要注意的事项

  1. 线程之间的安全性。
  2. 线程之间的死锁。
  3. 线程太多了会将服务器资源耗尽形成死机当机。

1.10 线程间通信

1.10.1 线程间通信的定义

线程的通信可以被定义为:当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以避免无效的资源争夺。

线程间通信的方式可以有很多种:等待-通知、共享内存、管道流。

“等待-通知”通信方式是Java中使用普遍的线程间通信方式,其经典的案例是“生产者-消费者”模式。

1.10.2 低效的线程轮询

Java语言中“等待-通知”方式的线程间通信使用对象的wait()、notify()两类方法来实现。每个Java对象都有wait()、notify()两类实例方法,并且wait()、notify()方法和对象的监视器是紧密相关的。

wait()、notify()两类方法在数量上不止两个。wait()、notify()两类方法不属于Thread类,而是属于Java对象实例(Object实例或者Class实例)。

1.10.3 wait方法和notify方法的原理

1.对象的wait()方法

对象的wait()方法的主要作用是让当前线程阻塞并等待被唤醒。wait()方法与对象监视器紧密相关,使用wait()方法时一定要放在同步块中。

Object类中的wait()方法有三个版本:

(1)void wait()这是一个基础版本,当前线程调用了同步对象locko的wait()实例方法后,将导致当前的线程等待,当前线程进入locko的监视器WaitSet,等待被其他线程唤醒。

(2)void wait(long timeout)这是一个限时等待版本,导致当前的线程等待,等待被其他线程唤醒,或者指定的时间timeout用完,线程不再等待。

(3)void wait(long timeout,int nanos)这是一个高精度限时等待版本,其主要作用是更精确地控制等待时间。参数nanos是一个附加的纳秒级别的等待时间,从而实现更加高精度的等待时间控制。

2.wait()方法的核心原理

对象的wait()方法的核心原理大致如下:

(1)当线程调用了locko(某个同步锁对象)的wait()方法后,JVM会将当前线程加入locko监视器的WaitSet(等待集),等待被其他线程唤醒。

(2)当前线程会释放locko对象监视器的Owner权利,让其他线程可以抢夺locko对象的监视器。

(3)让当前线程等待,其状态变成WAITING。

3.对象的notify()方法

对象的notify()方法的主要作用是唤醒在等待的线程。notify()方法与对象监视器紧密相关,调用notify()方法时也需要放在同步块中。

notify()方法有两个版本:

版本一:void notify()

notify()方法的主要作用为:locko.notify()调用后,唤醒locko监视器等待集中的第一条等待线程;被唤醒的线程进入EntryList,其状态从WAITING变成BLOCKED。

版本二:void notifyAll()

locko.notifyAll()被调用后,唤醒locko监视器等待集中的全部等待线程,所有被唤醒的线程进入EntryList,线程状态从WAITING变成BLOCKED。

4.notify()方法的核心原理

对象的notify()或者notifyAll()方法的核心原理大致如下:

(1)当线程调用了locko(某个同步锁对象)的notify()方法后,JVM会唤醒locko监视器WaitSet中的第一条等待线程。

(2)当线程调用了locko的notifyAll()方法后,JVM会唤醒locko监视器WaitSet中的所有等待线程。

(3)等待线程被唤醒后,会从监视器的WaitSet移动到EntryList,线程具备了排队抢夺监视器Owner权利的资格,其状态从WAITING变成BLOCKED。

(4)EntryList中的线程抢夺到监视器的Owner权利之后,线程的状态从BLOCKED变成Runnable,具备重新执行的资格。

1.10.4 “等待-通知”通信模式演示案例

Java的“等待-通知”机制是指:一个线程A调用了同步对象的wait()方法进入等待状态,而另一线程B调用了同步对象的notify()或者notifyAll()方法通知等待线程,当线程A收到通知后,重新进入就绪状态,准备开始执行。

线程间的通信需要借助同步对象(Object)的监视器来完成,Object对象的wait()、notify()方法就如开关信号,用于完成等待方和通知方之间的通信。

1.10.5 生产者-消费者之间的线程间通信

使用“等待-通知”机制通信的生产者-消费者实现版本

1.10.6 需要在synchronized同步块的内部使用wait和notify

在调用同步对象的wait()和notify()系列方法时,“当前线程”必须拥有该对象的同步锁,也就是说,wait()和notify()系列方法需要在同步块中使用,否则JVM会抛出类似如下的异常:

wait()方法的原理:

首先,JVM会释放当前线程的对象锁监视器的Owner资格;

其次,JVM会将当前线程移入监视器的WaitSet队列,而这些操作都和对象锁监视器是相关的。

所以,wait()方法必须在synchronized同步块的内部调用。在当前线程执行wait()方法前,必须通过synchronized()方法成为对象锁的监视器的Owner。

notify()方法的原理:

JVM从对象锁的监视器的WaitSet队列移动一个线程到其EntryList队列,这些操作都与对象锁的监视器有关。

所以,notify()方法也必须在synchronized同步块的内部调用。在执行notify()方法前,当前线程也必须通过synchronized()方法成为对象锁的监视器的Owner。

调用wait()和notify()系列方法进行线程通信的要点如下:

(1)调用某个同步对象locko的wait()和notify()类型方法前,必须要取得这个锁对象的监视锁,所以wait()和notify()类型方法必须放在synchronized(locko)同步块中,如果没有获得监视锁,JVM就会报IllegalMonitorStateException异常。(2)调用wait()方法时使用while进行条件判断,如果是在某种条件下进行等待,对条件的判断就不能使用if语句做一次性判断,而是使用while循环进行反复判断。只有这样才能在线程被唤醒后继续检查wait的条件,并在条件没有满足的情况下继续等待。

2. 造火箭攻略

2.1 在Java中能不能指定CPU去执行某个线程?

不能。Java是做不到的,唯一能够去干预的就是C语言调用内核的API去指定才行。

2.2 在项目开发中,是否考虑线程优先级?

不考虑。因为线程的优先级很依赖于系统的平台,所以这个优先级可能无法对号入座,无法做到我们所想象中的优先级,属于不稳定、有风险。 例如:Java线程优先级有十级,而此时操作系统优先级只有2~3级,那么就对应不上。

在不同的 JVM 以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。

2.3 sleep和wait的区别

sleep是休眠,待休眠时间过后,才会参与CPU资源的竞争,最终能否执行取决于操作系统的调度。 sleep是无条件休眠。

wait是等待,被唤醒后,才会参与CPU资源的竞争,最终能否执行取决于操作系统的调度。 wait是因某些原因与条件需要等待一下(资源不满足)。

2.4 在Java中能不能强制中断线程的执行?

虽然提供了 stop 等函数,但是此函数不推荐使用,因为这种暴力的方式很危险。例如:下载图片5kb,只下载了4kb等。我们可以使用 interrupt 来处理线程的停止,但是需注意interrupt只是协作的方式,并不能绝对保证中断,并不是抢占式的。

线程自然终止要么是 run 执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。

暂停、恢复和停止操作对应在线程 Thread 的 API 就是 suspend()、resume() 和 stop()。但是这些 API 是过期的,也就是不建议使用的。不建议使用的原因主 要有:以 suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如 锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方 法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为 suspend()、 resume()和 stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方 法。

安全的中止则是其他线程通过调用某个线程 A 的 interrupt()方法对其进行中断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表 线程 A 会立即停止自己的工作,同样的 A 线程完全可以不理会这种中断请求。 因为 java 里的线程是协作式的,不是抢占式的。线程通过检查自身的中断标志 位是否被置为 true 来进行响应,

线程通过方法 isInterrupted()来进行判断是否被中断,也可以调用静态方法 Thread.interrupted()来进行判断当前线程是否被中断,不过 Thread.interrupted() 会同时将中断标识位改写为 false。

如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、 thread.wait 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在 这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为 false。

不建议自定义一个取消标志位来中止线程的运行。因为run方法里有阻塞调 用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取 消标志。

这种情况下,使用中断会更好,因为:

  1. 一般的阻塞方法,如 sleep 等本身就支持中断的检查,
  2. 检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。

注意:处于死锁状态的线程无法被中断

2.5 start与run

Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread() 其实只是 new 出一个 Thread 的实例,还没有与操作系统中真正的线程挂起钩来。 只有执行了 start()方法后,才实现了真正意义上的启动线程。

start()方法让一个线程进入就绪队列等待分配 cpu,分到 cpu 后才调用实现的 run()方法,start()方法不能重复调用,如果重复调用会抛出异常。

而run方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。

2.6 调用 yield() 、sleep()、wait()、notify()等方法对锁有何影响?

yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。

调用 wait()方法后,会释放当前线程持有的锁,而且当被唤醒后,会重新去竞争锁,锁竞争到后才会执行 wait 方法后面的代码。

调用 notify()系列方法后,对锁无影响,线程只有在 syn 同步代码执行完后才 会自然而然的释放锁,所以 notify()系列方法一般都是 syn 同步代码的最后一行。

2.7 ForkJoin

2.8 CountDownLatch

3. 拧螺丝攻略

4. 复制攻略

4.1 《Java高并发核心编程(卷2)》

4.2 面试官:说说多线程并发问题