单核CPU下,线程实际还是 串行执行
的。操作系统中有一个组件叫任务调度器,将CPU的时间片(windows下时间片最小约为15毫秒)分给不同的程序使用,只是由于CPU在线程间(时间片很短)的切换非常快,人类感觉是 同时运行的
。总结一句话: 微观串行,宏观并行
。
一般会将这种 线程轮流使用 CPU
的做法称为并发(concurrent)。
引用 Rob Pike 的一段描述:
生活例子:
以调用方角度来讲,如果
多线程可以让方法执行变为异步的。
比如说读取磁盘文件时,假设读取操作花费了5秒钟,如果没有线程调度机制,这5秒钟CPU什么都做不了,其他代码只能等待。
简单异步例子:
Thread t1 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
log.debug("t1");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.setName("t1");
t1.start();
log.debug("main");
运行结果
13:51:29.131 [main] DEBUG io.ray.threadstudy.test.Test01 - main
13:51:30.130 [t1] DEBUG io.ray.threadstudy.test.Test01 - t1
结果说明
main 线程不需要等待 t1 线程执行完才输出,此时程序是异步进行的。
充分利用多核 CPU 的优势,提高运行效率。想象下面的场景,执行3个计算,最后将计算结果汇总
计算 1 花费 10ms
计算 2 花费 11ms
计算 3 花费 9ms
汇总需要 1ms
10 + 11 + 9 + 1 = 31ms
11ms
最后加上汇总时间只会花费 12ms
注意: 需要多核 CPU 才能提高效率,单核仍然是轮流执行
每个Java程序启动的时候,默认都创建了一个主线程(main方法),如果想在主线程外创建线程,可以使用如下方法。
@Slf4j(topic = "io.ray")
public class Test02 {
public static void main(String[] args) {
// 创建线程对象
// 构造方法的参数是给线程指定名称(推荐)
Thread t1 = new Thread("t1") {
// run 方法内部实现了要执行的任务
@Override
public void run() {
// 要执行的任务
log.debug("t1 running");
}
};
// 启动线程
t1.start();
log.debug("main running");
}
}
输出
13:27:35.538 [main] DEBUG io.ray - main running
13:27:35.542 [t1] DEBUG io.ray - t1 running
把【Thread 线程】 和 【Runnable 任务】分开,更为灵活,推荐这种写法。
@Slf4j(topic = "io.ray")
public class Test03 {
public static void main(String[] args) {
// 创建任务对象
Runnable r1 = new Runnable() {
// run 方法内部实现了要执行的任务
@Override
public void run() {
// 要执行的任务
log.debug("r1 running");
}
};
// 创建线程对象
Thread t1 = new Thread(r1, "r1");
// 启动线程
t1.start();
log.debug("main running");
}
}
结果
13:37:07.419 [main] DEBUG io.ray - main running
13:37:07.419 [r1] DEBUG io.ray - r1 running
Java 8 以后可以使用 lambda 精简代码
// ------- lambda - 1
Runnable r2 = () -> { log.debug("r2 running"); };
Thread t2 = new Thread(r2, "r2");
t2.start();
// ------- lambda - 2
Thread t3 = new Thread(() -> { log.debug("r3 running"); }, "r3");
t3.start();
当创建 Thread 指定参数 Runnable时,会调用其 init 方法
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
// 继续往下寻找
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
// 继续往下寻找
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
// 将参数的runnable对象赋给了成员变量target
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
// 可以看到,target 成员变量实际在run方法中用到
@Override
public void run() {
// 如果 target 不为空的时候,调用的是 target 中的 run 方法
// 反之,调用的是自己重写的 run 方法
if (target != null) {
target.run();
}
}
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
@Slf4j(topic = "io.ray")
public class Test04 {
public static void main(String[] args) {
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
// 参数1 是任务对象;参数2 是线程名称,推荐
new Thread(task3, "t3").start();
try {
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:[{}]", result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
结果
13:47:22.044 [t3] DEBUG io.ray - hello
13:47:22.049 [main] DEBUG io.ray - 结果是:[100]
Java Virtual Machine Stacks (Java虚拟机栈)
JVM 由堆、栈、方法区所组成,其中栈内存是给谁用的呢?
其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
以下的原因会导致CPU不再执行当前的线程,转而执行另一个线程的代码
sleep
、yield
、wait
、join
、park
、synchronized
、lock
等方法当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 JVM 指令的执行地址,是线程私有的。
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() | 启动一个新线程,在新的线程运行 run 方法中的代码 | start 方法只是让线程进入就绪状态,里面的代码不一定会立刻执行( CPU 的时间片还没有分配给它)。每个线程对象的 start 方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException 异常。 | |
run() | 新线程启动后会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为。 | |
join() | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待 n 毫秒 | ||
getId() | 获取线程长整型的 id | id 唯一 | |
getName() | 获取线程名 | ||
setName(String) | 修改线程名 | ||
getPriority() | 获取线程优先级 | ||
setPriority(int) | 修改线程优先级 | Java 中规定线程优先级是 1~10 的整数,较大的优先级能提高线程被CPU调度的概率。 | |
getState() | 获取线程状态 | Java API 中线程状态是用 6个 enum 表示,分别是:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED | |
isInterrupted() | 判断是否被打断 | 不会清除打断标记 | |
isAlive() | 线程是否存活(还没执行完毕) | ||
interrupt() | 打断线程 | 如果被打断的线程正在 sleep、wait、join 会导致被打断的线程抛出 InterruptedException ,并清除打断标记;如果打断的是正在运行的线程,则会设置打断标记;park 的线程被打断,也会设置打断标记。 | |
interrupted() | static | 判断当前线程是否被打断 | 会清除打断标记 |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠 n 毫秒,休眠时会让出 CPU 的时间片给其他线程 | |
yield() | static | 提示线程调度器让出当前线程对 CPU 的使用 | 主要是为了测试和调试 |
@Slf4j
public class Test05 {
public static void main(String[] args) {
Test05 test05 = new Test05();
test05.testRun();
test05.testStart();
}
/**
* @Description: 调用run
* 方法的执行还是在 main 线程
**/
public void testRun() {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.run();
log.debug("do other things ..");
};
/**
* @Description: 调用start
* 方法的执行在 t2 线程
**/
public void testStart() {
Thread t2 = new Thread("t2") {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t2.start();
log.debug("do other things ..");
};
}
InterruptedException
@Slf4j
public class Test06 {
public static void main(String[] args) {
Runnable task1 = new Runnable() {
int count = 0;
@Override
public void run() {
for (;;) {
log.debug(" ----> 1 {}", count++);
}
}
};
Runnable task2 = new Runnable() {
int count = 0;
@Override
public void run() {
for (; ; ) {
log.debug(" ----> 2 {}", count++);
}
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
// 在启动前设置优先级(可选,默认 NORM_PRIORITY = 5)
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
为什么需要 join?
join 的作用: 等待线程运行结束
下面的代码执行,打印 r 是什么?
@Slf4j(topic = "io.ray")
public class Test07 {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test();
}
private static void test() throws InterruptedException {
log.debug("开始 - main");
Thread t1 = new Thread(() -> {
log.debug("开始 - t1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束 - t1");
r = 10;
}, "t1");
t1.start();
// 使用 join,main 线程进入阻塞状态,等待 t1 线程的结束
//t1.join();
log.debug("r 的结果:[{}]", r);
log.debug("结束 - main");
}
}
结果(没使用 join)
07:38:27.364 [main] DEBUG io.ray - 开始 - main
07:38:27.531 [main] DEBUG io.ray - r 的结果:[0]
07:38:27.537 [main] DEBUG io.ray - 结束 - main
07:38:27.541 [t1] DEBUG io.ray - 开始 - t1
07:38:28.544 [t1] DEBUG io.ray - 结束 - t1
结果(使用 join)
07:38:57.085 [main] DEBUG io.ray - 开始 - main
07:38:57.205 [t1] DEBUG io.ray - 开始 - t1
07:38:58.205 [t1] DEBUG io.ray - 结束 - t1
07:38:58.205 [main] DEBUG io.ray - r 的结果:[10]
07:38:58.208 [main] DEBUG io.ray - 结束 - main
分析
r=10
r=0
用 sleep 行不行 ?
解答:用 sleep 也可以,但时间不好把握,不知道 t1 线程要运行多久。
@Slf4j(topic = "io.ray")
public class Test08 {
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test();
}
private static void test() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
r1 = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
Thread t2 = new Thread(() -> {
try {
Thread.sleep(2000);
r2 = 20;
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2");
t1.start();
t2.start();
long start = System.currentTimeMillis();
log.debug("join begin");
t1.join();
log.debug("t1 join end");
t2.join();
log.debug("t2 join end");
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end-start);
}
}
结果
08:01:25.764 [main] DEBUG io.ray - join begin
08:01:26.763 [main] DEBUG io.ray - t1 join end
08:01:27.762 [main] DEBUG io.ray - t2 join end
08:01:27.762 [main] DEBUG io.ray - r1: 10 r2: 20 cost: 2000
分析执行流程
@Slf4j(topic = "io.ray")
public class Test09 {
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test();
}
public static void test() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(3000);
r1 = 10;
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
long start = System.currentTimeMillis();
t1.start();
log.debug("join begin");
// 有时效的 join,等待2秒
t1.join(2000);
long end = System.currentTimeMillis();
log.debug("r1: {} cost: {}", r1, end-start);
}
}
结果
08:09:54.335 [main] DEBUG io.ray - join begin
08:09:56.340 [main] DEBUG io.ray - r1: 0 cost: 2007
分析(超过时效)
t1 线程需要等待 3秒后才给 r1 赋值,但 t1 的 join 方法等待如果超过 2秒,直接往下执行,不需要等待 t1 线程结束,此时输出 r1 的值为 0,因为还差 1秒线程 t1 才给 r1 赋值,所以程序总耗时是 2 秒。
分析(未超过时效)
如果未超过时效,以 t1 实际的调用完毕为准,程序会提前结束,不会说等超过时效才结束程序。
@Slf4j(topic = "io.ray")
public class Test10 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("sleep..");
try {
Thread.sleep(5000); // sleep、wait、join 方法同理
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
// 主线程要等待一下才执行打断,不然并发执行,主线程打断的不是 t1 的阻塞线程,而是正常运行的线程,打断标记返回 true,不是我们想要的结果
Thread.sleep(100);
// 打断线程
t1.interrupt();
log.debug("打断标记: {}", t1.isInterrupted());
}
}
结果
08:32:04.102 [t1] DEBUG io.ray - sleep..
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at io.ray.threadstudy.test.Test10.lambda$main$0(Test10.java:19)
at java.lang.Thread.run(Thread.java:748)
08:32:04.201 [main] DEBUG io.ray - 打断标记: false
分析
对于 sleep
、wait
、join
这种方法被打断后,以异常的方式表示被打断了,并且会抹去打断标记,设置为 false,将来这个打断标记可以用于被打断后,程序是否继续运行,还是结束程序。
sequenceDiagram
participant t1 as 线程一
打断正常运行的线程,不会清空打断状态
@Slf4j(topic = "io.ray")
public class Test11 {
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t2 = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
if (interrupted) {
log.debug("打断状态:{}", interrupted);
break;
}
}
}, "t2");
t2.start();
Thread.sleep(500);
t2.interrupt();
}
}
输出
16:42:58.213 [t2] DEBUG io.ray - 打断状态:true
还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁
方法名 | static | 功能说明 |
---|---|---|
stop() | 停止线程运行 | |
suspend() | 挂起(暂停)线程运行 | |
resume() | 恢复线程运行 |
默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其他非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
@Slf4j(topic = "io.ray")
public class Test12 {
public static void main(String[] args) {
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("t1 开始运行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 结束运行");
}, "t1");
// 设置该线程为守护线程,必须在启动前设置
t1.setDaemon(true);
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("运行结束...");
}
}
输出
16:51:58.073 [main] DEBUG io.ray - 开始运行...
16:51:58.223 [t1] DEBUG io.ray - t1 开始运行
16:51:59.221 [main] DEBUG io.ray - 运行结束...
t1 线程是守护线程,所以main线程结束后,程序直接结束了,不会等待 t1 线程执行完毕
注意:
五种状态是以操作系统层面来描述的
六种状态是从 Java API 层面来描述的
根据 Thread.State
枚举,分为六种状态
start()
方法start()
方法之后,注意,Java API 层面的 RUNNABLE
状态涵盖了操作系统层面的【可运行状态】、【运行状态】、【阻塞状态】两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0吗?
@Slf4j(topic = "io.ray")
public class Test13 {
// 静态变量
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("counter = [{}]", counter);
}
}
输出
17:11:18.889 [main] DEBUG io.ray - counter = [-393]
问题分析
以上的结果可能是正数、负数、零。 为什么呢?因为 Java 中对静态变量的自增、自减并不是原子操作,要彻底理解,必须从字节码来进行分析。