一、 常见方法
1.1 概述
① start_vs_run:直接调用run方法并不会启动新的线程
import cn.itcast.n2.util.FileReader;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test")
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread("t1"){
@Override
public void run() {
log.debug("running");
FileReader.read(Constants.MP4_FULL_PATH);
}
};
t1.run();
log.debug("do other things......");
}
}
执行结果:直接调用run方法并不会启动新的线程, FileReader.read() 方法调用还是同步的,还是在主线程中调用run,并不能达到提升性能或者异步处理的效果,因此启动一个线程必须要start()。start()启动了线程,再由新的线程去调用run()方法
结论:
● 直接调用 run 是在主线程中执行了 run,没有启动新的线程
● 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
② sleep与yield
sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting (有时限的等待)状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit(1.5以后新增) 的 sleep 代替 Thread 的 sleep 来获得更好的可读性(其内部也是调用了sleep方法,但其进行了单位的换算使程序可读性更好)
yield
- 调用 yield(让出CPU的使用权) 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
sleep是让线程进入Timed Waiting(阻塞)状态,yield是让线程进入 Runnable(就绪)状态,两个状态表面上都是让线程先不运行,将机会让给其他线程。两者的区别是:
Runnable(就绪)状态:还是有机会被任务调度器调度(任务调度器还是会分时间片给就绪状态的线程)
Timed Waiting(阻塞)状态:任务调度器不会将时间片分给阻塞状态的线程
③ 程序优先级
Thread类中的setPriority()可以设置优先级
● 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
● 如果 cpu 比较繁忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
yield与线程优先级并不是很容易控制线程的调度
yield操作示例:(如果以下两个线程同时运行并且被CPU调度的机会均等,则最终的打印结果count值较为接近)
1. 不加任何控制
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test9")
public class Test9 {
public static void main(String[] args) {
// 任务对象1(死循环)
Runnable task1 = () -> {
int count = 0;
for (;;) {
System.out.println("---->1 " + count++);
}
};
// 任务对象2(死循环)
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();
}
}
被CPU调度机会大致形同
2. 线程2开启yield()===>线程1得到的时间片就较多进而导致线程1数字增长就较为快些
线程1、2增长效果差异较为明显
3. 使用优先级,将线程1的优先级设置较低,线程2的优先级较高
并没有明显效果
总结:无论是yield()还是优先级,他们仅仅是对调度器的一个提示,均不能真正去控制线程任务的调度。最终还是由操作系统的任务调度器来决定哪个线程获得更多的时间片
④ 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("开始");
// 线程1睡眠1秒
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() 之后即可
⑤ join-同步应用(案例1)
以调用方角度来讲,如果
● 需要等待结果返回,才能继续运行就是同步(上述例子中主线程需要同步等待t1线程结果返回)
● 不需要等待结果返回,就能继续运行就是异步
问,下面代码 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);
}
执行结果:(时间差大约为2秒)
分析如下
● 第一个 join:等待 t1 时, t2 并没有停止, 而在运行
● 第二个 join:1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s
有时效的 join
1. 未等够时间
import lombok.extern.slf4j.Slf4j;
import static cn.itcast.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.Test")
public class Test {
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();
log.debug("join begin");
// 主线程等待t1运行结束(最多等待1.5s)
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
}
运行结果:(1.5s后主线程便不再等待)
2. 等够时间
将 t1.join(1500)修改为 t1.join(3000);
运行结果:
如果线程提前结束,join()也会提前结束,并不会按最大时间等待;若未等够时间,则按时间结束
⑦ interrupt-打断
1. 打断阻塞状态的线程
● 打断 sleep,wait,join(这几个方法都会让线程进入阻塞状态) 这些处于阻塞状态的线程,也可以用来打断正在运行状态的线程
* join的底层原理也为wait
打断 sleep 的线程, 会清空打断标记,以 sleep 为例
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test11")
public class Test11 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("sleep...");
try {
Thread.sleep(5000); // wait, join
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
// 打断正在睡眠的线程,否则t1还未进入睡眠打断的为正常运行下的线程
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
// 正常情况下打断标记为ture,反之为false
log.debug("打断标记:{}", t1.isInterrupted());
}
}
运行结果:
阻塞状态的线程被打断后会抛出异常(InterruptedException),打断后还会有一个打断标记(布尔值),表示当前线程是否被其他线程所打断干扰过,但sleep,wait,join它们在被打断后都会将打断标记清空(重置为false)
2. 打断正常运行的线程
打断正常运行的线程,不会打断清空状态
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test12")
public class Test12 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true) {
// 获取当前线程(t1),判断是否被打断
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
log.debug("被打断了, 退出循环");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
// 主线程1s后打断t1线程
t1.interrupt();
}
}
运行结果:
打断状态可以用来停止线程
1.2 sleep应用案例(防止CPU占用100%)
sleep实现
while(true)应用:===>在做服务器开发时,编写服务端应用程序需要线程不断运行进而一直接收请求、访问、响应
在没有利用CPU来计算时,不要让while(true)空转浪费CPU,这时可以使用yield或sleep来让出CPU的使用权给其他程序
while (true){
try {
// 防止不断利用CPU计算,空转浪费CPU资源(防止对CPU资源的占用)
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
● 可以用wait或条件变量达到类似的效果
● 不同的是,后两种都需要加锁,并且需要响应的唤醒操作,一般适用于要进行同步的场景
● sleep使用于无需锁同步的场景