写于:2019-11-01
线程存在,就是为了让程序能够并行的执行。并行指的就是一系列任务在计算中同时运行的行为。如:浏览网页的同时播放音乐等。
一、线程简介
1、概念
2、一个简单的线程案例
模拟:同时浏览网页,同时听歌。
代码
public class SimpleThread {
public static void main(String[] args) {
new Thread(()->{
while (true){
System.out.println("======听音乐======");
sleep(1);
}
},"listen-music").start();
while (true){
System.out.println("======浏览网页======");
sleep(1);
}
}
public static void sleep(long mill){
try {
Thread.sleep(mill);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
控制台打印
======浏览网页======
======听音乐======
======浏览网页======
======听音乐======
======浏览网页======
======听音乐======
======听音乐======
.....................................
jconsole 查看线程
小贴士: 1、main 方法启动时,其本身就是一个线程。 2、Thread 线程,只有调用了 Thread#start 方法,才代表派生出了一个新的线程。
二、线程的生命周期
从图中可知,线程的生命周期大致分为5个主要阶段
1、NEW
new Thread(); new 一个 Thread 对象,此时线程处于 NEW 状态。它只是一个普通的对象,在没有调用 start 方法前,该线程根本不存在。
2、RUNNABLE
调用 Thread#start() 方法。 调用 start 方法后,真正的在 JVM 中创建了一个线程,此时线程并不能马上执行,需要根据 CPU 的调度才能执行,此时属于等待执行的状态,叫做:RUNNABLE 状态【可运行状态】。
RUNNABLE 只能进入 RUNNING 状态,或者意外终止。调用 wait、sleep或其他 block 的 IO 操作,也需要先获取CPU 执行权,也就是先进入 RUNNING 状态。
3、RUNNING
当 CPU 空闲并从任务可执行队列中选中线程,此时线程获取到的 CPU 执行权,此时线程处于 RUNNING 状态。
在 RUNNING 状态时,可能存在的状态变更 1、直接进入 TERMINATED 状态,如:JDK stop 方法,获取存在某个判断结束的标识。 2、进入 BLOCK 状态。如:调用 sleep,或 wait 方法将线程加入 waitSet 中。 3、进入某个阻塞的IO操作。如:因网络数据读写而进入 BLOCKED 状态。 4、获取某个锁,从而加入该锁阻塞的队列中从而进入 BLOCKED 状态。 5、CPU 时间片执行完,CPU 放弃执行该线程,线程进入 RUNNABLE 状态。 6、主动调用 yield 方法,放弃 CPU 执行权,进入 RUNNABLE 状态。
4、BLOCKED
线程因为调用 sleep、wait方法,进入某个阻塞的 IO 操作或者争抢锁资源而进入 BLOCKED 状态。
在 BLOCKED 状态时,可能存在的状态变更 1、直接进入 TERMINATED。如:JDK stop 方法,或者意外死亡。 2、阻塞的 IO 操作结束。如:读取数据结束进入 RUNNABLE 状态。 3、线程完成指定时间的休眠,进入到 RUNNABLE 状态。 4、wait 中的线程被其他线程 notify/notifyAll 唤醒,进入 RUNNABLE 状态 5、线程获取到某个锁资源,进入 RUNNABLE 状态。 6、线程在阻塞过程中被打断,比如其他线程的 interrupt 方法,进入 RUNNABLE 状态。
5、TERMINATED
线程的最终状态,进入该状态,表示线程的生命周期结束。 会进入该状态的操作: 1、线程正常结束,结束生命周期 2、线程运行出错意外结束 3、JVM Crash,导致所有的线程都结束了。
三、Runnable 接口
1、源码分析 Runnable 接口
小贴士:在 JDK 中代表线程的只有 Thread 这个类。
Runnable 是一个简单的接口,只定义了一个无参无返回值的方法,代码如下:
@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();
}
首先、Runnable 是一个函数式接口,可以使用 java8 lambda 表达式。
其次、通过 Runnable#run() 的 java doc 我们可以得知,Runnable#run 会在线程 start 的使用被 Thread#run 调用。
下面我们来看看 Thread#run 方法代码
public class Thread implements Runnable {
/* What will be run. */
// 构造函数传入的 Runnable ,如果没有传入则为空
private Runnable target;
@Override
public void run() {
// 如果存在传入的 Runnable 实现,会执行 Runnable#run 方法
if (target != null) {
target.run();
}
// 如果 target 为空,则需要重写该方法,写入业务逻辑
}
}
2、Runnable 接口 对比重写 Thread#run
1、重写 Thread#run 方法是,业务逻辑实在 run 方法中的,换句话说就是,业务逻辑和 Thread 是耦合的。换句话说就是多线程间的 run 方法是不能共享的,也就是线程A不能把线程B的run方法当做自己的执行单元。
2、使用 Runnable 接口,能够将执行单元和 Thread 的操作进行解耦,同时多个线程在构造时能够使用同一套执行单元。
四、多线程一定快吗?
数据验证
对 a、b 两个数进行循环累加操作 并发操作:两个线程分别对 a,b 进行累加操作 串行操作:直接在一个线程内进行 a,b 的累加操作
并发代码
串行代码
不同操作次数统计表格如下
循环次数 | 串行执行耗时(ms) | 并发执行耗时(ms) | 串行并发对比 |
---|---|---|---|
100万次 | 5 | 21 | 串行快 |
200万次 | 6 | 13 | 串行快 |
400万次 | 7 | 5 |
不同操作系统,不同数据量有所不同。
平均来说循环次数较小时,串行比多线程快。
数据量达到一定量时,多线程的平均运行速递比串行快。
结论:线程间存在上下文切换
CPU 单核或是多核都支持多线程执行,这一机制的实现源自于 CPU时间片
。
CPU时间片
是 CPU 分配给每个线程的执行时间,这个时间是非常短的通常为几十毫秒,一个线程消耗完了换另一个线程来,CPU 通过不断的切换线程执行,让我们感觉多个线程是同时执行的。
一次上下文切换的过程:
- CPU 通过时间片分配算法循环执行任务
- 一个任务执行一个时间片后切换到下一个任务,切换前进行任务状态保存
任务从保存再到重新加载执行的过程就是一次上下文切换。