文章目录
1 基本概念
进程:计算机分配资源(cpu,内存,磁盘io)的最小单位。进程间相互独立。
线程:cpu 调度,或者说执行任务的最小单位。线程必须依赖进程存在。
线程生命周期:
- new:新建线程;
- ready/runnable:线程已就绪,等待线程调度器的调度、分配 cpu 资源来执行任务;
- running:线程已获得 cpu 资源,正在执行任务;
- blocked:线程阻塞,不占用 cpu,但也不同于就绪状态;
- terminated:线程终止。
线程上下文:用于保证线程再次获得处理器使用权时,能知道上次执行到哪。一般包括:通用寄存器和程序计数器的内容。
线程上下文切换:一个线程被剥夺处理器使用权,另一个线程被线程调度器选中获得处理器使用权的过程。
线程上下文切换分类:
- 自发性切换:线程自己让出处理器使用权;
- 非自发性切换:线程被迫交出处理器使用权。
线程上下文切换开销:
- 操作系统保存和恢复上下文,主要是时间开销;
- 线程调度器进行线程调度,如按照规则计算出由哪个线程获得处理器使用权;
- 处理器高速缓存重新加载,线程在另一个未执行过此线程的处理器上恢复执行,处理器需要从内存或其它处理器加载数据;
- 可能会冲刷一级高速缓存的内容到二级缓存或内存。
一些线程调度算法(主要考量:吞吐量、平均响应时间、公平性、切换开销):
- First In First Out(FIFO,先进先出):先进入队列的先执行,执行完成退出;
- Shortest Job First(SJF,最短耗时任务优先):耗时短的任务先执行;
- Round Robin(RR,时间片轮转):每个任务轮流执行相同的时间;
- Max-Min Fairness(最大最小公平算法);
- Multi-level Feedback Queue(MFQ)
串行:多个任务依次执行,单核单线程;
并发:多个任务交替执行,单核多线程或多核多线程;
并行:多个任务同时执行,多核多线程,且这些线程分布在不同的核心。
并发编程的好处:
- 充分利用 cpu 资源;
- 缩短响应时间;
并发编程需要注意:
- 线程安全;
- 死锁;
- 线程太多导致死机。
2 java 并发编程基础
2.1 java Thread 基本 API
2.1.1 创建线程
2.1.1.1 new Thread()
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": my thread");
}
public static void main(String[] args) {
Thread thread = new MyThread();
}
}
2.1.1.2 new Thread(new Runnable())
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": my thread");
}
});
}
2.1.1.3 new Thread(new FutureTask<>(new Callable<>()))
public static void main(String[] args) {
Thread thread = new Thread(new FutureTask<>(new Callable<Object>() {
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName() + ": my thread");
return null;
}
}));
}
java 创建线程 API 仅仅是在内存中创建出 Thread 实例,操作系统中并无线程。
2.1.2 启动线程
thread.start();
调用 JNI,最终调用操作系统函数创建线程,进入Ready
状态,等待调度。
注意:只能调用一次,第二次会报错。
2.1.3 终止线程
- run 方法执行完成;
- run 方法执行抛出异常;
- 调用 stop 方法。
需要注意的是,stop 方法已被弃用了。因为 stop 方法终止线程过于暴力,不管被 stop 的线程在干什么,一旦被 stop 它就得立刻停止执行,然后释放锁,这是非常不安全的。
详情参看《让Thread#stop方法无法终止你的线程》。
2.1.4 线程运行
线程运行过程中包含两个状态:Ready 和 Running。
线程从 Ready 状态到 Running 状态是操作系统调度的,线程自己是控制不了的。
线程从 Running 状态到 Ready 状态,有三种途径:
- 操作系统调度,收回当前线程的 cpu 资源,分配给其它线程;
- 调 Thread#yield 方法;
- 调 sleep(0);
2.1.4.1 yield 方法
/**
* A hint to the scheduler that the current thread is willing to yield
* its current use of a processor. The scheduler is free to ignore this
* hint.
*
* <p> Yield is a heuristic attempt to improve relative progression
* between threads that would otherwise over-utilise a CPU. Its use
* should be combined with detailed profiling and benchmarking to
* ensure that it actually has the desired effect.
*
* <p> It is rarely appropriate to use this method. It may be useful
* for debugging or testing purposes, where it may help to reproduce
* bugs due to race conditions. It may also be useful when designing
* concurrency control constructs such as the ones in the
* {@link java.util.concurrent.locks} package.
*/
public static native void yield();
这是 java 8 的 yield 方法定义,这时候已经是一个 native 方法了。
在某些书上看到说,java 5 之前,yield 方法的实现是 sleep(0),由于没有找到 1.4 的 jdk,所以没有去看。
yield 方法是一个非强制性的提示,告诉线程调度器自己可以让出 cpu,但最终是否会让出 cpu 还要看线程调度器自己的意思。
如果线程调度器响应了 yield 方法,线程将会从 Running 状态退回到 Ready 状态等待调度。
2.1.4.2 sleep(0)
sleep(0) 可以达到与 yield 方法类似的效果。
sleep 方法可以使线程进入 Blocked 状态指定的时间后回到 Ready 状态,而如果将时间设为 0,则相当于直接回到 Ready 状态。
2.1.4.3 注意点
yield 和 sleep(0) 都不靠谱,不要过于相信它们。因为实际效果完全依赖于运行环境和 jvm 实现。
2.1.5 线程阻塞
线程状态从 Running 到 Blocked。
主要有以下几种途径:
- sleep:进入阻塞状态,持续一定的时间,或被打断;
- wait:进入阻塞状态,持续一定的时间,或永久直到被唤醒或被打断;
- join:进入阻塞状态,持续一定时间,或被 join 的线程执行完,或被打断;
- suspend:进入阻塞状态,永久,直到被 resume。
suspend 也已经被弃用,因为它会永久地带着锁处于阻塞状态,除非被 resume,所以容易导致死锁。
Blocked 状态的线程不参与线程调度,但可以被 stop。
2.1.6 线程唤醒
线程状态从 Blocked 到 Ready。
对应于线程阻塞的几种途径:
- sleep:指定时间到,或被打断;
- wait:指定时间到,或被 notify,或被打断;
- join:指定时间到,或被 join 的线程执行完,或被打断;
- suspend:被 resume。
resume 方法唯一的作用就是唤醒被 suspend 的线程。由于 suspend 被弃用,所以 resume 也被弃用。
2.1.7 线程打断
即 interrupt,主要涉及三个方法:
- void interrupt();
- boolean isInterrupted();
- static boolean interrupted()。
2.1.7.1 void interrupt()
public void interrupt() {
//...
}
作用是:将调用此方法的线程置为被打断状态。可以假想存在一个 isInterrupted 标识位,并且被置为了 true,当然 Thread 类里面并没有这个成员变量。
主要应用场景有:
- 借这个标识位做一些业务逻辑的判断;
- 打断 sleep 方法,使其抛出 InterruptedException;
- 打断 wait 方法,使其抛出 InterruptedException;
- 打断 join 方法,使其抛出 InterruptedException。
sleep,wait,join 方法被打断后,会自动清除打断状态。
2.1.7.2 boolean isInterrupted()
public boolean isInterrupted() {
return isInterrupted(false);
}
作用是:检测线程是否为被打断状态。是则返回 true,否则返回 false。
简单来说,调用了 interrupt() 方法,那么再调用 isInterrupted() 方法就会返回 true。
2.1.7.3 static boolean interrupted()
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
作用是:检测线程是否为被打断状态。是则返回 true,否则返回 false。同时,清除打断状态。
与 isInterrupted() 方法的区别是:
- 如果线程调用了 interrupt() 方法,那么 isInterrupted() 方法无论调多少次返回都是 true;
- 如果线程调用了 interrupt() 方法,第一次调用 interrupted() 方法返回 true,同时清除标识位,后面再调用 interrupted() 或者 isInterrupted() 方法都会返回 false,直到再次调用 interrupt() 方法。
2.1.8 线程优先级
public final void setPriority(int newPriority) {
//...
}
java 定义了 10 个不同的优先级,从低到高依次为 1~10,但可靠性不高,要依赖操作系统。
一般情况都不要设置。
2.1.9 守护线程
public final void setDaemon(boolean on) {
checkAccess();
if (isAlive()) {
throw new IllegalThreadStateException();
}
daemon = on;
}
通过 setDaemon(true) 可将线程设为守护线程。
当只有守护线程在运行(即所有用户线程退出)时,jvm 进程自动结束。
需要注意的是:
- setDaemon() 方法必须在 start() 方法之前调用,否则会抛异常;
- 守护线程的自动终止比 stop() 方法更暴力,它是因为 jvm 进程退出而终止,所以 finally 代码块也不能保证被执行。
2.1.10 线程 id,线程名称
线程 id 是在同一次 jvm 唯一的,不可设置。
线程名称可设置,并且可以设置相同,虽然并不建议。