Java程序创建启动的时候,默认就有一个线程,也就是主线程,在运行了。
一、创建和运行线程
1)方法1:直接使用Trhead
//创建线程对象
Thread t = new Thread() {
@Override
public void run() {
//要执行的任务
}
};
//设置线程名称 默认是Tread-x x表示数字从0递增
t.setName("t1");
//启动线程
t.start();
2)方法2:使用Runnable 配合 Thread
把【线程】和【任务】(要执行的代码)分开
- Thread 代表线程
- Runnable 可运行的任务 (线程要执行的代码)
Runnable runnable = new Runnable() {
@Override
public void run() {
// 要执行的任务
}
};
//创建线程对象 可以使用两个参数的构造,第二个参数表示设置线程名称
Thread t = new Thread( runnable );
//启动线程
t.start();
3)简化
Java 8 以后可以使用功能lambda精简代码
有且仅有一个抽象方法的接口,会在接口上方添加注解,这种接口可以使用lambda 精简代码
lambda语法可以跳转这篇
此处为语雀内容卡片,点击链接查看:https://www.yuque.com/liziing/qm71mg/hqlcf3pgsey339bi
4) 总结
- 方法1 是把线程和任务合并在一起, 方法2 是把线程和任务分开了
- 用Runnable 更容易与线程池等高级API 配合
- 用 Runnable 让任务类脱离了Thread 继承体系,更灵活
5)方法3:FutrueTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数, 用来处理有返回结果的情况
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// 执行的任务
//返回的值
return xxx;
}
});
Thread t = new Thread(task, "t3");
t.start();
//主线程阻塞, 同步等待 task 执行完毕的结果
task.get();
二、原理之线程运行
1)栈 与 栈帧
JVM由堆、栈、方法区所组成,栈内存是给线程用的,每个线程启动,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧(Frame) 组成,对应着每次方法调用所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
2) 线程上下文切换(Thread Context Switch )
使用cpu -> 不使用cpu 称为一次线程上下文切换
因为以下一些原因导致 cpu 不在执行当前的线程, 转而执行 另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、 wait、 join、 park、 synchronized、 lock 等方法
当Context Switch 发生时,需要有操作系统保存当前的状态,并恢复另一个线程的状态,Java中对应的概念是程序计数器(Program Counter Register),它的作用是记住下一条jvm指令的执行地址,是线程私有的。
- 记录的状态有:程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会印象性能
三、常见方法
1) start 与 run
线程需要通过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.debud("do other things ...);
2)sleep 与 yield
1. Sleep()
- 调用sleep方法之后,当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 正在睡眠的线程,可以通过其他线程 调用正在睡眠的线程的 interrupt 方法来打断正在睡眠的线程,这时候 sleep 方法被打断了终止了,会抛出 InterruptedException 异常
- 睡眠结束后的线程未必会立刻得到执行(可能CPU的时间片正在执行其他线程,需要等待时间片到来)
- 建议用 TimeUnit 的sleep 代替 Thread 的sleep 来获取更好的可读性 (TimeUnit.SECONDS.sleep(1) <=> Thread.sleep(1000))
2. Yield()
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态, 然后调度执行其他线程
- 但是让出了cpu使用权,这时候如果没有 其实时间片可以执行,那么也是会回到自己身上
3. 总结:
- sleep 调用是进入等待阻塞状态, cpu时间片不会分配给阻塞状态的线程,需要等到他醒过来进入就绪状态。
- yield 调用是进入 就绪 状态, cpu 是会将时间片分配给就绪状态的线程, 也就是说如果没有其他线程,这时候yield 也就是没让出去
- sleep 有时间的概念, 但是yield 是没有的,只是当前这一刻的cpu使用权让出去了,如果没有其他线程,还是会重新分配给当前线程。
4. setPriority()
- 调用setPriority可以设置线程的优先级, 1-10 ,默认5, 数字越大,优先级越高,CPU分配时间片的概率就大
- 但是具体还是要看任务调度器具体分配,有可能设置之后,没有效果
- 因为如果cpu比较忙的时候,设置优先级高的的线程会获得更多的时间片,但是如果cpu比较闲,那么设置就几乎没作用
5. 案例 - 防止 CPU 占用 100 %
sleep实现
在没有利用cpu来计算时,不要让 while(true) 空转浪费cpu,这时可以使用yield 或 sleep 来让出cpu的使用权给其他程序
3)join
同步应用
场景:
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(1);
r = 10;
});
t1.start();
// t1.join(); // 解决方法
log.debug("结果为:{}", r);
}
因为线程1等待了1秒,所以主线程已经输出了 r 的结果,结果为0
解决:
可以让主线程 sleep 1秒来得到 r = 10 的结果,但是这种方式需要我们知道sleep具体的等待时间,所以不好控制
可以让 要等待哪个线程结束就让哪个线程调用 join 方法,这样就会等待 线程执行结束之后才会往下执行
限时同步
join (毫秒数) 可以添加参数,指的是具体等待多少毫秒之后结束,如果线程还没有执行完,也会直接结束
但是如果等待超过了线程的执行时间,那么具体会以 线程执行时间为准,不会真的等待那么长时间,会提前结束
4) interrupt
1. 打断阻塞(sleep、wait、join)的线程
- 打断等待中的线程之后,会清空打断状态,也就是调用打断的线程调用 isInterruped()方法显示为false
- 因为打断等待中的线程会抛出 InterruptedException异常,然后将 是否打断的标记置为 false
2. 打断正常线程
- 正常的线程调用interrupt打断方法之后,线程会被标记成被打断,调用 isInterruped()方法显示为true
- 但是线程不会因此而终止,只是有一个标记是打断了,然后可以通过这个变量来判断是否需要终止运行
3. 设计模式
3.1. 两阶段终止模式(Two Phase Termination)
3.1.1. 错误思路
3.1.2. 执行流程
4. 补充:
调用下面方法,如果判断是true,然后会将true置为false,这是两者区别
5. 打断park()
park() 不是 Thread中的方法,是LockSupport中的方法,线程中调用park方法之后,会让线程停在park位置,
当其他线程调用了park所在的线程的interrupt方法,也就是打断之后,那么就会终止park,继续执行park所在线程中的代码。
但是一旦打断之后,打断标记为true,在park线程中第二次调用park就失效了,因为已经是打断标记是true了,所以这时候可以用到上面的interrupted方法,直接置为false。
才能使得第二次调用有效停止。
5)主线程和守护线程
默认情况下,Java进程需要等待所有线程都运行结束,才会结束。
有一种特殊的线程叫做守护线程,调用线程的setDaemon(true)
只要其他非守护线程运行结束了,即使守护线程的代码还没有执行完,也会强制结束。
注意:
- 垃圾回收器线程就是一种守护线程,如果没有引用指向之后就会执行垃圾回收,如果程序终止了,那么垃圾回收也就终止了。
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待他们处理完当前请求。