并发[0]-二档起步:线程
文章目录
前置(所谓“二挡起步”)
- JVM内存模型
- 基本线程概念认知
线程的生命周期
状态 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用 start()方法 |
RUNNABLE | 运行状态,Java 线程将操作系统中的就绪和运行两种状态笼统地称作“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
TIME_WAITING | 超时等待状态,该状态不同于 WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
上下文切换
CPU在运行一个线程的过程中,转而去运行另外一个线程,这个叫做线程 上下文切换(对于进程也是类似)。
一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
实质上, 线程的上下文切换就是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行,这正是有程序计数器所支持的。
线程的创建
1 | public class ThreadTest { |
start()方法的作用是通知 “线程规划器” 该线程已经准备就绪,以便让系统安排一个时间来调用其 run()方法,也就是使线程得到运行。
Thread 类详解
start 方法
start() 用来启动一个线程,当调用该方法后,相应线程就会进入就绪状态,该线程中的run()方法会在某个时机被调用。
run 方法
run()方法是不需要用户来调用的。当通过start()方法启动一个线程之后,一旦线程获得了CPU执行时间,便进入run()方法体去执行具体的任务。注意,创建线程时必须重写run()方法,以定义具体要执行的任务。
一般来说,有两种方式可以达到重写run()方法的效果:
- 直接重写:直接继承Thread类并重写run()方法;
- 间接重写:通过Thread构造函数传入Runnable对象 (注意,实际上重写的是 Runnable对象 的run() 方法)。
sleep 方法
作用是在指定的毫秒数内让当前正在执行的线程(即currentThread()
方法所返回的线程)睡眠,并交出 CPU 让其去执行其他的任务。当线程睡眠时间满后,不一定会立即得到执行,因为此时 CPU 可能正在执行其他的任务。所以说,调用sleep方法相当于让线程进入阻塞状态。该方法有如下两条特征:
- 如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出;
- sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
yield 方法
调用 yield()方法会让当前线程交出CPU资源,让CPU去执行其他的线程。但是yield()不能控制具体的交出CPU的时间。
- yield()方法只能让 拥有相同优先级的线程 有获取 CPU 执行时间的机会;
- 调用yield()方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新得到 CPU 的执行;
- 它同样不会释放锁。
join 方法
假如在main线程中调用thread.join方法,则main线程会等待thread线程执行完毕或者等待一定的时间。详细地,如果调用的是无参join方法,则等待thread执行完毕;如果调用的是指定了时间参数的join方法,则等待一定的时间。join()方法有三个重载版本:
1 | public final synchronized void join(long millis) throws InterruptedException {...} |
join()方法是通过wait()方法 (Object 提供的方法) 实现的。当 millis == 0 时,会进入 while(isAlive())
循环,并且只要子线程是活的,宿主线程就不停的等待。 wait(0)
的作用是让当前线程(宿主线程)等待,而这里的当前线程是指 Thread.currentThread()
所返回的线程。所以,虽然是子线程对象(锁)调用wait()方法,但是阻塞的是宿主线程。
- join方法同样会会让线程交出CPU执行权限;
- join方法同样会让线程释放对一个对象持有的锁;
- 如果调用了join方法,必须捕获InterruptedException异常或者将该异常向上层抛出。
interrupt 方法
单独调用interrupt方法可以使得 处于阻塞状态的线程 抛出一个异常,也就是说,它可以用来中断一个正处于阻塞状态的线程;
另外,通过 interrupted()方法 和 isInterrupted()方法 可以停止正在运行的线程。
直接调用interrupt() 方法不能中断正在运行中的线程。但是,如果配合 isInterrupted()/interrupted() 能够中断正在运行的线程,因为调用interrupt()方法相当于将中断标志位置为true,那么可以通过调用isInterrupted()/interrupted()判断中断标志是否被置位来中断线程的执行。
但是,一般情况下,不建议通过这种方式来中断线程,一般会在MyThread类中增加一个 volatile 属性 isStop 来标志是否结束 while 循环,然后再在 while 循环中判断 isStop 的值。
1 | // interrupt 阻断线程 |
1 | // 退出标记 阻断线程 |
stop 方法
stop() 方法已经是一个废弃的方法,它是一个 不安全的 方法。因为调用 stop() 方法会直接终止run方法的调用,并且会抛出一个ThreadDeath错误,如果线程持有某个对象锁的话,会完全释放锁,导致对象状态不一致。所以, stop() 方法基本是不会被用到的。
线程常用操作
获得代码调用者信息
currentThread()
方法返回代码段正在被哪个线程调用的信息。
判断线程是否处于活动状态
isAlive()
的功能是判断调用该方法的线程是否处于活动状态。其中,活动状态指的是线程已经 start (无论是否获得CPU资源并运行) 且尚未结束。
获取线程唯一标识
getId()
的作用是取得线程唯一标识,由JVM自动给出。
线程名称 getName 和 setName
用来得到或者设置线程名称。如果我们不手动设置线程名字,JVM会为该线程自动创建一个标识名,形式为: Thread-数字。
优先级 getPriority 和 setPriority
在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程。设置线程优先级有助于帮助 “线程规划器” 确定在下一次选择哪个线程来获得CPU资源。特别地,在 Java 中,线程的优先级分为 1 ~ 10 这 10 个等级,如果小于 1 或大于 10,则 JDK 抛出异常 IllegalArgumentException ,该异常是 RuntimeException 的子类,属于不受检异常。JDK 中使用 3 个常量来预置定义优先级的值,如下:
1 | public static final int MIN_PRIORITY = 1; |
- 线程优先级的继承性
在 Java 中,线程的优先级具有继承性,比如 A 线程启动 B 线程, 那么 B 线程的优先级与 A 是一样的。 - 线程优先级的规则性和随机性
线程的优先级具有一定的规则性,也就是CPU尽量将执行资源让给优先级比较高的线程。特别地,高优先级的线程总是大部分先执行完,但并不一定所有的高优先级线程都能先执行完。
守护线程 (Daemon)
当进程中不存在非守护线程时,则守护线程自动销毁,典型的守护线程就是垃圾回收线程。
只要当前JVM实例中存在任何一个非守护线程没有结束,守护线程就在工作;只有当最后一个非守护线程结束时,守护线程才随着JVM一同结束工作。
小结
对于上述线程的各项基本操作,其 所操作的对象 满足:
- 若该操作是静态方法,也就是说,该方法属于类而非具体的某个对象,那么该操作的作用对象就是 currentThread() 方法所返回 Thread 对象;
- 若该操作是实例方法,也就是说,该方法属于对象,那么该操作的作用对象就是调用该方法的 Thread 对象。
对于上述线程的各项基本操作,有:
- 线程一旦被阻塞,就会释放 CPU;
- 当线程出现异常且没有捕获处理时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。
- 对于一个线程,CPU 的释放 与 锁的释放没有必然联系。