总结自《Java编程思想》第21章并发(P650~)
1.进程、线程含义
- 进程:一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是系统进行资源分配的基本单位,运行在它自己的地址空间内的自包容的程序,它是由一组机器指令、数据和堆栈等组成的,是一个能独立运行的活动实体。
- 线程:线程是进程中的一个实体,作为系统调度和分派的基本单位.
1.1线程与进程的比较
引用自《计算机操作系统》第四版,P76
类别 | 进程 | 线程 |
---|---|---|
调度基本单位 | 传统OS中进程是调度的基本单位 | 引入线程的OS中,线程是调度的基本单位 |
并发性 | 进程间可并发执行 | 进程中的多个线程可并发执行 |
拥有资源 | 系统拥有资源的一个基本单位 | 不拥有系统资源,仅拥有保证独立运行的少量资源 |
独立性 | 进程拥有自己独立的地址空间和其它资源,其它进程不可访问 | 同一进程的不同线程共享进程的内存地址空间和资源 |
系统开销 | 创建与撤消进程时,OS需要大量的开销 | OS创建开销小,并且上下文切换远比进程快 |
支持多处理系统 | 传统单线程进程只能运行在一个处理机上 | 多线程进程可将进程的多个线程分配到多个处理机上并发执行 |
通信 | 管道、FIFO、消息队列、信号量、共享存储、Socket | 线程共享同一进程中的内存与资源,可直接通信 |
状态 | 创建、就绪、阻塞、执行、终止 | 新建、就绪、阻塞、死亡 |
1.2总结
进程与线程都是顺序执行程序,一个线程就是在进程中的一个单一的顺序控制流。进程与线程都是可以解决并发编程的问题,线程是轻量级的进程,线程诞生于进程中。线程的底层机制是切分CPU时间,CPU将轮流给每个任务分配其占用时间
2.线程驱动任务执行
2.1 任务的概念
Java中实现了接口java.lang.Runable的类就可以称之为任务,其实现方法run()中编写的即是需要并发执行的代码。
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
2.2 线程的简单实现
线程在Java中只有一个类表示,那就是java.lang.Thread,简单启动一个线程的代码如下
fun main(args: Array<String>) {
println(Thread.currentThread().name)
val t = Thread(Runnable {
print("ThreadName = ${Thread.currentThread().name} it's other thread")
})
t.start()
}
/*output
main
ThreadName = Thread-0 it's other thread
*/
创建Thread时在其构造函数中传入一个Runable类参数,当线程调用start()方法时,即会驱动Runable中代码段的执行
2.3线程与任务的关系
线程不是任务,这两者的概念一定要区分清楚。Thread类自身不执行任何操作,它只是驱动赋予它的任务。
下面我将用一张图来通俗地讲解一个线程驱动任务运行的过程
- 将任务比做一个集装箱,而线程则比做一个无动力货船,这艘货船设定一次只能装载一个集装箱
- 集装箱要想被运送到目的地,必须得装入货船中。要首先必须要有一个货船,货船是调度的基本单位,它不能自己运行,只能通过动力拖船来将货船拉至目的地码头,而动力拖船也只能拖动货船而不能直接运送集装箱。对应创建一个Thread对象,在构造器内传入一个任务(即Runable对象),这样CPU才能够执行Thread中的任务。
- 货船装好集装箱后,要做一些被动力船拖运的准备,比如拉起锚,人员就绪,扬帆等,让动力拖船随时都可以来拖动该船。对应Thread实例对象调用start(),表明该线程执行必须的初始化操作,之后线程调度机制即进行该线程的CPU时间片分配,让线程中的任务能被执行。
- 动力拖船会受调度中心的指挥先拖A船一段时间,再拖B船一段时间,后再拖C船一段时间,最终将三艘船都拖至码头结束三个集装箱的运送任务。对应线程调度让CPU执行线程A中的任务多少时间,B和C各多少时间,这个时间段是不确定的,由线程调度策略决定,让每个线程都会分配至数量合理的时间去驱动它的任务。
- 在只有一艘动力拖船的情况下,在动力拖船拉货船至目的码头的过程中,具体某一时刻只能有一艘货船在被拖动。对应单CPU环境中,某一时刻只能有一个线程在执行,虽然表面看上去任务是并发执行的,实际上依然是顺序执行的。在多核CPU的环境中,线程会被分配至不同的CPU中执行,实现真正意义上的任务并发执行。Java线程调度的机制是抢占式的,所以CPU会周期性地中断线程,将上下文切换到另一个线程,为每个线程提供时间片。
3.线程的多种实现方式
3.1 自定义类继承自Thread类
3.1.1 传统方式
继承类通用重写thread中的run方法,然后生成实例调用start(),即可运行一个线程
3.1.2 聚合方式
在传统方式中将start()的调用放至自定义类中的构造方法中执行,从而在生成实例时即开始运行一个线程。
实例如下
class SimpleThread : Thread{
private var countDown = 5
companion object {
private var threadcount = 0
}
constructor():this(Integer.toString(++threadcount))
constructor(name: String): super(name)
init {
start() //聚合方式
}
override fun toString(): String {
return "#$name($countDown)"
}
override fun run() {
while (true){
print(this)
if(--countDown == 0) return
}
}
}
fun main(args: Array<String>) {
for(i in 1..5){
SimpleThread()
//SimpleThread().start() 传统方式启动线程
}
}
/*output 输出结果是不确定的,有各种可能
#2(5)#2(4)#1(5)#2(3)#3(5)#2(2)#1(4)#1(3)#4(5)#2(1)#5(5)#3(4)#5(4)#4(4)#1(2)#4(3)#5(3)#3(3)#5(2)#4(2)#1(1)#4(1)#5(1)#3(2)#3(1)
or
1(5)#2(5)#3(5)#1(4)#2(4)#1(3)#3(4)#1(2)#4(5)#2(3)#4(4)#1(1)#3(3)#4(3)#2(2)#4(2)#3(2)#4(1)#2(1)#3(1)#5(5)#5(4)#5(3)#5(2)#5(1)
*/
3.2 实现Runable接口
3.2.1 传统方式
自定义类实现Runable接口,重写run方法,然后创建一个Thread类对象,在构造器中传入一个Runable的对象,最后调用Thread对象的start()运行线程,如2.2代码所示
3.2.2 聚合方式
在自定义类中持有一个Thread对象,然后在构造器中调用Thread#start()运行线程,即实现一个自管理的Runable.
实例如下:
class SelfManaged: Runnable{
private var countDown = 5
private var t = Thread(this)
init {
t.start()
}
override fun toString(): String {
return "${Thread.currentThread().name}($countDown)"
}
override fun run() {
while (true){
print(this)
if(--countDown == 0){
return
}
}
}
}
fun main(args: Array<String>) {
for(i in 1..5){
SelfManaged()
}
}
/*output
Thread-1(5)Thread-2(5)Thread-0(5)Thread-2(4)Thread-1(4)Thread-2(3)Thread-0(4)Thread-2(2)Thread-1(3) Thread-2(1)Thread-0(3)Thread-1(2)Thread-0(2)Thread-1(1)Thread-0(1)Thread-3(5)Thread-3(4)Thread-3(3) Thread-3(2)Thread-3(1)Thread-4(5)Thread-4(4)Thread-4(3)Thread-4(2)Thread-4(1)
*/
3.3 通过内部类来将线程代码隐藏在类中,即是在类中再放置一个上述内部线程类用以实现线程并启动。
class InnerThread(name: String){
private var countDown = 5
private var inner1:Inner
init {
inner1 = Inner(name)
}
private inner class Inner : Thread{
constructor(name:String): super(name){
start()
}
override fun run() {
try {
while (true){
println(this)
if(--countDown == 0 ) return
sleep(10)
}
}catch (e:InterruptedException){
}
}
override fun toString(): String {
return "$name:$countDown"
}
}
}
fun main(args: Array<String>) {
InnerThread("InnerThread")
}
/*output
InnerThread:5
InnerThread:4
InnerThread:3
InnerThread:2
InnerThread:1
*/
4.Thread类简单分析
4.1 实现了Runable接口
Thread实现了Runable接口,所以可通过继承Thread重写run方法的方式来实现多线程。
public class Thread implements Runnable {}
4.2 线程名称
- 线程名字是Thread的一个标识,用下述字段表示
private volatile char name[];
- 通过setName()与getName()进行名称的设置与获取。
- 通过静态方法
Thread.currentThread()
来获取当前运行的线程对象 - Thread重写了toString(),输出的内容为Thread[线程名称,线程优先级,线程组名]
public String toString() {
ThreadGroup group = getThreadGroup();
if (group != null) {
return "Thread[" + getName() + "," + getPriority() + "," +
group.getName() + "]";
} else {
return "Thread[" + getName() + "," + getPriority() + "," +
"" + "]";
}
}
4.3 优先级
- Thread类中用以表示优先级的字段
private int priority;
- 通过
setPriority()、getPriority()
进行线程优先级的设置与获取。 - Java中的线程优先级一共有10级,与多数操作系统都不能映射地很好,所以调整优先级时一般只使用
MAX_PRIORITY,NORM_PRIORITY,MIN_PRIORITY
这三种级别,默认是NORM_PRIORITY = 5
。 - 优先级高的会让调度器倾向先执行,优先级低的线程并不会得不到执行,只是执行的频率较低,一般不建议操纵线程优先级
4.4 后台(daemon)线程
- 后台线程是指在程序运行的时候在后台提供一种通用服务的线程,并且这些线程并不属于程序中不可或缺的部分。
- 因此当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。
- 反过来说,只要有任何非后台线程还在运行,程序就不会终止。执行main()的就是一个非后台线程。
- 由后台线程所创建的任何线程都将被自动设置成后台线程。
实例如下:
class SimpleDaemons: Runnable{
override fun run() {
try {
while (true){
TimeUnit.MILLISECONDS.sleep(100)
println("${Thread.currentThread()} $this")
}
}catch (e: InterruptedException){
println("sleep() interrupted")
}
}
}
fun main(args: Array<String>) {
for(i in 0 until 10){
val daemon = Thread(SimpleDaemons())
daemon.isDaemon = true //设置线程为后台线程,必须在Start之前调用
daemon.start()
}
println("All daemons started")
TimeUnit.MILLISECONDS.sleep(99) //当睡眠时间为99ms时小于子线程的睡眠时间100ms时,所有后台子线程都不会执行
}
/**output
99ms
All daemons started
100ms 有可能执行全部或部分子线程,也可能都不执行
All daemons started
Thread[Thread-8,5,main] mutilthread.SimpleDaemons@1857af2f
Thread[Thread-9,5,main] mutilthread.SimpleDaemons@2afd6006
Thread[Thread-7,5,main] mutilthread.SimpleDaemons@4c630b0d
Thread[Thread-6,5,main] mutilthread.SimpleDaemons@61e0162
Thread[Thread-5,5,main] mutilthread.SimpleDaemons@21f189d9
Thread[Thread-4,5,main] mutilthread.SimpleDaemons@488a7f84
*/
结论是当main线程停止执行时程序退出,所有的后台线程都跟着一起终止不会得到执行