常用方法概述
start 与 run
start是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的 start 方法只能调用一次,如果调用了多次会出现 illegalThreadStateException。而 run 则是新线程启动后会调用的方法,如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为。
@Slf4j(topic = "c.main")
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@SneakyThrows
@Override
public void run() {
log.debug(Thread.currentThread().getName());
Thread.sleep(3000);
}
};
t1.run();
log.debug("do other things ...");
}
}
@Slf4j(topic = "c.main")
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@SneakyThrows
@Override
public void run() {
log.debug(Thread.currentThread().getName());
Thread.sleep(3000);
}
};
t1.start();
log.debug("do other things ...");
}
}
通过对比可以发现,只有当调用start方法的时候才是我们创建的Thread类对象t1去执行run方法的代码,但是如果直接调用run方法,则是调用这个方法的线程(即Main线程)直接去执行run方法里面的代码。
注意:
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
sleep 与 yield
- sleep方法
- 调用 sleep 会让当前线程从 Running进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
Thread t1 = new Thread("t1") {
@SneakyThrows
@Override
public void run() {
log.debug(Thread.currentThread().getName());
//线程睡眠3s
Thread.sleep(3000);
}
};
t1.start();
- yield方法
- 调用 yield 会让当前线程从 Running 进入 Runnable就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
- 线程优先级
Java中线程优先级可以指定,范围是 1~10。但是并不是所有的操作系统都支持10级优先级的划分(比如有些操作系统只支持3级划分:低,中,高),Java只是给操作系统一个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。Java默认的线程优先级为5,线程的执行顺序由调度程序来决定,线程的优先级会在线程被调用之前设定。通常情况下,高优先级的线程将会比低优先级的线程有更高的几率得到执行。我们使用方法Thread类setPriority()实例方法来设定线程的优先级。
说明:
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
@Slf4j(topic = "c.main")
public class Main {
public static void main(String[] args) {
Runnable task1 = () -> {
int count = 0;
for (; ; ) {
System.out.println("---->1 " + count++);
}
};
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();
}
}
我们注释 Thread.yield 和 设置t1、t2的优先级,执行程序,t1、t2的默认优先级都为5,打印的结果应该比较接近。如:
现在测试在t2中调用yield方法,测试结果:
可以看到yield方法确实有一点作用,二者的结果差了一个数量级。接着我们看设置线程优先级:
可以看到居然差别不是很大,这是因为我电脑的 cpu 不忙,只有当 cpu 比较忙的时候,线程优先级才会被考虑,这样证明了Java的线程优先级只是一个参考值,真正的调度只能由操作系统来执行。
join 方法
@Slf4j(topic = "c.JoinTest")
public class JoinTest {
static int r = 0;
public static void main(String[] args) {
test1();
}
private static void test1() {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束");
r = 10;
});
t1.start();
log.debug("结果为:{}", r);
log.debug("结束");
}
}
思考: 打印结果r是什么,是0还是10?为什么?
可以看到打印的结果是0,而不是10,分析如下:首先,线程t1和main线程是并行执行的,t1线程需要1s后才能计算出r=10,但是main线程是立刻就需要打印r的值,因此打印出来的r还是原来的0。
思考: 那么有什么方法可以让r打印的值变成10?
首先,我们可以实验sleep方法,但是需要让main线程sleep的时间长于t1线程,如:
main线程3s后在打印r值,而t1线程计算r=10的时间只需要1s,因此这样就可以达到目的了,测试截图:
但是,这样让线程sleep的方法明显有点硬编码的意思,不够灵活,这时候join方法就有用处了。join方法的作用是在当前线程等待其它线程运行结束再运行该线程,即同步。如下代码:
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束");
r = 10;
});
t1.start();
//在main线程中等待t1线程的结束才继续执行main线程的代码
t1.join();
log.debug("结果为:{}", r);
log.debug("结束");
}
可以看到结果依然可以打印到10
其线程运行关系图如下所示:
思考: 下面的代码运行时,main线程需要等待多少时间?如果t1和t2 join方法 的顺序颠倒了?
分析:
如果t1、t2是并行的,则等待t1时,t2并没有停止运行,因此main线程只需要2s即可,t1和t2颠倒调用顺序也是一样;如果是单核cpu,那么t1和t2无法并行,只能并发执行,因此main线程一共需要等待3s。
此外,sleep方法还可以具有等待时效,如果超过等待时间,将不会再等待其它线程的执行了,如:
从测试结果可以看到,500ms之后不再等待t1计算r=10,而是直接输出r=0
interrupt 方法
interrupt方法是打断线程的方法,但是关于打断的线程,需要分情况而论:
- 如果是打断正在sleep、wait、join的线程则会抛出 InterruptedException 异常,并且打断标志置不会返回true,而是返回false
- 如果打断的是正常运行的线程,该线程的打断标记会置为true,但是不会去停止被打断的线程,只是告诉它我想要打断,要真正打断还是需要它自己去停止自己,即给它处理后事的机会
说明: 可以调用Thread类的isInterrupted方法获取线程打断标志
打断正在sleep、wait、join的线程:
private static void test1() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
Thread.sleep(500);
t1.interrupt();
log.debug(" 打断状态: {}", t1.isInterrupted());
}
打断正常运行的线程:
private static void test3() 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();
}
可以看到,真正停止线程的还是t2自己,只是main线程告诉t2线程说“我要打断你,你处理好后事后自己停止”
注意: Thread类有一个静态方法 Thread.interrupted(),它也会返回线程打断标志,但是和isInterrupted不同的是返回之后会置打断标志为false。
park/unpark方法
- LockSupport.park():暂停当前线程
- LockSupport.unpark(Thread t):恢复线程 t 的运行
注意:
- park之后的线程状态是WAITING状态
- 可以提前unpark,要是执行到park时,发现提交提前被调用了unpark,就继续执行
- 不必和Monitor配合使用