线程与进程
文章目录
前言
一、初识多线程
1.1 并行、并发、串行
1.2 上下文切换
1.2.1 上下分切换的分类
1.2.2 减少上下文切换的方式
1.2.3 上下文切换的优化示例
1.3 并发编程的优缺点
1.3.1 并发编程的优点*
1.3.2 并发编程的缺点*
1.4 并发编程三要素
1.4.1 原子性*
1.4.2 可见性*
1.4.3 有序性*
1.5 同步与异步
1.6 进程与线程
1.7 线程调度
1.8 相关问题
1.8.1 编写多线程程序的时候你会遵循哪些最佳实践
1.8.2 什么是线程调度器和时间分片
1.8.3 Linux环境下如何查找哪个线程使用CPU最长
1.8.4 多进程和多线程的区别
二、线程的基本使用
2.1 创建线程
2.1.1 继承Thread类*
2.1.2 实现Runnable接口*
2.1.3 实现Callable接口*
2.1.4 创建线程池*
2.1.5 4种创建方式对比
2.2 启动线程
2.2.1 线程每次只能使用一次*
2.2.2 线程的run和start有什么区别*
2.2.3 为什么不能直接调用run()方法*
2.2.4 线程类的构造方法、静态块是被哪个线程调用的
2.3 线程属性
2.3.1 线程优先级*
2.3.2 守护线程和用户线程*
2.3.3 线程名称
2.4 线程的生命周期*
2.4.1 从代码角度理解
2.4.2 从使用角度理解*
2.5 Thread类的常用方法
2.5.1 interrupt*
2.5.1 interrupted*
2.5.2 join*
2.5.3 sleep*
2.5.4 yield
2.6 线程相关的一些问题
2.6.1 interrupt、interrupted和isInterrupted方法的区别*
2.6.2 sleep方法和yield方法有什么区别*
2.6.3 线程怎么处理异常
2.6.4 Thread.sleep(0)的作用是什么*
2.6.5 一个线程如果出现了运行时异常会怎么样
2.6.6 终止线程运行的几种情况
2.6.7 如何优雅地设置睡眠时间
2.6.8 如何设置上下文类加载器
2.6.9 如何停止一个正在运行的线程
2.6.10 为什么Thread类的sleep()和yield()方法是静态的
2.6.11 怎么检测一个线程是否拥有锁
2.6.12 线程的调度策略
2.6.13 线程的调度策略
2.6.14 join可以保证线程执行顺序的原理
2.6.15 stop()方法和interrupt()方法的区别
2.6.16 有三个线程T1,T2,T3,如何保证顺序执行*
2.6.17 线程中断是否能直接调用stop
三、线程的活性故障
3.1 死锁*
3.1.1 死锁的产生条件*
3.1.2 死锁的规避*
3.2 线程饥饿和活锁
3.3 死锁与活锁的区别,死锁与饥饿的区别
本系列文章:
多线程(一)线程与进程、Thread
多线程(二)Java内存模型、同步关键字
多线程(三)线程池
多线程(四)显式锁、队列同步器
多线程(五)可重入锁、读写锁
多线程(六)线程间通信机制
多线程(七)原子操作、阻塞队列
多线程(八)并发容器
多线程(九)并发工具类
多线程(十)多线程编程示例
前言
计算机的组成
一个程序要运行,首先要被加载到内存,然后数据被运送到CPU的寄存器里。寄存器用来存储数据;PC为程序计数器,用来记录要执行的程序的位置;算术逻辑单元执行具体的计算,然后将结果再传送给内存。
CPU执行运算的大致过程:CPU读取指令,然后程序计数器存储程序的执行位置,然后从寄存器中读取原始数据,计算完成后,再将结果返回给内存,一直循环下去。
线程之间的调度由线程调度器负责,确定在某一时刻运行哪个线程。
线程上下文切换,简单来说,指的是CPU保存现场,执行新线程,恢复现场,继续执行原线程的一个过程。
一、初识多线程
多线程可以理解为在同一个程序中能够同时运行多个不同的线程来执行不同的任务,这些线程可以同时利用CPU的多个核心运行。多线程编程能够最大限度的利用CPU的资源。如果某一个线程的处理不需要占用CPU资源时(例如IO线程),可以使当前线程让出CPU资源来让其他线程能够获取到CPU资源,进而能够执行其他线程对应的任务,达到最大化利用CPU资源的目的。
1.1 并行、并发、串行
并发
多个任务在同一个CPU核上,按细分的时间片轮流执行,从逻辑上来看任务是同时执行。
并行
单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
串行
有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
图示:
可以看出:串行是利用一个资源,依次、首尾相接地把不同的事情做完;并发也是利用一个资源,在做一件事时的空闲时间去做另一件事;并行是投入多个资源,去做多件事。
多线程编程的实质就是将任务的处理方式由串行改成并发。
1.2 上下文切换
上下文,指某一时间点CPU寄存器和程序计数器的内容。
寄存器,是CPU内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的RAM主内存)。
程序计数器,是一个专用的寄存器,用于表明指令序列中CPU正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置。
一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是交替地为每个线程分配时间片,当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用。
概括来说:当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
在时间片切换到别的任务和切换到当前任务的时候,操作系统需要保存和恢复相应线程的进度信息。这个进度信息就是上下文,它一般包括通用寄存器的内容和程序计数器的内容。
使用vmstat可以测量上下文切换的次数。示例:
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 127876 398928 2297092 0 0 0 4 2 2 0 0 99 0 0
0 0 0 127868 398928 2297092 0 0 0 0 595 1171 0 1 99 0 0
0 0 0 127868 398928 2297092 0 0 0 0 590 1180 1 0 100 0 0
0 0 0 127868 398928 2297092 0 0 0 0 567 1135 0 1 99 0 0
CS(Content Switch)表示上下文切换的次数,例子中的上下文每1秒切换1000多次。
1.2.1 上下分切换的分类
上下文切换可以分为自发性上下文切换和非自发性上下文切换(通常说的上下文切换指的是第一种):
类型 含义 原因
自发性上下文切换 由于自身因素导致的切出 Thread.sleep(long mills);
Object.wait();
Thread.yiels();
Thread.join();
LockSupport.park();
线程发起了IO操作;
等待其他线程持有的锁 。
非自发性上下文切换 由于线程调度器的原因被迫切出 当前线程的时间片用完;
有一个比当前线程优先级更高的线程需要运行;
Java虚拟机的垃圾回收动作。
1.2.2 减少上下文切换的方式
1、无锁并发编程
类似ConcurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
2、CAS算法
利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换。
3、使用最少线程
避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态。
4、协程
在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
1.2.3 上下文切换的优化示例
1、用jstack命令dump线程信息
此处查看pid为3117的进程:
sudo -u admin /opt/ifeve/java/bin/jstack 31177 > /home/tengfei.fangtf/dump17
2、统计所有线程分别处于什么状态
发现300多个线程处于WAITING(onobject-monitor)状态:
[tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}'
| sort | uniq -c
39 RUNNABLE
21 TIMED_WAITING(onobjectmonitor)
6 TIMED_WAITING(parking)
51 TIMED_WAITING(sleeping)
305 WAITING(onobjectmonitor)
3 WAITING(parking)
3、打开dump文件查看处于WAITING(onobjectmonitor)的线程在做什么
发现这些线程基本全是JBOSS的工作线程,在await。说明JBOSS线程池里线程接收到的任务太少,大量线程都闲着:
"http-0.0.0.0-7001-97" daemon prio=10 tid=0x000000004f6a8000 nid=0x555e in
Object.wait() [0x0000000052423000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
at java.lang.Object.wait(Object.java:485)
at org.apache.tomcat.util.net.AprEndpoint$Worker.await(AprEndpoint.java:1464) - locked <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1489)
at java.lang.Thread.run(Thread.java:662)
4、做出优化
减少JBOSS的工作线程数,找到JBOSS的线程池配置信息,将maxThreads降到100:
<maxThreads="250" maxHttpHeaderSize="8192"
emptySessionPath="false" minSpareThreads="40" maxSpareThreads="75"
maxPostSize="512000" protocol="HTTP/1.1"
enableLookups="false" redirectPort="8443" acceptCount="200" bufferSize="16384"
connectionTimeout="15000" disableUploadTimeout="false" useBodyEncodingForURI= "true">
5、验证
重启JBOSS,再dump线程信息,然后统计WAITING(onobjectmonitor)的线程,发现减少了175个。WAITING的线程少了,系统上下文切换的次数就会少,因为每一次从WAITTING到RUNNABLE都会进行一次上下文的切换。
[tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}'
| sort | uniq -c
44 RUNNABLE
22 TIMED_WAITING(onobjectmonitor)
9 TIMED_WAITING(parking)
36 TIMED_WAITING(sleeping)
130 WAITING(onobjectmonitor)
1 WAITING(parking)
1.3 并发编程的优缺点
1.3.1 并发编程的优点*
1、充分利用多核CPU的计算能力
可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的,采用多线程的方式去同时完成几件事情而不互相干扰。
2、方便进行业务拆分,提升应用性能
多线程并发编程是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
1.3.2 并发编程的缺点*
1、频繁的上下文切换
时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,达到一种"不同应用似乎是同时运行的错觉",时间片一般是几十毫秒。每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。
2、产生线程安全问题
即死锁、线程饥饿等问题。
1.4 并发编程三要素
线程安全:多线程访问同一代码,不会产生不确定的结果。
线程安全问题概括来说表现为3个方面:原子性、可见性和有序性。
1.4.1 原子性*
1、如何理解原子性
对于涉及共享变量的操作,若该操作从其执行线程以外的任意线程来看是不可分割的,那么该操作就是原子操作,相应地我们称该操作具有原子性。
原子性问题由线程切换导致。
原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。
在理解原子操作时有两点需要注意:
原子操作是针对共享变量的操作而言的;
原子操作是在多线程环境下才有意义。
原子操作的“不可分割”具有两层含义:
1、访问(读、写)某个共享变量的操作,从其执行线程以外的任何线程来看,该操作要么已经执行结束要么尚未发生,不会“看到”该操作执行部分的中间效果。
2、访问同一组共享变量的原子操作是不能够被交错的。
在Java中,对基本数据类型数据(long/double除外,仅包括byte、boolean、short、char、float和int)的变量和引用型变量的写操作都是原子的。
虚拟机将没有被volatile修饰的64位数据(long/double)的读写操作划分为两次32位的操作来进行。
如果要保证long/double的写操作具有原子性,可以使用volatile变量修饰long/double变量。值得注意的是:volatile关键字仅能保证变量写操作的原子性,并不能保证其他操作(如read-modify-write操作和check-then-act操作)的原子性。
Java中任何变量的读操作都是原子操作。
2、原子性问题的例子
一个关于原子性的典型例子:counter++这并不是一个原子操作,包含了三个步骤:
读取变量counter的值;
对counter加一;
将新值赋值给变量counter。
3、解决原子性问题方法
Atomic开头的原子类、synchronized、LOCK等(即:锁机制和无锁CAS机制),都可以解决原子性问题。
1.4.2 可见性*
1、如何理解可见性
如果一个线程对某个共享变量进行更新后,后续访问该变量的线程可以读取到本次更新的结果,那么就称这个线程对该共享变量的更新对其它线程可见(一个线程对共享变量的修改,另一个线程能够立刻看到)。
可见性问题由缓存导致。
2、如何实现可见性
主要有三种实现可见性的方式:
volatile,通过在汇编语言中添加lock指令,来实现内存可见性。
synchronized,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。
final,被final关键字修饰的字段在构造器中一旦初始化完成,并且没有发生this逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
3、一些可见性场景
Java中默认的两种可见性的存在场景:
父线程在启动子线程之前对共享变量的更新对于子线程来说是可见的。
一个线程终止后该线程对共享变量的更新对于调用该线程的join方法的线程而言是可见的。
1.4.3 有序性*
有序性指的是:程序执行的顺序按照代码的先后顺序执行。有序性问题由编译优化导致。
volatile和synchronized都可以保证有序性:
volatile关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
synchronized关键字同样可以保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。
1.5 同步与异步
同步
当一个同步调用发出去后,调用者要一直等待调用结果的返回后,才能进行后续的操作。
异步
当一个异步调用发出去后,调用者不用管被调用方法是否完成,都会继续执行后面的代码。 异步调用,要想获得结果,一般有两种方式:
主动轮询异步调用的结果;
被调用方通过callback来通知调用方调用结果(常用)。
比如在超市购物,如果一件物品没了,你等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用就像网购,在网上付款下单后就不用管了,当货物到达后你收到通知去取就好。
1.6 进程与线程
程序:含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程:程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出设备的使用权等等。
线程:与进程相似,但线程是一个比进程更小的执行单位。进程是程序向操作系统申请资源的基本单位,线程是进程中可独立执行的最小单位。通常一个进程可以包含多个线程,至少包含一个线程,同一个进程中所有线程共享该进程的资源。系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程(使用多线程而不是多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程)。
1、根本区别
进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
2、资源开销
每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
3、包含关系
一个进程里可以包含多个线程。
4、内存分配
同一进程的线程共享本进程的地址空间和资源,而线程之间的地址空间和资源是相互独立的。
5、影响关系
一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
6、执行过程
每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。
1.7 线程调度
一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。多线程的并发运行,指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。
线程调度模型有两种:
1、分时调度模型
分时调度模型是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片。如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
2、抢占式调度模型
抢占式调度模型是指优先让运行池中优先级高的线程占用CPU,如果运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
Java虚拟机(JVM)采用抢占式调度模型。 Java 中线程会按优先级分配 CPU 时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。
线程让出 cpu 的情况:
当前运行线程主动放弃 CPU,JVM 暂时放弃 CPU 操作,例如调用 yield()方法。
当前运行线程因为某些原因进入阻塞状态,例如阻塞在 I/O 上。
当前运行线程结束,即运行完 run()方法里面的任务。
1.8 相关问题
1.8.1 编写多线程程序的时候你会遵循哪些最佳实践
1)给线程命名,这样可以帮助调试。
2)最小化同步的范围,而不是将整个方法同步,只对关键部分做同步。
3)如果可以,更偏向于使用volatile而不是synchronized。
4)使用更高层次的并发工具,而不是使用wait()和notify()来实现线程间通信,如BlockingQueue、CountDownLatch及Semeaphore。
5)优先使用并发集合,而不是对集合进行同步。并发集合提供更好的可扩展性。
6)使用线程池。
1.8.2 什么是线程调度器和时间分片
线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。
线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择。
1.8.3 Linux环境下如何查找哪个线程使用CPU最长
1、获取项目的pid,jps或者ps -ef | grep java。
2、top -H -p pid,顺序不能改变。
使用"top -H -p pid"+"jps pid"可以很容易地找到某条占用CPU高的线程的线程堆栈,从而定位占用CPU高的原因,一般是因为不当的代码操作导致了死循环。
最后提一点,"top -H -p pid"打出来的LWP是十进制的,"jps pid"打出来的本地线程号是十六进制的,转换一下,就能定位到占用CPU高的线程的当前线程堆栈了。
1.8.4 多进程和多线程的区别
多进程
进程是程序在计算机上的一次执行活动,即正在运行中的应用程序,通常称为进程。当你运行一个程序,你就启动了一个进程。每个进程都有自己独立的地址空间(内存空间),每当用户启动一个进程时,操作系统就会为该进程分配一个独立的内存空间,让应用程序在这个独立的内存空间中运行。
在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是多进程,也称多任务。现代的操作系统几乎都是多任务操作系统,能够同时管理多个进程的运行。
多任务带来的好处是明显的,比如你可以边听mp3边上网,与此同时甚至可以将下载的文档打印出来,而这些任务之间丝毫不会相互干扰。
多线程
线程是一个轻量级的子进程,是最小的处理单元;是一个单独的执行路径。可以说:线程是进程的子集(部分),一个进程可能由多个线程组成。
线程是独立的。如果在一个线程中发生异常,则不会影响其他线程。它使用共享内存区域。
多线程是一种执行模型,它允许多个线程存在于进程的上下文中,以便它们独立执行但共享其进程资源。
区别:
维度 多进程 多线程 总结
数据共享、同步 数据是分开的,共享复杂,需要用IPC(进程间通信);同步简单 多线程共享进程数据,共享简单;同步复杂 各有优势
内存、CPU 占用内存多,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高 线程占优
创建销毁、切换 创建销毁、切换复杂,速度慢 创建销毁、切换简单,速度快 线程占优
编程调试 编程简单,调试简单 编程复杂,调试复杂 进程占优
可靠性 进程间不会相互影响 一个线程挂掉将导致整个进程挂掉 进程占优
分布式 适应于多核、多机分布 ;如果一台机器不够,扩展到多台机器比较简单 适应于多核分布 线程占优
二、线程的基本使用
在Java中创建一个线程,可以理解创建一个Thread类(或其子类)的实例。线程的任务处理逻辑可以在Thread类的run实例方法中实现,运行一个线程实际上就是让Java虚拟机执行该线程的run方法。run方法相当于线程的任务处理逻辑的入口方法,它由虚拟机在运行相应线程时直接调用,而不是由相应代码进行调用。
启动一个线程的方法是调用start方法,其实质是请求Java虚拟机运行相应的线程,而这个线程具体何时运行是由线程调度器决定的。因此,start方法调用结束并不意味着相应线程已经开始运行。
2.1 创建线程
创建线程有4种方式。
2.1.1 继承Thread类*
继承Thread类,作为线程对象存在。使用方式:
继承Thread类;
重写run方法;
创建Thread对象;
通过start()方法启动线程。
示例:
/*继承Thread类*/
public class WelcomeThread extends Thread{
@Override
public void run() {
System.out.printf("test");
}
}
public class ThreadTest1 {
public static void main(String[] args) {
// 创建线程
Thread welcomeThread = new WelcomeThread();
// 启动线程
welcomeThread.start();
}
}
也可以使用匿名类的方式的来创建。示例:
new Thread(){
@Override
public void run() {
System.out.println("线程执行了...");
}
}.start();
//JDK1.8后,可以使用Lambda表达式来创建
new Thread(()->{
System.out.println("Lambda Thread Test!");
}).start();
2.1.2 实现Runnable接口*
实现runnable接口,作为线程任务存在。使用方式:
实现Runnable接口;
重写run方法;
创建Thread对象,将实现Runnable接口的类作为Thread的构造参数;
通过start()进行启动。
此种方式用到了代理模式,示例:
public class RunnableDemo implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
thread.start();
}
}
Runnable只是来修饰线程所执行的任务,它不是一个线程对象。想要启动Runnable对象,必须将它放到一个线程对象里。
前两种比较的话, 推荐使用第二种方式,原因:
Java是单继承,将继承关系留给最需要的类。
Runnable可以实现多个相同的程序代码的线程去共享同一个资源。当以Thread方式去实现资源共享时,实际上Thread内部,依然是以Runnable形式去实现的资源共享。
2.1.3 实现Callable接口*
前两种方式比较常见,Callable的使用方式:
创建实现Callable接口的类;
以Callable接口的实现类为参数,创建FutureTask对象;
将FutureTask作为参数创建Thread对象;
调用线程对象的start()方法。
示例:
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
return 1;
}
}
public class CallableTest {
public static void main(String[] args) {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
}
}
使用该方法创建线程时,核心方法是call(),该方法有返回值,其返回值类型就是Callable接口中泛型对应的类型。
2.1.4 创建线程池*
由于线程的创建、销毁是一个比较消耗资源的过程,所以在实际使用时往往使用线程池。
在创建线程池时,可以使用现成的Executors工具类来创建,该工具类能创建的线程池有4种:newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool。此处以newSingleThreadExecutor为例,其步骤为:
使用Executors类中的newSingleThreadExecutor方法创建一个线程池;
调用线程池中的execute()方法执行由实现Runnable接口创建的线程;或者调用submit()方法执行由实现Callable接口创建的线程;
调用线程池中的shutdown()方法关闭线程池。
示例:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
}
}
public class SingleThreadExecutorTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
MyRunnable runnableTest = new MyRunnable();
for (int i = 0; i < 5; i++) {
executorService.execute(runnableTest);
}
System.out.println("线程任务开始执行");
executorService.shutdown();
}
}
2.1.5 4种创建方式对比
1、继承Thread类
优点 :代码简单 。
缺点 :该类无法继承别的类。
如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用 this 即可获得当前线程。
2、实现Runnable接口
实现Runnable接口,比继承Thread类所具有的优势:
1)适合多个相同的程序代码的线程去处理同一个资源;
2)可以避免Java中的单继承的限制;
3)增加程序的健壮性,代码可以被多个线程共享,代码和数据独立 ;
4)线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类;
5)Runnable实现线程可以对线程进行复用,因为Runnable是轻量级的对象,重复new不会耗费太大资源,而Thread则不然,它是重量级对象,而且线程执行完就完了,无法再次利用。
3、实现Callable接口
优点:可以获得异步任务的返回值。
如果要访问当前线程,则必须使用Thread.currentThread()方法。
线程类实现Runnable接口或Callable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况。
4、线程池
优点:实现自动化装配,易于管理,循环利用资源。
2.2 启动线程
2.2.1 线程每次只能使用一次*
当线程的run方法执行结束,相应的线程的运行也就结束了。
线程每次只能使用一次,即只能调用一次start方法。在线程未结束前,多次调用start方法会抛出IllegalThreadStateException,Thread类中的start方法中可以看出该逻辑:
public synchronized void start() {
checkNotStarted();
hasBeenStarted = true;
nativeCreate(this, stackSize, daemon);
}
private void checkNotStarted() {
if (hasBeenStarted) {
throw new IllegalThreadStateException("Thread already started");
}
}
可以看出:start()方法使用synchronized关键字修饰,说明start()方法是同步的,它会在启动线程前检查线程的状态,如果不是初始化状态,则直接抛出异常。所以,一个线程只能启动一次,多次启动是会抛出异常的。
2.2.2 线程的run和start有什么区别*
start()方法用于启动线程,run()方法用于实现具体的业务逻辑。
run()可以重复调用,而start()只能调用一次。
通过调用Thread类的start方法来启动一个线程,无需等待run()方法体代码执行完毕,可以直接继续执行其他的代码, 此时线程是处于就绪状态,并没有运行。 只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。
run方法运行结束, 此线程终止。然后CPU再调度其它线程。
当直接调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
2.2.3 为什么不能直接调用run()方法*
JVM执行start方法,会另起一条线程执行thread的run方法,这才起到多线程的效果。如果直接调用Thread的run()方法,其方法还是运行在主线程中,没有起到多线程效果。
新建一个线程,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。
如果直接执行run()方法,会把run方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。示例:
public class JavaTest {
public static void main(String[] args) {
System.out.println("main方法中的线程名:"
+Thread.currentThread().getName()); //main方法中的线程名:main
Thread welcomeThread = new WelcomeThread();
System.out.println("以start方法启动线程");
welcomeThread.start(); //Thread子类中的线程名:Thread-0
System.out.println("以run方法启动线程");
welcomeThread.run(); //Thread子类中的线程名:main
}
}
class WelcomeThread extends Thread{
@Override
public void run() {
System.out.println("Thread子类中的线程名:"
+Thread.currentThread().getName());
}
}
总结: 调用start方法方可启动线程并使线程进入就绪状态,而run方法只是thread的一个普通方法调用,还是在主线程里执行。
2.2.4 线程类的构造方法、静态块是被哪个线程调用的
线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:
Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的
Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的。
2.3 线程属性
Thread类的私有属性有许多,了解几个常用的即可:线程的编号(ID)、名称(Name)、线程类别(Daemon)和优先级(Priority)。
这几个属性中,ID仅可读,其他都是可读写。具体:
属性 属性类型 用途 注意事项
编号(ID) long 用于标识不同的线程,不同的线程拥有不同的编号 某个编号的线程运行结束后,该编号可能被后续创建的线程使用,因此该属性的值不适合用作某种唯一标识
名称(Name) String 用于区分不同的线程,默认值的格式为“Thread-线程编号” 尽量为不同的线程设置不同的值
线程类别(Daemon) boolean 值为true表示相应的线程为守护线程,否则表示相应的线程为用户线程。该属性的默认值与相应线程的父线程的该属性的值相同 该属性必须在相应线程启动之前设置,即调用setDaemon方法必须在调用start方法之前,否则会出现IllegalThreadStateException
优先级(Priority) int 优先级高的线程一般会被优先运行。优先级从1到10,默认值一般为5(普通优先级),数字越大,优先级越高。
对于具体的一个线程而言,其优先级的默认值与其父线程的优先级值相等。 一般使用默认的优先级即可,不恰当地设置该属性值可能会导致严重的问题(线程饥饿)
获取4个属性值示例:
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
//10,Thread-0,5,false
System.out.println(Thread.currentThread().getId()+","
+Thread.currentThread().getName()+","
+Thread.currentThread().getPriority()+","
+Thread.currentThread().isDaemon());
}
}).start();
}
2.3.1 线程优先级*
Java线程的优先级属性本质上只是一个给线程调度器的提示信息,以便于线程调度器决定优先调度哪些线程运行。每个线程的优先级都在1到10之间,1的优先级为最低,10的优先级为最高,在默认情况下优先级都是Thread.NORM_PRIORITY(常数 5)。
虽然开发者可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。
线程优先级特性:
1、继承性
比如A线程启动B线程,则B线程的优先级与A是一样的。
2、规则性
高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。
3、随机性
优先级较高的线程不一定每一次都先执行完。
在不同的JVM以及OS上,线程规划会存在差异,有些OS会忽略对线程优先级的设定。
设置和获取线程优先级的方法:
//为线程设定优先级
public final void setPriority(int newPriority)
//获取线程的优先级
public final int getPriority()
示例:
public class RunnableDemo implements Runnable {
@Override
public void run() {
int nowPriority = Thread.currentThread().getPriority();
System.out.println("1.优先级:"+nowPriority); //1.优先级:5
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
nowPriority = Thread.currentThread().getPriority();
System.out.println("2.优先级:"+nowPriority); //2.优先级:10
}
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
thread.start();
}
}
2.3.2 守护线程和用户线程*
Java中的线程分为两种:守护线程和用户线程。任何线程都可以设置为守护线程和用户线程,通过方法setDaemon(true)可以把该线程设置为守护线程,反之则为用户线程。
用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程。
守护线程:运行在后台,为其他前台线程服务,比如垃圾回收线程,JIT(编译器)线程就可以理解为守护线程。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。
守护线程应该永远不去访问固有资源 ,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
注意事项:
setDaemon(true)必须在start()方法前执行,否则会抛出IllegalThreadStateException。
在守护线程中产生的新线程也是守护线程。
不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑。
守护线程中不能依靠finally块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作,所以守护线程中的finally语句块可能无法被执行。
设置和获取线程是否是守护线程的方法:
//设置线程是否为守护线程,true则把该线程设置为守护线程,反之则为用户线程
public final void setDaemon(boolean on)
//判断线程是否是守护线程
public final boolean isDaemon()
当程序中所有的用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出。
2.3.3 线程名称
相比于上面的两个属性,实际运用中,往往线程名称会被修改,目的是为了调试。获取和设置线程名称的方法:
//获取线程名称
public final String getName()
//设置线程名称
public final synchronized void setName(String name)
示例:
public class RunnableDemo implements Runnable {
@Override
public void run() {
String nowName = Thread.currentThread().getName();
System.out.println("1.线程名称:"+nowName); //1.线程名称:Thread-0
Thread.currentThread().setName("测试线程");
nowName = Thread.currentThread().getName();
System.out.println("2.线程名称:"+nowName); //2.线程名称:测试线程
}
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
thread.start();
}
}
2.4 线程的生命周期*
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。
2.4.1 从代码角度理解
在Thread类中,线程状态是一个枚举类型:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED
}
线程的状态可以通过public State getState()来获取,该方法的返回值是一个枚举类型,线程状态定义如下:
1、NEW
一个已创建而未启动(即没调用start方法)的线程处于该状态。
2、RUNNABLE
该状态可以被看成一个复合状态。它包括两个子状态:READY和RUNNING。前者表示处于该状态的线程可以被线程调度器进行调度而使之处于RUNNING状态,后者表示线程正在运行状态。
执行Thread.yield()的线程,其状态可能由RUNNING转换为READY。
3、BLOCKED
处于BLOCKED状态的线程并不会占处理器资源,当阻塞式IO操作完成后,或线程获得了其申请的资源,状态又会转换为RUNNABLE。
4、WAITING
一个线程执行了某些特定方法之后就会处于这种等待其他线程执行另外一些特定操作的状态。
能够使线程变成WAITING状态的方法包括:Object.wait()、Thread.join(),能够使线程从WAITING状态变成RUNNABLE状态的方法有:Object.notify()、Object.notifyAll()。
5、TIMED_WAITING
该状态和WAITING类似,差别在于处于该状态的线程是处于带有时间限制的等待状态。
当其他线程没有在特定时间内执行该线程所期待的特定操作时,该线程的状态自动转换为RUNNABLE。
6、TERMINATED
已经执行结束的线程处于该状态。
Thread.run()正常返回或由于抛出异常而提前终止都会导致相应线程处于该状态。
6种状态的转换:
2.4.2 从使用角度理解*
在实际开发中,往往将线程的状态理解为5种:新建、可运行、运行、阻塞、死亡。
1、新建(new)
新创建了一个线程对象。用new方法创建一个线程后,线程对象就处于新建状态。此时仅由JVM为其分配内存,并初始化其成员变量的值。
2、可运行(runnable)
线程对象创建后,当调用线程对象的start()方法,该线程处于就绪状态,但还没分配到CPU,处于线程就绪队列,等待系统为其分配CPU。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
3、运行(running)
可运行状态(runnable)的线程获得了CPU时间片,执行程序代码。
就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态。
4、阻塞(block)
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪(runnable)状态,才有机会再次被CPU调用以进入到运行状态。
阻塞的情况分三种:
等待阻塞(位于对象等待池中的阻塞)
运行状态中的线程执行wait()方法,JVM会把该线程放入等待队列中,使本线程进入到等待阻塞状态;
同步阻塞(位于对象锁池中的阻塞)
线程在获取synchronized同步锁失败(因为锁被其它线程所占用),则JVM会把该线程放入锁池中,线程会进入同步阻塞状态;
其他阻塞
通过调用线程的sleep()或 join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5、死亡(dead)
线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。
2.5 Thread类的常用方法
以下是Thread类中较常用的几个方法,并不包含线程间协作的方法(如await、notify等),这些方法的使用随后介绍。其中的yield方法并不常用,但常常拿来和sleep、await等方法进行比较,所以也介绍下。
方法 功能 备注
static Thread currentThread() 返回当前线程,即当前代码的执行线程
void run() 用于实现线程的任务处理逻辑 该方法由Java虚拟机直接调用
void start() 启动线程 调用该方法并不代表相应的线程已经被启动,线程是否启动是由虚拟机去决定的
void join() 等待相应线程运行结束 若线程A调用线程B的join方法,那么线程A的运行会被暂停,直到线程B运行结束
static void yield() 使当前线程主动放弃其对处理器的占用,这可能导致当前线程被暂停 这个方法是不可靠的,该方法被调用时当前线程仍可能继续运行
void interrupt() 中断线程
static void sleep(long millis) 使当前线程休眠(暂停运行)指定的时间
2.5.1 interrupt*
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。
调用interrupt()方法并不会中断一个正在运行的线程。也就是说处于Running状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
若调用sleep()而使线程处于TIMED-WATING状态,这时调用interrupt()方法,会抛出InterruptedException,从而使线程提前结束TIMED-WATING状态。
许多声明抛出InterruptedException的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位。
中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程thread的时候,可以调用thread.interrupt()方法,在线程的run方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。
2.5.1 interrupted*
测试当前线程是否已经中断。中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作,常常被用于线程间的协作。
其他线程可以调用指定线程的interrupt()方法对其进行中断操作,同时指定线程可以调用isInterrupted()来感知其他线程对其自身的中断操作,从而做出响应。
另外,也可以调用Thread的静态方法interrupted()对当前线程进行中断操作,该方法会清除中断标志位。需要注意的是,当抛出InterruptedException时候,会清除中断标志位,此时再调用isInterrupted,会返回false。
和中断相关的方法有3个:
方法名 详细解释 备注
public void interrupt() 中断一个线程 如果该线程被调用了Object wait/Object wait(long),或者被调用sleep(long),join()/join(long)方法时会抛出interruptedException并且中断标志位将会被清除
public boolean isinterrupted() 测试该线程对象是否被中断 中断标志位不会被清除
public static boolean interrupted() 查看当前中断信号是true还是false并且清除中断信号 中断标志位会被清除
关于interrupt和isinterrupted的使用,示例:
public class JavaTest {
public static void main(String[] args) throws InterruptedException {
//sleepThread睡眠1000ms
final Thread sleepThread = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
super.run();
}
};
//busyThread一直执行死循环
Thread busyThread = new Thread() {
@Override
public void run() {
while (true) ;
}
};
sleepThread.start();
busyThread.start();
sleepThread.interrupt();
busyThread.interrupt();
while (sleepThread.isInterrupted()) ;
System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
}
}
测试结果:
在上面的代码中,开启了两个线程分别为sleepThread和BusyThread, sleepThread睡眠1s,BusyThread执行死循环。然后分别对着两个线程进行中断操作,可以看出sleepThread抛出InterruptedException后清除标志位,而busyThread就不会清除标志位。
另外,可以通过中断的方式实现线程间的简单交互,因为可以通过isInterrupted()方法监控某个线程的中断标志位是否清零,针对不同的中断标志位进行不同的处理。
2.5.2 join*
join方法也是一种线程间协作的方式,很多时候,一个线程的输入可能非常依赖于另一个线程的输出。如果在一个线程threadA中执行了threadB.join(),其含义是:当前线程threadA会等待threadB线程终止后,threadA才会继续执行。
方法名 详细注释 备注
public final void join() throws InterruptedException 等待这个线程死亡。 如果任何线程中断当前线程,如果抛出InterruptedException异常时,当前线程的中断状态将被清除
public final void join(long millis) throws InterruptedException 等待这个线程死亡的时间最多为millis毫秒。
如果参数为 0,意味着永远等待。 如果millis为负数,抛出IllegalArgumentException异常
public final void join(long millis, int nanos) throws InterruptedException 等待最多millis毫秒加上这nanos纳秒。 如果millis为负数或者nanos不在0-999999范围抛出IllegalArgumentException异常
看个例子:
public class JoinDemo {
public static void main(String[] args) {
Thread previousThread = Thread.currentThread();
for (int i = 1; i <= 10; i++) {
Thread curThread = new JoinThread(previousThread);
curThread.start();
previousThread = curThread;
}
}
static class JoinThread extends Thread {
private Thread thread;
public JoinThread(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
System.out.println(thread.getName() + " terminated.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试结果:
在上面的例子中一个创建了10个线程,每个线程都会等待前一个线程结束才会继续运行。可以通俗的理解成接力,前一个线程将接力棒传给下一个线程,然后又传给下一个线程…
2.5.3 sleep*
public static native void sleep(long millis)
sleep是Thread的静态方法,它的作用是:让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。需要注意的是如果当前线程获得了锁,sleep方法并不会失去锁。
一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,让出cpu资源,但不释放对象锁,指定时间到后又恢复运行。作用:给其它线程执行机会的最佳方式。
Thread.sleep方法经常拿来与Object.wait()方法进行比较,sleep和wait两者主要的区别:
sleep()方法是Thread的静态方法,而wait是Object实例方法;
wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方使用。
wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;
sleep()方法在休眠时间达到后,如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。
关于sleep方法的使用,示例:
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("第一个线程的执行时间:"+new Date());
}
}).start();
System.out.println("sleep2秒");
Thread.sleep(2000);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("第二个线程的执行时间:"+new Date());
}
}).start();
}
结果示例:
可以看出,第2个线程的执行时间是晚于第1个线程2秒的。
2.5.4 yield
public static native void yield()
yield方法的作用:使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。
yield方法是一个静态方法,一旦执行,它会是当前线程让出CPU。但是,让出了CPU并不是代表当前线程不再运行了。线程调度器可能忽略此此消息,并且如果在下一次竞争中,又获得了CPU时间片当前线程依然会继续运行。另外,让出的时间片只会分配给大于等于当前线程优先级的线程。
在线程中,用priority来表示优先级,priority的范围从1~10。在构建线程的时候可以通过 setPriority(int) 方法进行设置,默认优先级为5,优先级高的线程相较于优先级低的线程优先获得处理器时间片。
需要注意的是,sleep()和yield()方法,同样都是当前线程会交出处理器资源,而它们不同的是,sleep()交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。而yield()方法只允许大于等于当前线程优先级的线程,竞争CPU时间片。
2.6 线程相关的一些问题
2.6.1 interrupt、interrupted和isInterrupted方法的区别*
interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。
调用目标线程的interrupt()方法,给目标线程发一个中断信号,线程被打上中断标记。
线程中断仅仅是设置线程的中断状态标识,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态标识被置为“中断状态”,就会抛出中断异常。
interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。判断目标线程是否被中断,不会清除中断标记。
isInterrupted:查看当前中断信号是true还是false。判断目标线程是否被中断,不会清除中断标记。
2.6.2 sleep方法和yield方法有什么区别*
1、sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
2、线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
3、sleep()方法声明抛出 InterruptedException(其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException),而 yield()方法没有声明任何异常;
4、sleep()方法比 yield()方法具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行;
5、建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性。
6、sleep()方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
7、sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
8、当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
2.6.3 线程怎么处理异常
如果线程运行中产生了异常,首先会生成一个异常对象。我们平时throw抛出异常,就是把异常交给JVM处理。JVM首先会去找有没有能够处理该异常的处理者(首先找到当前抛出异常的调用者,如果当前调用者无法处理,则会沿着方法调用栈一路找下去),能够处理的调用者实际就是看方法的catch关键字,JVM会把该异常对象封装到catch入参,允许开发者手动处理异常。
若找不到能够处理的处理者(实际就是没有手动catch异常,比如未受检异常),就会交该线程处理;JVM会调用Thread类的dispatchUncaughtException()方法,该方法调用了getUncaughtExceptionHandler(),uncaughtExceptoin(this,e)来处理了异常,如果当前线程设置了自己的UncaughtExceptionHandler,则使用该handler,调用自己的uncaughtException方法。如果没有,则使用当前线程所在的线程组的Handler的uncaughtExceptoin()方法,如果线程中也没有设置,则直接把异常定向到System.err中,打印异常信息(控制台红色字体输出的异常就是被定向到System.err的异常)。
2.6.4 Thread.sleep(0)的作用是什么*
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
Thread.sleep(0) 和 Thread.yield()
Thread.sleep(0) 和 Thread.yield() 主要取决于JVM的实现。这两种方式都可以让出cpu时间片,以允许其他线程获取运行的机会。
不同的地方:
1、sleep()方法给其他线程运行机会的时候,不考虑线程的优先级,因此当高优先级线程sleep()后,低优先级任务有机会运行;但是yield()只会给同优先级或更高优先级线程运行的机会,甚至可能是自己继续运行。
2、线程调用sleep()后,转入阻塞状态,而调用yield()后转入了就绪状态。
3、sleep方法声明抛出InterruptedException,而yield没有声明任何异常。
2.6.5 一个线程如果出现了运行时异常会怎么样
如果这个异常没有被捕获的话,这个线程就停止执行了。
另外重要的一点是:如果这个线程持有某个对象的监视器器,那么这个对象监视器器会被立即释放。
2.6.6 终止线程运行的几种情况
线程体中调用了yield方法让出了对CPU的占用权利;
线程体中调用了sleep方法使线程进入睡眠状态;
线程由于IO操作受到阻塞;
另外一个更高优先级线程出现,导致当前线程未分配到时间片;
在支持时间片的系统中,该线程的时间片用完。
使用stop方法强行终止,但是不推荐这个方法,因为stop是过期作废的方法。
使用interrupt方法中断线程。
有两种情况。
1、线程处于阻塞状态:当调用线程的 interrupt()方法时,会抛出InterruptException异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。
2、线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true。示例:
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
try{
Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕获到异常之后,执行 break 跳出循环
}
}
}
}
2.6.7 如何优雅地设置睡眠时间
JDK1.5之后,引入了一个枚举TimeUnit,对sleep方法提供了很好的封装。
比如要休眠2小时22分55秒899毫秒,两种写法:
Thread.sleep(8575899);
TimeUnit.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(22);
TimeUnit.SECONDS.sleep(55);
TimeUnit.MILLISECONDS.sleep(899);
2.6.8 如何设置上下文类加载器
获取线程上下文类加载器:
public ClassLoader getContextClassLoader()
设置线程类加载器(可以打破Java类加载器的父类委托机制):
public void setContextClassLoader(ClassLoader cl)
2.6.9 如何停止一个正在运行的线程
当run方法完成后线程自动终止
使用stop方法强行终止
不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
可以使用共享变量的方式
在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的。线程用来作为是否中断的信号,通知中断线程的执行。
在一般情况下,在 run 方法执行完毕的时候,线程会正常结束。然而,有些线程是后台线程,需要长时间运行,只有在系统满足某些特殊条件后,才能退出这些线程。这时可以使用一个变量来控制循环,比如设置一个 Boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while 循环是否退出。
public class ThreadDemo extends Thread {
public volatile boolean exit = false;
@Override
public void run() {
while (!exit) {
//业务逻辑代码
}
}
}
使用interrupt方法终止线程
如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?建议是不要使用stop()方法,而是使用Thread提供的interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,退出堵塞代码。
当线程处于阻塞状态时,调用线程的interrupt()实例方法,线程内部会触发InterruptedException异常,并且会清除线程内部的中断标志(即将中断标志置为false)。
示例:
class MyThread extends Thread {
volatile boolean stop = false;
public void run() {
while (!stop) {
System.out.println(getName() + " is running");
try {
sleep(1000);
} catch (InterruptedException e) {
System.out.println("week up from blcok...");
stop = true; // 在异常处理代码中修改共享变量的状态
}
}
System.out.println(getName() + " is exiting...");
}
}
class InterruptThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
MyThread m1 = new MyThread();
System.out.println("Starting thread...");
m1.start();
Thread.sleep(3000);
System.out.println("Interrupt thread...: " + m1.getName());
m1.stop = true; // 设置共享变量为true
m1.interrupt(); // 阻塞时退出阻塞状态
Thread.sleep(3000); // 主线程休眠3秒以便观察线程m1的中断情况
System.out.println("Stopping application...");
}
}
2.6.10 为什么Thread类的sleep()和yield()方法是静态的
Thread类的sleep()和yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。
2.6.11 怎么检测一个线程是否拥有锁
在Thread类中有一个静态方法叫holdsLock(Object o),返回true表示:当且仅当当前线程拥有某个具体对象的锁。
2.6.12 线程的调度策略
线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:
1、线程体中调用了yield方法让出了对CPU的占用权利;
2、线程体中调用了sleep方法使线程进入睡眠状态;
3、线程由于IO操作受到阻塞;
4、另外一个更高优先级线程出现;
5、在支持时间片的系统中,该线程的时间片用完。
2.6.13 线程的调度策略
1)使用top命令查找java命令下cpu占用最高的进程:
例如pid为9595的进程是占用cpu使用率最大的。
2)使用top -H -p 9595查看当前pid为9595进程下各线程占用cpu情况:
可以看到,pid为10034的线程占用cpu是最高的。
3)将线程的pid由10进制转成16进制:
4)把进程的全部堆栈信息导入到临时文件中:
jstack 9595 > /tmp/a.txt
5)通过vi /tmp/a/txt查看该文件:
2.6.14 join可以保证线程执行顺序的原理
Thread的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");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
可以看到,有一个long类型参数的join()方法使用了synchroinzed修饰,说明这个方法同一时刻只能被一个实例或者方法调用。由于,传递的参数为0,所以,程序会进入如下代码逻辑。
if (millis == 0) {
while (isAlive()) {
wait(0);
}
}
首先,在代码中以while循环的方式来判断当前线程是否已经启动处于活跃状态,如果已经启动处于活跃状态,则调用同类中的wait()方法,并传递参数0。继续跟进wait()方法,如下所示。
public final native void wait(long timeout) throws InterruptedException;
wait()方法是一个本地方法,通过JNI的方式调用JDK底层的方法来使线程等待执行完成。
调用线程的wait()方法时,会使主线程处于等待状态,等待子线程执行完成后再次向下执行。也就是说,在ThreadSort02类的main()方法中,调用子线程的join()方法,会阻塞main()方法的执行,当子线程执行完成后,main()方法会继续向下执行,启动第二个子线程,并执行子线程的业务逻辑,以此类推。
2.6.15 stop()方法和interrupt()方法的区别
1、stop()方法
stop()方法会真的杀死线程。如果线程持有ReentrantLock锁,被stop()的线程并不会自动调用ReentrantLock的unlock()去释放锁,那其他线程就再也没机会获得ReentrantLock锁, 这样其他线程就再也不能执行ReentrantLock锁锁住的代码逻辑。
2、interrupt()方法
interrupt()方法仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。被interrupt的线程,有两种方式接收通知:一种是异常, 另一种是主动检测。
1)通过异常接收通知
当线程A处于WAITING、 TIMED_WAITING状态时, 如果其他线程调用线程A的interrupt()方法,则会使线程A返回到RUNNABLE状态,同时线程A的代码会触发InterruptedException异常。线程转换到WAITING、TIMED_WAITING状态的触发条件,都是调用了类似wait()、join()、sleep()这样的方法, 我们看这些方法的签名时,发现都会throws InterruptedException这个异常。这个异常的触发条件就是:其他线程调用了该线程的interrupt()方法。
当线程A处于RUNNABLE状态时,并且阻塞在java.nio.channels.InterruptibleChannel上时, 如果其他线程调用线程A的interrupt()方法,线程A会触发java.nio.channels.ClosedByInterruptException这个异常;当阻塞在java.nio.channels.Selector上时,如果其他线程调用线程A的interrupt()方法,线程A的java.nio.channels.Selector会立即返回。
2)主动检测通知
如果线程处于RUNNABLE状态,并且没有阻塞在某个I/O操作上,例如中断计算基因组序列的线程A,此时就得依赖线程A主动检测中断状态了。如果其他线程调用线程A的interrupt()方法, 那么线程A可以通过isInterrupted()方法, 来检测自己是不是被中断了。
2.6.16 有三个线程T1,T2,T3,如何保证顺序执行*
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。
public class JoinTest {
// 1.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行
public static void main(String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1");
}
});
final Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 引用t1线程,等待t1线程执行完
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 引用t2线程,等待t2线程执行完
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}
});
t3.start();//这里三个线程的启动顺序可以任意,大家可以试下!
t2.start();
t1.start();
}
}
2.6.17 线程中断是否能直接调用stop
Java提供的终止方法只有一个stop,但是不建议使用此方法。
stop方法是过时的,已经过时的方式不建议采用。
stop方法会导致代码逻辑不完整。stop方法是一种"恶意"的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,这是非常危险的。
三、线程的活性故障
线程活性故障是由于资源稀缺性或程序自身的问题和缺陷导致线程一直处于非RUNNABLE状态,或者线程虽然处于RUNNABLE状态,但是其要执行的任务却一直无法进展的故障现象。
3.1 死锁*
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
如图所示,线程A持有资源2,线程B持有资源1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态:
3.1.1 死锁的产生条件*
当线程产生死锁时,这些线程及相关的资源将满足如下全部条件:
1、互斥
一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放。
2、请求与保持(不主动释放)条件
一个线程(进程)因请求被占用资源(锁)而发生阻塞时,对已获得的资源保持不放。
3、不剥夺(不能被强占)条件
线程(进程)已获得的资源,在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
4、循环等待(互相等待)条件
当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞。
用一句话该概括:
两个或多个线程持有并且不释放独有的锁,并且还需要竞争别的线程所持有的锁,导致这些线程都一直阻塞下去。
这些条件是死锁产生的必要条件而非充分条件,也就是说只要产生了死锁,那么上面的这些条件一定同时成立,但是上述条件即便同时成立也不一定产生死锁。
可能产生死锁的特征就是在持有一个锁的情况下去申请另外一个锁,通常是锁的嵌套,示例:
//内部锁
public void deadLockMethod1(){
synchronized(lockA){
//...
synchronized(lockB){
//...
}
}
}
//显式锁
public void deadLockMethod2(){
lockA.lock();
try{
//...
lockB.lock();
try{
//...
}finally{
lockB.unlock();
}
}finally{
lockA.unlock();
}
}
示例:
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
结果:
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
线程A通过synchronized (resource1)获得resource1的监视器锁,然后让线程A休眠1s,为的是让线程B得到执行然后获取到resource2的监视器锁。线程A和线程B休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。
3.1.2 死锁的规避*
由上文可知,要产生死锁需要同时满足四个条件。所以,只要打破其中一个条件就可以避免死锁的产生(第一个条件 “互斥” 是不能破坏的,因为加锁就是为了保证互斥)。常用的规避方法有如下几种:
1、粗锁法
用一个粒度较粗的锁替代原来的多个粒度较细的锁,这样涉及的线程都只需要申请一个锁从而避免了死锁。粗锁法的缺点是它明显降低了并发性并可能导致资源浪费。
2、锁排序法
相关线程使用全局统一的顺序申请锁。假设有多个线程需要申请锁(资源),那么只需要让这些线程依照一个全局(相对于使用这种资源的所有线程而言)统一的顺序去申请这些资源,就可以消除“循环等待资源”这个条件,从而规避死锁。一般,可以使用对象的hashcode作为资源的排序依据。
3、使用ReentrantLock.tryLock(long timeout, TimeUnit unit) 申请锁
ReentrantLock.tryLock(long timeout, TimeUnit unit) 允许为申请锁这个操作加上一个超时时间。在超时事件内,如果相应的锁申请成功,该方法返回true。如果在tryLock执行的那一刻相应的锁正在被其他线程持有,那么该方法会使当前线程暂停,直到这个锁申请成功(此时该方法返回true)或者等待时间超过指定的超时时间(此时该问题返回false)。因此,使用ReentrantLock.tryLock(long timeout, TimeUnit unit) 来申请锁可以避免一个线程无限制地等待另外一个线程持有的资源,从而最终能够消除死锁产生的必要条件中的“占用并等待资源”。示例:
boolean locked = false;
try {
locked = lock.tryLock(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(locked) lock.unlock();
}
4、使用锁的替代品
使用一些锁的替代品(无状态对象、线程特有对象以及volatile关键字等),在条件允许的情况下,使用这些替代品在保障线程安全的前提下不仅能够避免锁的开销,还能够直接避免死锁。
3.2 线程饥饿和活锁
线程饥饿是指一直无法获得其所需的资源而导致任务一直无法进展的一种活性故障。
线程饥饿的一个典型例子是在争用的情况下使用非公平模式的读写锁。此种情况下,可能会导致某些线程一直无法获取其所需的资源(锁),即导致线程饥饿。
把锁看作一种资源的话,其实死锁也是一种线程饥饿。死锁的结果是故障线程都无法获得其所需的全部锁中的一个锁,从而使其任务一直无法进展,这相当于线程无法获得其所需的全部资源(锁)而使得其任务一直无法进展,即产生了线程饥饿。由于线程饥饿的产生条件是一个(或多个)线程始终无法获得其所需的资源,显然这个条件的满足并不意味着死锁的必要条件(而不是充分条件)的满足,因此线程饥饿并不会导致死锁。
Java中导致饥饿的原因:
高优先级线程抢占了所有的低优先级线程的 CPU 时间。
线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。
线程饥饿涉及的线程,其生命周期不一定就是WAITING或BLOCKED状态,其状态也可能是RUNNING(说明涉及的线程一直在申请宁其所需的资源),这时饥饿就会演变成活锁。
活锁指线程一直处于运行状态,但是其任务却一直无法进展的一种活性故障。
3.3 死锁与活锁的区别,死锁与饥饿的区别
死锁
是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
活锁
任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,却一直获得不了锁。
饥饿
一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
1、活锁与死锁的区别
活锁和死锁的区别在于:处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。活锁可以认为是一种特殊的饥饿。
2、死锁活锁与饥饿的区别
进程会处于饥饿状态是因为持续地有其它优先级更高的进程请求相同的资源。不像死锁或者活锁,饥饿能够被解开。例如,当其它高优先级的进程都终止时并且没有更高优先级的进程强占资源。

浙公网安备 33010602011771号