常见方法
方法 | 功能 | 说明 |
---|---|---|
public void start() | 启动一个新线程;Java虚拟机调用此线程的run方法 | start 方法只是让线程进入就绪,里面代码不一定立刻 运行(CPU 的时间片还没分给它)。每个线程对象的 start方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException |
public void run() | 线程启动后调用该方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则 线程启动后会调用 Runnable 中的 run 方法,否则默 认不执行任何操作。但可以创建 Thread 的子类对象, 来覆盖默认行为 |
public voidsetName(String name) | 给当前线程取名字 | |
public void getName() | 获取当前线程的名字。线程存在默认名称:子线程是Thread-索引,主线程是main | |
public static Thread currentThread() | 获取当前线程对象,代码在哪个线程中执行 | |
public static void sleep(long time) | 让当前线程休眠多少毫秒再继续执行。Thread.sleep(0) : 让操作系统立刻重新进行一次cpu竞争 | |
public static native void yield() | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |
public final int getPriority() | 返回此线程的优先级 | |
public final void setPriority(int priority) | 更改此线程的优先级,常用1 5 10 | java中规定线程优先级是1~10 的整数,较大的优先级 能提高该线程被 CPU 调度的机率 |
public void interrupt() | 中断这个线程,异常处理机制 | |
public static boolean interrupted() | 判断当前线程是否被打断,清除打断标记 | |
public boolean isInterrupted() | 判断当前线程是否被打断,不清除打断标记 | |
public final void join() | 等待这个线程结束 | |
public final void join(long millis) | 等待这个线程死亡millis毫秒,0意味着永远等待 | |
public final native boolean isAlive() | 线程是否存活(还没有运行完毕) | |
public final void setDaemon(boolean on) | 将此线程标记为守护线程或用户线程 | |
public long getId() | 获取线程长整型 的 id | id 唯一 |
public state getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED |
public boolean isInterrupted() | 判断是否被打 断 | 不会清除 打断标记 |
1. start 与 run
调用 run
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
FileReader.read(Constants.MP4_FULL_PATH);
}
};
t1.run();
log.debug("do other things ...");
}
输出
19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...
程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的
调用start
将上述代码的 t1.run() 改为
t1.start();
输出
19:41:30 [main] c.TestStart - do other things ...
19:41:30 [t1] c.TestStart - t1
19:41:30 [t1] c.FileReader - read [1.mp4] start ...
19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms
程序在 t1 线程运行, FileReader.read() 方法调用是异步的
小结
-
直接调用 run 是在主线程中执行了 run,没有启动新的线程
-
使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
public static void main(String[] args) { Thread t1 = new Thread("t1") { @Override public void run() { log.debug("running..."); } }; System.out.println(t1.getState()); t1.start(); System.out.println(t1.getState()); }
可以看见,start方法创建了一个新线程,将线程从就绪状态切换为Runnable
NEW RUNNABLE 03:45:12.255 c.Test5 [t1] - running...
一点点面试相关
-
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
- new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。
start() 会执行线程的相应准备工作
,然后自动执行 run() 方法的内容,这是真正的多线程工作。 - 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
- new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。
-
线程的 run()和start()有什么区别?
-
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。
-
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start()只能调用一次。
-
start()方法来启动一个线程
,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码
; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。 -
run()方法是在本线程里的
,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
2. sleep 与 yield
sleep
-
调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
-
其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("enter sleep...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.debug("wake up...");
e.printStackTrace();
}
}
};
t1.start();
Thread.sleep(1000);
log.debug("interrupt...");
t1.interrupt();
}
输出结果:
03:47:18.141 c.Test7 [t1] - enter sleep...
03:47:19.132 c.Test7 [main] - interrupt...
03:47:19.132 c.Test7 [t1] - wake up...
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at cn.xiaozheng.test.Test7$1.run(Test7.java:14)
-
睡眠结束后的线程未必会立刻得到执行
-
建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 。其底层还是sleep方法。
@Slf4j(topic = "c.Test8")
public class Test8 {
public static void main(String[] args) throws InterruptedException {
log.debug("enter");
TimeUnit.SECONDS.sleep(1);
log.debug("end");
// Thread.sleep(1000);
}
}
- 在循环访问锁的过程中,可以加入sleep让线程阻塞时间,防止大量占用cpu资源。
yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
- 它是一个静态方法而且只保证当前线程放弃 CPU 占用而不能保证使其它线程一定能占用 CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。
线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,
但它仅仅是一个提示
,调度器可以忽略它 - 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
/**
* 线程可以拥有的最低优先级。
*/
public static final int MIN_PRIORITY = 1;
/**
* 分配给线程的默认优先级。
*/
public static final int NORM_PRIORITY = 5;
/**
* 线程可以拥有的最大优先级。
*/
public static final int MAX_PRIORITY = 10;
/**
* 更改此线程的优先级。
* <p>
* 首先调用该线程的{@code checkAccess}方法,不带任何参数。这可能会导致抛出{@code SecurityException} 。
* <p>
* 否则,此线程的优先级设置为指定的{@code newPriority}和线程的线程组的最大允许优先级中的较小者。
*
* @param newPriority 将此线程设置为的优先级
* @throws IllegalArgumentException If the priority is not in the
* range {@code MIN_PRIORITY} to
* {@code MAX_PRIORITY}.
* @throws SecurityException if the current thread cannot modify
* this thread.
* @see #getPriority
* @see #checkAccess()
* @see #getThreadGroup()
* @see #MAX_PRIORITY
* @see #MIN_PRIORITY
* @see ThreadGroup#getMaxPriority()
*/
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
测试优先级和yield
@Slf4j(topic = "c.TestYield")
public class TestYield {
public static void main(String[] args) {
// 没有yield
Runnable task1 = () -> {
int count = 0;
for (;;) {
System.out.println("---->1 " + count++);
}
};
// 有yield
Runnable task2 = () -> {
int count = 0;
for (;;) {
// Thread.yield();
System.out.println(" ---->2 " + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
测试结果:
#优先级
---->1 283500
---->2 374389
#yield
---->1 119199
---->2 101074
结论:可以看出,线程优先级和yield会对线程获取cpu时间片产生一定影响,但不会影响太大。
一点点面试相关
-
Thread 类中的 yield 方法有什么作用?
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
-
Thread.sleep(0)的作用是什么?
由于
Java 采用抢占式的线程调度算法
,因此可能会出现某条线程常常获取到 CPU 控制权的情况,为了让某些优先级比较低的线程也能获取到 CPU 控制权
,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作
,这也是平衡 CPU 控制权的一种操作
。
∗ 应用之限制(案例 1 ) \textcolor{green}{* 应用之限制(案例1)} ∗应用之限制(案例1)
sleep 实现
在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权 给其他程序
while(true) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 可以用 wait 或 条件变量达到类似的效果
- 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
- sleep 适用于无需锁同步的场景
wait 实现
synchronized(锁对象) {
while(条件不满足) {
try {
锁对象.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
// do sth...
}
条件变量实现
lock.lock();
try {
while(条件不满足) {
try {
条件变量.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// do sth...
} finally {
lock.unlock();
}
3. join 方法详解
为什么需要 join
下面的代码执行,打印 r 是什么?
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
});
t1.start();
log.debug("结果为:{}", r);
log.debug("结束");
}
分析
- 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
- 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0
解决方法
- 用 sleep 行不行?为什么?
- 用 join,加在 t1.start() 之后即可
∗ 应用之同步(案例 1 ) \textcolor{green}{* 应用之同步(案例1)} ∗应用之同步(案例1)
以调用方角度来讲,如果
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
等待多个结果
问,下面代码 cost 大约多少秒?
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(1);
r1 = 10;
});
Thread t2 = new Thread(() -> {
sleep(2);
r2 = 20;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
分析如下
- 第一个 join:等待 t1 时, t2 并没有停止, 而在运行
- 第二个 join:1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s
如果颠倒两个 join 呢?
最终都是输出
20:45:43.239 [main] c.TestJoin - r1: 10 r2: 20 cost: 2005
有时效的join
当线程执行时间没有超过join设定时间
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test3();
}
public static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(1);
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
// 线程执行结束会导致 join 结束
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
输出
20:48:01.320 [main] c.TestJoin - r1: 10 r2: 0 cost: 1010
当执行时间超时
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test3();
}
public static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(2);
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
// 线程执行结束会导致 join 结束
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
输出
20:52:15.623 [main] c.TestJoin - r1: 0 r2: 0 cost: 1502
4. interrupt方法详解
Interrupt
说明
interrupt
的本质是将线程的打断标记设为true,并调用线程的三个parker对象(C++实现级别)unpark该线程。
基于以上本质,有如下说明:
- 打断线程不等于中断线程,有以下两种情况:
- 打断正在运行中的线程并不会影响线程的运行,但如果线程监测到了打断标记为true,可以自行决定后续处理。
- 打断阻塞中的线程会让此线程产生一个
InterruptedException
异常,结束线程的运行。但如果该异常被线程捕获住,该线程依然可以自行决定后续处理(终止运行,继续运行,做一些善后工作等等)
打断 sleep,wait,join 的线程
这几个方法都会让线程进入阻塞状态
打断 sleep 的线程, 会清空打断状态,以 sleep 为例
private static void test1() throws InterruptedException {
Thread t1 = new Thread(()->{
sleep(1);
}, "t1");
t1.start();
sleep(0.5);
t1.interrupt(); // 打断t1 线程
log.debug(" 打断状态: {}", t1.isInterrupted());
}
输出
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at cn.xiaozheng.n2.util.Sleeper.sleep(Sleeper.java:8)
at cn.xiaozheng.n4.TestInterrupt.lambda$test1$3(TestInterrupt.java:59)
at java.lang.Thread.run(Thread.java:745)
21:18:10.374 [main] c.TestInterrupt - 打断状态: false
打断正常运行的线程
打断正常运行的线程, 不会清空打断状态
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();
sleep(0.5);
t2.interrupt();
}
输出
20:57:37.964 [t2] c.TestInterrupt - 打断状态: true
* 模式之两阶段终止
Two Phase Termination 在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。
错误思路
- 使用线程对象的 stop() 方法停止线程
- stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁, 其它线程将永远无法获取锁
- 使用 System.exit(int) 方法停止线程
- 目的仅是停止一个线程,但这种做法会让整个程序都停止
两阶段终止模式
利用 isInterrupted
interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行
class TPTInterrupt {
private Thread thread;
// 启动监控线程
public void start(){
thread = new Thread(() -> {
while(true) {
// 获取当前线程打断状态
Thread current = Thread.currentThread();
// 判断是否打断
if(current.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000); // 情况一 异常打断
log.debug("将结果保存"); // 情况二 正常打断
} catch (InterruptedException e) {
// sleep 打断后 会清除打断标记,所以要重新设置
current.interrupt();
e.printStackTrace();
}
// 执行监控操作
}
},"监控线程");
thread.start();
}
public void stop() {
thread.interrupt();
}
}
调用
TPTInterrupt t = new TPTInterrupt();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();
结果
11:49:42.915 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:43.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:44.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:45.413 c.TestTwoPhaseTermination [main] - stop
11:49:45.413 c.TwoPhaseTermination [监控线程] - 料理后事
利用停止标记
// 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性
// 我们的例子中,即主线程把它修改为 true 对 t1 线程可见
class TPTVolatile {
private Thread thread;
private volatile boolean stop = false;
public void start(){
thread = new Thread(() -> {
while(true) {
Thread current = Thread.currentThread();
if(stop) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("将结果保存");
} catch (InterruptedException e) {
}
// 执行监控操作
}
},"监控线程");
thread.start();
}
public void stop() {
stop = true;
thread.interrupt();
}
}
调用
TPTVolatile t = new TPTVolatile();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();
结果
11:54:52.003 c.TPTVolatile [监控线程] - 将结果保存
11:54:53.006 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.007 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.502 c.TestTwoPhaseTermination [main] - stop
11:54:54.502 c.TPTVolatile [监控线程] - 料理后事
打断 park 线程
打断 park 线程, 不会清空打断状态
private static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
sleep(0.5);
t1.interrupt();
}
输出
21:11:52.795 [t1] c.TestInterrupt - park...
21:11:53.295 [t1] c.TestInterrupt - unpark...
21:11:53.295 [t1] c.TestInterrupt - 打断状态:true
如果打断标记已经是 true, 则 park 会失效
private static void test4() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
log.debug("park...");
LockSupport.park();
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}
});
t1.start();
sleep(1);
t1.interrupt();
}
输出
21:13:48.783 [Thread-0] c.TestInterrupt - park...
21:13:49.809 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.812 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.813 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.813 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.813 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
提示 :可以使用 Thread.interrupted() 清除打断状态
5. 不推荐的方法
还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁
方法名 | static | 功能说明 |
---|---|---|
stop() | 停止线程运行 | |
suspend() | 挂起(暂停)线程运行 | |
resume() | 恢复线程运行 |
6. 主线程与守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
例:
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
sleep(2);
log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");
输出:
08:26:38.123 [main] c.TestDaemon - 开始运行...
08:26:38.213 [daemon] c.TestDaemon - 开始运行...
08:26:39.215 [main] c.TestDaemon - 运行结束...
注意
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求