目录
一、Thread类的常见构造方法
二、Thread 的几个常见属性
三、和线程相关的几个重要的操作
3.1 启动线程 - start()
3.2 中断线程
3.3 等待线程 - join()
3.4 获取到线程引用
3.5 休眠线程 - sleep()
四、线程的状态
4.1 Java 线程中的基本状态
4.2 线程之间的状态是如何转换的
Thread():
默认无参构造方法,如 :Thread t1 = new Thread();
Thread(Runnable target):
使用 Runnable 创建一个任务,再把 任务 放到线程里面,
如 Thread t2 = new Thread(new MyThread());
Thread(String name):
给线程起一个名字,线程 在操作系统内核里 没有名字,只有一个身份标识~
但是 Java中 为了让程序猿 调试的时候方便理解,这个线程是谁,就在 JVM 里给对应的 Thread对象 加了个名字(JVM中的 Thread对象 和 操作系统内核里面的线程 一一对应)
这个名字对于程序的执行没影响。
如果不手动设置,也会有默认的名字,形如 Thread-0、Thread-1、......
Thread(Runnable target):
使用 Runnable对象 创建线程,并命名
getId():获取到的是 线程在 JVM 中的身份标识
线程中的身份标识是有好几个的:
- 内核的 PCB 上有标识
- 用户态线程库里也有标识(pthread,操作系统提供的线程库)
- JVM 里又有一个标识(JVM Thread类 底层也是调用操作系统的 pthread 库)
三个标识各不相同,但是目的都是作为身份的区分
由于 介绍的是 Java程序,所以我们只需要知道 JVM 中的身份标识即可~
getName():
获取到 自己在 Thread 构造方法里所起的名字
getState():
PCB 里面有状态,此处得到的状态是 JVM 里面设立的状态体系,这个状态比操作系统内置的状态要更丰富一些
getPriority():
获取到优先级
isDaemon():
这里的后台线程也叫守护线程
- 前台线程会阻止进程的结束,前台线程的工作没做完,进程是完不了的,
- 后台线程不会阻止进程的结束,后台线程的工作没做完,进程照样可以结束,
- 换句话说,进程会保证所有的前台线程都执行完了 才会退出~
类似于 手机app,打开一个app,此时这个app就来到了 "前台",当按到 "回到菜单" 这样的按钮,此时app就切换到 "后台"~
线程也分成 前台线程 和 后台线程, 我们手写的线程默认都是前台线程,包括main也是默认前台,但是我们可以通过setDeamon()设置成后台线程后台线程不会阻止进程结束,所以 进程退出的时候 不关心后台进程是否执行完 就退出了
package thread; public class Demo7 { public static void main(String[] args) { Thread t = new Thread(new Runnable() { @Override public void run() { while (true) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } },("我的线程")); t.start(); } }
执行结果:
分析:
由于该线程是一个前台线程,所以需要等待其运行结束,进程才会结束,所以会一直执行下去
再如:我们可以把他手动设置成 后台线程:t.setDaemon(true);
package thread; public class Demo7 { public static void main(String[] args) { Thread t = new Thread(new Runnable() { @Override public void run() { while (true) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } },("我的线程")); //手动设置成 后台线程 t.setDaemon(true); t.start(); } }
运行结果:
分析:
通过 setDaemon(true) 可以把线程设置为后台线程,等到主线程执行完,进程就结束了~
需要注意的是,先要 设置线程的前后台,然后再启动线程
isAlive判断当前线程是否还存活,调用 start 前 isAlive 是 false,调用后 isAlive是true
虽然线程把run里要干的事干完后就销毁了,pcb也随之释放了,但是Thread t这个对象还不一定被释放,此时isAlive也是false,t的销毁要等GC回收
总结:
package thread;
public class Demo7 {
public static void main(String[] args) {
Thread t = new Thread(() ->{
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"我的线程");
t.start();
System.out.println("线程Id:" + t.getId());
System.out.println("线程名称:" + t.getName());
System.out.println("线程状态:" + t.getState());
System.out.println("线程优先级:" + t.getPriority());
System.out.println("是否后台线程(true/false):" + t.isDaemon());
System.out.println("是否存活:" + t.isAlive());
}
}
运行结果:
注意:这些操作都是获取到的是 一瞬间的状态,而不是持续性的状态~
new Thread实例的时候,并没有真的在操作系统内核里创建出线程(仅仅只是在安排任务,"跑步时 的 '各就各位,预备' ")!!!
而是 调用 start,才是真正创建出线程("发令枪响")!!!
线程的概念和创建【javaee初阶】_嘎嘎烤鸭的博客-CSDN博客
怎么让线程执行结束,其实方法很简单:只要让线程的 入口方法 执行完了,线程就随之结束了~
主线程 的入口方法,就可以视为 mian方法~
所谓的 "中断线程",就是让线程尽快把 入口方法 执行结束~
方法一:直接自己定义 标志位 来区分线程是否要结束~
package thread;
public class Demo9 {
//用一个布尔变量来表示 线程是否要结束
//这个变量是一个成员变量,而不是局部变量
private static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!isQuit) {
System.out.println("线程运行中......");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("新线程执行结束!");
});
t.start();
Thread.sleep(5000);
System.out.println("控制新线程退出!");
isQuit = true;
}
}
这个代码中,控制线程结束,主要是这个线程里有一个循环,这个循环执行完,就算结束了~
很多时候创建线程都是 让线程完成一些比较复杂的任务,往往都有一些循环,正是因为有循环,执行的时间才可能比较长;如果线程本身执行的很快,刷的一下就结束了,那么也就没有提前控制它的必要了~
方法二:使用 Thread类 中自带的标志位~
Thread 其实内置了一个标志位,不需要咱们去手动创建标志位Thread.currentThread().isInterrupted()
currentThread() 是 Thread类的静态方法,获取到当前线程的实例,这个方法中有一个线程会调用它
线程1 调用这个方法,就能返回线程1 的 Thread对象;
线程2 调用这个方法,就能返回线程2 的 Thread对象~
类似于 JavaSE 里面的 this~
isInterrupted() 为判定内置的标志,可以获取到标志位的值,为 true 表示线程要被中断~
package thread;
public class Demo10 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程运行中......");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(5000);
System.out.println("控制新线程退出!");
t.interrupt();
}
}
interrupt会做两件事:
说明:调用了 interrupt,产生了一个异常。异常虽然出现了,但是线程仍然在继续运行。
注意理解 interrupt方法 的行为:
- 如果 t 线程 没有处在阻塞状态,此时 interrupt 就会修改内置的标志位~
- 如果 t 线程 正在处于阻塞状态,此时 interrupt 就让线程内部产生阻塞的方法,例如 sleep 抛出异常~
例如:
- 线程在sleep中休眠,此时调用interrupt会把t线程唤醒,从sleep中提前返回。
- interrupt会触发sleep内部的异常,导致sleep提前返回
上述循环代码中,正好异常被捕获了~
而如果 把上述代码中的 e.printStackTrace(); 给注释掉,那么就啥也不打印,运行结果调用栈也不打印了,直接忽略异常了。
正是因为这样的捕获操作,程序员就可以自行控制线程的退出行为了
注意:调用interrupt只是通知终止,线程会不会终止,还得看代码怎么写的
当然,除了 Thread.currentThread().isInterrupted(),Thread类还自带了一个静态方法 interrupted() 也可以访问标志位。使用 Thread.interrupted() 即可.一般掌握一个就可以了
我们已经知道,线程是一个随机调度的过程,线程之间的执行顺序完全是随机的,看系统的调度
一般情况下写代码,其实是比较讨厌这种随机性的,更需要能够让顺序给确定下来
join() 等待线程就是一种确定线程执行顺序的辅助手段,来控制两个线程的结束顺序。
join() 的行为:
如:咱们不可以确定两个线程的开始执行顺序,但是可以通过 join() 来控制两个线程的结束顺序~
private static void concurrency() {
//concurrency 的意思是 "并发"
long begin = System.currentTimeMillis();
Thread t1 = new Thread(() ->{
int a = 0;
for(long i = 0; i < count; i++) {
a++;
}
});
Thread t2 = new Thread(() ->{
int a = 0;
for(long i = 0; i < count; i++) {
a++;
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("多线程并发执行的时间:" + (end-begin) + "毫秒");
}
我们知道,main、t1、t2 三个线程是随机调度执行的~
但是此处的需求是,希望让 t1、t2 都执行完了之后,main再进行计时,来统计执行时间~
就是希望 t1、t2 先执行完,main 后执行完~
咱们无法干预 调度器的行为(调度器还是该咋随机咋随机),但是可以让 main 线程进行等待~
就在 main 里分别调用了 t1.join() 和 t2.join() ~
当 t1、t2 都执行完了以后,main 解除阻塞,然后才能继续往下执行,才能执行完
main 阻塞了,就不参与调度了,但是 t1、t2 仍然参与调度,它们的执行还是会 随机调度、交替执行的~
main 有点特殊,不太方便 join()
一般情况下,想让谁阻塞,谁就调用 join() 即可
如:要实现让 t2 等待 t1,main 等待 t2,就可以 main 去调用 t2.join(),t2 调用 t1.join() 即可
package thread; public class Demo11 { private static Thread t1 = null; private static Thread t2 = null; public static void main(String[] args) throws InterruptedException { System.out.println("main begin"); t1 = new Thread(() -> { System.out.println("t1 begin"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1 end"); }); t1.start(); t2 = new Thread(() -> { System.out.println("t2 begin"); try { t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2 end"); }); t2.start(); t2.join(); System.out.println("main end"); } }
package thread; //控制 main 先运行 t1,t1 执行完 再执行 t2 public class Demo12 { public static void main(String[] args) throws InterruptedException { System.out.println("main begin"); Thread t1 = new Thread(() -> { System.out.println("t1 begin"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1 end"); }); t1.start(); //等待 t1 结束 t1.join(); Thread t2 = new Thread(() -> { System.out.println("t2 begin"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2 end"); }); t2.start(); t2.join(); System.out.println("main end"); } }
当然,join() 还有其他的版本
在实际的开发过程中,在一般情况下,都不会使用 "死等" 的方式,因为 "死等" 的方式有风险。
万一代码出了 bug 没控制好,就很容易让服务器 "卡死",无法继续工作。
更多的情况下是 等待的时候预期好最多等多久,超过时间了就需要做出一些措施。
为了对线程进行操作,就需要获取到线程的引用
这里的操作,就是指:线程等待、线程中断、获取各种线程的属性
如果是 继承 Thread类,重写 run方法,可以直接在 run 中使用 this 即可获取到线程的实例
但是,如果是 Runnable 或者 Lambda,this 就不行了(指向的就不是 Thread实例)
更通用的方法是,使用 Thread.currentThread() ,currentThread() 是一个静态方法,其特点是 哪个线程来调用这个方法,得到的线程就是哪个实例
sleep() 能够让线程休眠一会儿~
前面已经所介绍了 sleep() 的使用方法,现在来画图介绍一下 sleep() 到底是如何使用的~
注意:
如果写了一个 sleep(1000),那么也不一定 就会在1000ms 之后就上 CPU 运行~
1000ms 之后只是把这个 PCB 放回了就绪队列!!!至于说这个线程啥时候执行,还得看调度器的 调度
因此,sleep(1000) 意味着休眠时间不小于 1000ms,实际的休眠时间会略大于 1000ms,这个误差精度在 10ms 以下
操作系统中 进程的状态 有 阻塞状态、就绪状态、执行状态~
而在 Java/JVM里 线程中也有一些状态,更是对此做出了一些细分~
Blocked、Waiting、Time Waiting 三种状态都是 阻塞状态,只不过是产生阻塞状态的原因不一样,Java里面使用不同的状态来表示了
package thread;
public class Demo13 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//在 t.start() 之前获取,获取到的是 线程还未创建的状态
System.out.println(t.getState());
t.start();
t.join();
//在 t.join() 之后获取,获取到的是线程已经结束之后的状态
System.out.println(t.getState());
}
}
主干道是:New => Runnable => Terminated
在 Runnable状态会根据特定的代码进入支线任务,这些 "支线任务" 都是 "阻塞状态"
这三种 "阻塞状态",进入的方式不一样,同时阻塞的时间也不同,被唤醒的方式也不同
如:sleep() 等到时间到了自动唤醒,至于 wait() 和 synchronized() 是如何唤醒的以后会介绍的