线程概述
线程在现代操作系统中的大致分两种(参考来源:https://developer.aliyun.com/article/641914)
内核级线程(KLT,Kernel Level Thread)
线程管理的所有工作(创建和撤销)由操作系统内核完成;操作系统内核提供一个应用程序设计接口API,供开发者使用KLT;
线程交给内核管理,内核需要维护线程表,线程表保存了寄存器、状态和其他信息。当然每个线程所属进程的进程表也是维护在内核的,内核创建和销毁线程的代价是比较大的。
用户级线程(ULT,User Level Thread)
应用程序可以通过使用用户空间运行线程库被设计成多线程程序。线程的创建,消息传递,调度,保存/恢复上下文都有线程库来完成。内核感知不到多线程的存在。内核继续以进程为调度单位,并且给该进程指定一个执行状态(就绪、运行、阻塞等)。
对照一下,就是将线程的调度放在用户态执行,对于内核来说就像是单线程一样。
而Java线程创建是依赖于系统内核,通过JVM调用系统库创建内核线程,内核线程与Java-Thread是1:1的映射关系,当然也有其它映射关系如1:N、N:N,如go语言使用的就是1:N,所以常常go语言被硬吹并发比java高很多就归功于其线程模型。
KLT与ULT(图片来自于网络)
Java线程基础
创建线程的方式
继承
Thread
类,重写run
方法,新建Thread
类对象,使用该对象调用Thread
类的start
方法启动线程1
2
3
4
5
6public class MyWorker extends Thread {
public void run() {
// todo
}
}1
2
3
4public static void main(String[] args) {
Thread myWorker = new MyWorker();
myWorker.start();
}实现
Runnable
接口,使用Runnable
的实例作为构造方法的参数新建Thread
类对象,调用Thread
对象的start
方法创建线程1
2
3
4
5
6public class MyRunner implements Runnable {
public void run() {
// todo
}
}1
2
3
4
5public static void main(String[] args) {
MyRunner myRunner = new MyRunner();
Thread thread = new Thread(myRunner);
thread.start();
}实现
Callable
接口,将Callable
实例提交到一个线程池(见下文)ExecutorService
中执行任务;准确的说这并不算是创建线程的一个方式,因为ExecutorService
接收任务后并不一定创建线程去执行。1
2
3
4
5
6
7class MyCaller implements Callable<String> {
public String call() throws Exception {
Thread.sleep(3000);
return "call end";
}
}1
2
3
4
5
6
7
8
9
10
11public static void main(String[] args) {
MyCaller myCaller = new MyCaller();
ExecutorService executorService = Executors.newFixedThreadPool(3);
try {
Future<String> future = executorService.submit(myCaller);
String result = future.get();
System.out.println(result);
}finally {
executorService.shutdown();
}
}
Callable接口出现的必然性
Callable
接口与Runnable
接口:Callable
的call
方法有返回值的,且允许抛出异常,Runnable
接口的run
方法无返回值,不允许抛出异常(这里的异常肯定是检查型异常);
1 | public interface Callable<V> { |
jdk提供Runnable
接口,我们可以在当前线程之外开启子线程去干一些事情(异步),根据Runnable
接口的run
方法的特点,这个子线程执行了就执行了,我们不清楚这个线程允许情况如何,有时候我们希望子线程执行结束后,我们能拿到些东西,Runnable
接口显然并不能满足,于是Callable
接口出现了。
显然“任务”这个概念更适合我们对子线程的描述,我们可以这样理解:
Runnable
是一个不期望有结果的任务,执行了就执行了,自己做好异常处理;Callable
是一个期望有结果的任务,我们可以关心它抛出的异常。
下文《Executor框架》将会讲解任务相关的管理。
线程生命周期
网上众多讲java线程生命周期时想必都会贴出下面这么一张图,然后就人云亦云
准确的说,上述这5个状态是从cpu角度看操作系统的线程各个阶段的状态。前文说到Java中的线程是KLT,所以Java线程在cpu角度看也是有这5个状态,但是不代表Java就是如此定义线程状态的!!!
JavaThread
类使用内部枚举State
来标识线程的状态。
1 | public enum State { |
可以看到总共有6个状态,下面一一举例
NEW:新生态,线程对象被创建时处于该状态
1
2
3
4
5public static void main(String[] args) {
Thread t = new Thread();
System.out.println(t.getState());
}RUNNABLE:运行态,线程正在运行其线程栈内代码(指令)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
long start = System.currentTimeMillis();
for (;;) {
if (System.currentTimeMillis() - start > 1000 * 3) // 循环3秒以上
break;
}
});
t.start();
Thread.sleep(1000L);// sleep 1秒
System.out.println(t.getState());// 此时线程t未结束,正在执行其循环代码
}BLOCKED:阻塞状态,线程想获取不到synchronized同步锁时,进入阻塞状态,如下线程t2获取target对象的锁时,因为线程t1还未释放,所以t2进入阻塞状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31public static void main(String[] args) throws InterruptedException {
Object target = new Object();
Thread t1 = new Thread(() -> {
synchronized (target) {// 获取target的锁
long start = System.currentTimeMillis();
for (;;) {
if (System.currentTimeMillis() - start > 1000 * 3) // 循环3秒
break;
}
}
});
t1.start();
Thread.sleep(100L);// 等t1线程启动
Thread t2 = new Thread(() -> {
synchronized (target) {// 获取target的锁
System.out.println("--- run ---");
}
});
t2.start();
Thread.sleep(1000L);// 等两个线程都启动
System.out.println(t2.getState());// 此时线程t1 t2 都未结束
}WAITING:等待状态,准确的说是死等,线程调用object.wait()方法,等待其它线程调用object.notify()或object. notifyall()方法,此时该线程进入等待状态。或调用
LockSupport.park()
方法线程也会进入该状态。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public static void main(String[] args) throws InterruptedException {
Object target = new Object();
Thread t1 = new Thread(() -> {
synchronized (target) {// 获取target的锁
try {
target.wait();// 调用wait()方法
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
Thread.sleep(1000L);
System.out.println(t1.getState());// 由于没有线程调用target.notifyAll()或target.notify(),此时线程t1一直处于等待状态,
}TIMED_WAITING: 等待未超时,线程调用object.wait(long)方法,等待其它线程调用object.notify()或object. notifyall()方法,不过只会等待指定的时间,未超时之前该线程进入等待未超时状态。或线程进入通过sleep方法进行“睡眠”时,也会进入等待未超时状态。或调用
LockSupport.parkNanos(Object, long)
方法、LockSupport.parkUntil(Object, long)
方法线程也会进入该状态。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public static void main(String[] args) throws InterruptedException {
Object target = new Object();
Thread t1 = new Thread(() -> {
synchronized (target) {// 获取target的锁
try {
target.wait(3000L);// 调用wait()方法,等待其它线程调用target.notifyAll()或target.notify(),不过只等待3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
Thread.sleep(1000L);
// 由于没有线程调用target.notifyAll()或target.notify(),3秒内(此时过去了1秒)线程t1一直处于等待未超时状态
System.out.println(t1.getState());
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(1000L);
// 线程t1依旧在睡眠,其处于等待未超时状态
System.out.println(t1.getState());
}TERMINATED:终止(死亡)状态,代码执行结束或外部干涉导致线程终止,进入死亡状态。
1
2
3
4
5
6
7
8
9
10
11
12public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("--- run ---");
});
t1.start();
Thread.sleep(1000L);//
// 过了1秒,线程t1已经执行完,其处于死亡状态
System.out.println(t1.getState());
}
线程状态的获取
通过调用Thread#getState
方法获取到当前线程的状态。
线程优先级
java线程优先级范围:1~10,优先级从低到高。
优先级越高,越容易得到cpu时间片。
相关方法:
- 获取线程的优先级:
Thread#getPriority
- 设置线程优先级:
Thread#setPriority
守护线程
- java中线程分为用户线程(User Thread )和守护线程(Daemon Thread )
- 虚拟机必须确保用户线程执行完毕
- 虚拟机可以不等待守护线程执行完毕
- 通过调用
Thread#setDaemon(true)
方法将线程设置为守护线程
守护线程最典型的应用就是 GC (垃圾回收器)。用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于,当用户线程全部执行结束,只剩下守护线程存在了,虚拟机也就退出了, 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。
停止线程的方法
Thread#stop
方法,过时,不建议使用Thread#destroy
方法,废弃1
2
3
4
public void destroy() {
throw new NoSuchMethodError();
}设置某种标志位,结束线程代码的执行,正常停止线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class MyRunnable implements Runnable {
private volatile boolean flag = true;
public void run() {
while (flag) {
System.out.println("运行");
//... do something
}
System.out.println("结束");
}
// 对外提供停止方法
public void doStop() {
flag = false;
}
}1
2
3
4
5
6
7public static void main(String[] args) throws InterruptedException {
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable).start();
Thread.sleep(800);
myRunnable.doStop();
}
线程休眠
调用Thread.sleep
方法使当前线程进入阻塞状态,线程结束休眠后进入就绪态;
线程通过Thread.sleep
方法进入休眠时,不会释放对象锁
线程礼让
调用Thread.yield()
方法使当前线程让出cpu时间片,使线程从运行态重新进入就绪态。值得注意的是:A线程让出cpu时间片,不代表B线程就可以获取到cpu时间片,下一次获取cpu时间片的线程可能还是A。
线程“插队”
如在线程A的代码中,调用B线程对象的Thread#join
方法,将B线程加入到A线程,A线程需要等待B线程执行完之后才能继续执行自己的代码。好比插队,可传入等待时间表示允许插队的最长时间,超过该时间,B线程还未执行结束,则不再等待。
Executor框架
下面一一引出Executor框架相关的类或接口
Executor
前文我们将Runnable
和Callable
称之为任务,那么任务有了,就要有对应的任务执行者和管理者,如果不进行管理,直接使用new Thread()的方法创建线程有很多缺点:
- new Thread()耗费性能
- 调用new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制创建,之间相互竞争,会导致过多占用系统资源
- 不利于扩展,比如如定时执行、定期执行、线程中断等
所以任务(线程)的管理是必须的。
对于Runnable
,我们不期望有结果(无返回值),只管执行,所以Executor
接口出现了。
通过Executor
的executor
方法来启动任务(子线程),更加便捷,并且可以避免“this逃逸”问题
this逃逸问题
this逃逸是指在构造函数返回之前其他线程就持有该对象的引用,调用尚未构造完全的对象的方法可能引发奇怪的错误。
代码示例:
1 | public class Worker { |
可修改如下规避this逃逸
1 | public class Worker { |
ExecutorService
对于Callable
,也需要有一个执行者和管理者,于是ExecutorService
出现了,无论是Callable
还是Runnable
,这些任务都涉及管理和扩展方面的优化,ExecutorService
索性直接扩展Executor
增加对Callable
支持。
ExecutorService
提供了任务生命周期管理等功能 ,实际开发中用的更多的是ExecutorService
,它的底层实现就是线程池。
Future
任务提交后,我们肯定需要知道任务执行得怎么样了,而ExecutorService
使用Future
来跟踪任务,通过ExecutorService
提交任务后就可以得到Future
对象。
Future
接口的主要方法如下:
boolean cancel(boolean mayInterruptIfRunning);
尝试取消任务,成功则返回true,失败false(任务已经完成、已经被取消或由于其他原因无法取消)
mayInterruptIfRunning 参数:是否中断任务,true,中断任务且取消任务,false,允许任务继续执行到结束,但是获取结果时会抛异常。
boolean isCancelled();
任务是否已经取消
boolean isDone();
任务是否执行结束
V get() throws InterruptedException, ExecutionException;
获取任务执行结果,获取不到结果之前会一直等待,如果任务已经取消了则抛出异常
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
获取任务执行结果,但只会等待指定的时间,超时则抛出异常。
timeout 参数:时间数量
unit 参数:时间单位
Executors
Executors
类是Executor
体系相关的工具类,通过它可以很方便的操作Executor
体系相关的接口示例,如创建线程池。
总结
以上就是Executor框架的主要知识点,总结一下,Executor框架要由三大部分组成:
- 任务(Runnable /Callable)
- 任务的执行(Executor)
- 异步计算的结果(Future)
Executor 框架的使用
主线程首先要创建实现Runnable
或者Callable
接口的任务对象,把任务对象交给ExecutorService
执行,得到结果对象Future
,最后,主线程可以通过对象Future
来获取任务结果或取消任务。
本文参考B站UP主“狂神说Java”https://space.bilibili.com/95256449/的Java多线程教学视频进行整理。
感谢成长路上为在下传道受业解惑之人