目录
1. Thread类常用构造方法
2. Thread类的几个常见属性
3. 启动一个线程
4. 中断一个线程
4.1 方法1:手动设置标志位
4.2 方法2:使用Thread内置的标志位
5. 等待一个线程
6. 获取当前线程引用
7. 休眠当前线程
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象并命名 |
Thread(Runnable target,String name) | 使用Runnable对象创建线程对象并命名 |
Thread(ThreadGroup group,Runnable target) | 使用线程组对线程分组管理 |
前两个构造方法前文已经使用,此处不再赘述,此处主要展示带有命名的线程对象的创建:
public class Demo1 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while(true){
System.out.println("Hello Thread1.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "Thread t1");
t1.start();
Thread t2 = new Thread(()->{
while(true){
System.out.println("Hello Thread2.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "Thread t2");
t2.start();
}
}
运行代码并根据jdk安装路径打开bin文件找到jconcole.exe文件:
打开在本地进程中连接对应进程:
进入界面选择进程后,就可以看到正在运行的线程,点击线程还可以显示线程的执行位置:
注:(1)给Thread命名仅便于调试时对线程进行区分,对于线程执行没有其他影响;
(2)java进程启动后不只有我们手动编写的线程,还有一些JVM自己创建的线程用于其他的不同工作,比如收集统计调试信息,监听网络链接等等;
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台程序 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
注:(1)ID是线程的唯一标识,不同线程不会重复;
(2)获得的名称也就是构造时指定的名称,主要用于调试时便于查看线程状态;
(3)状态表示当前线程所处的状态,后续详细介绍;
(4)优先级高的线程理论上来说更容易被调度到;
(5)后台线程:
① 如果一个线程是后台线程,就不影响进程退出,main方法执行完毕后就会结束进程,并强行终止后台线程;
如果一个线程是前台线程,即使main方法执行完毕,也必须等前台线程执行完毕1才能结束进程,JVM会在一个进程的所有非后台线程结束后才会结束运行;
② 代码里手动创建的线程(包括main线程)默认是前台线程,其他的jvm自带的线程都是后台线程,也可以使用setDaemon将一个前台线程设置为后台线程;
③ 注意区别线程调度与后台线程;
(6)是否存活可以理解为:操作系统中对应的线程是否正在运行;
Thread t 对象的生命周期和内核中对应的线程生命周期并不完全一致,t对象被创建后,在调用start方法之前,系统中是没有对应线程的,在run方法执行完毕后,系统中的线程就销毁了,但是t对象还可能存在;
在调用start之后,run执行完之前,isAlive就是返回true,
如果是调用start之前,run执行完之后,isAlive就是返回false;
(7)线程的中断问题,后续详细介绍;
public class Demo2 {
public static void main(String[] args) {
Thread t = new Thread(()->{
while(true){
System.out.println("Hello Thread.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
while(true){
System.out.println("Hello Main.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
运行以上代码,截取部分输出结果如下:
将上文代码的t.start()语句更换为t.run()语句再试运行,截取部分输出结果如下:
因为run方法只是一个普通的方法,在main线程中调用run,其实并没有创建心得线程,上文中的循环语句依然是在main线程中执行的,在一个线程中,代码就会按照从前至后的顺序进行运行,此时就只会在第一个循环结构中一直打印Hello Thread.;
可以通过修改循环条件进行验证:
public class Demo2 {
public static void main(String[] args) {
Thread t = new Thread(()->{
for(int i=0;i<3;i++){
System.out.println("Hello Thread.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.run();
for(int i=0;i<2;i++){
System.out.println("Hello Main.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
再运行代码,输出结果为:
注:run方法只是一个遵循串型执行的、描述任务内容的普通调用方法;
调用start方法,才会在操作系统的底层创建一个线程;
中断一个线程的关键在于使当前线程对应的run方法执行完;
但对于特殊的main线程来说,需要main方法执行完线程才完;
此处的标志位是自己创建的变量,来控制线程是否要执行结束:
// 通过设置标志位中断一个线程
public class Demo2 {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(flag){
System.out.println("Hello Thread.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
//在主线程中通过更改flag变量的取值来操作t线程是否结束
//当flag为假时不再进入循环
Thread.sleep(3000);
flag = false;
}
}
输出结果为:
注:(1)t线程开始执行后,当3s时间截止后,标志位flag被修改,此时t线程run方法所在循环被终止,t线程终止,再后的main方法也执行完毕,main线程也终止,同时进程也终止了;
(2)此处因为多个线程共用一个虚拟地址空间,因此main修改的flag与t线程判定的flag是同一个值;
(3)以上写法并不严谨,后续再进行介绍,同时由于手动设置标志位来中断一个线程不能及时响应,尤其是在sleep休眠时间比较久时,故而引出中断一个线程的第二种方法;
① 判断线程是否被中断:Thread.currentThread().isInterrupted()实例方法,其中currentThread可以获取到当前线程的对象的引用;
② 设置线程中断位置:t.interrupt()
(更推荐② 一个代码中的线程可能有很多,随时哪个线程都有可能终止,①方法表示一个程序只有一个标志位,但是②方法判定的标志位是Thread的普通成员,每个实例都有自己的标志位;)
代码示例1:
public class Demo4 {
public static void main(String[] args) {
Thread t = new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
System.out.println("Hello Thread.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
//在主线程中调用interrupt方法来中断t线程
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中断t线程
t.interrupt();
}
}
运行结果如下:
注:(1)调用interrupt()方法可能产生两种情况:
如果调用线程处于就绪状态,则是设置线程标志位为true;
如果调用线程处于阻塞状态(sleep),interrupt方法就会强行打破休眠,则sleep会触发InterruptException异常,导致线程从阻塞状态被唤醒,将线程从sleep中被唤醒时,又会将原先设定的标志位再设置回false,即清空了标志位;
不只是sleep方法可以将线程意外中断后清除标志位,像join、wait等可以造成线程堵塞的方法都有类似于清除标志位的设定;
(2)上文代码一旦出发了异常就会进入catch语句,在catch语句中,就打印出当前异常位置的代码调用栈,打印完毕后继续运行;
这显然不是我们期待的结果,故而需要修改:
代码示例2:
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
System.out.println("Hello Thread.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//触发异常后稍后几秒再终止t线程
try{
Thread.sleep(1000);
}catch(InterruptedException ex){
ex.printStackTrace();
}
//触发异常后立刻退出循环
break;
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt(); //通知终止t线程
}
}
在t线程抛出Interrupted异常后立刻停止工作,输出结果为:
也可看出interrupt唤醒sleep中的线程后,要将标志位重新置回的意义:
t.interrupt()只是main线程通知t线程终止,而t线程是否终止取决于其本身,重新将标志位置回意味着可以在t线程内部选择继续执行、终止、稍后终止,否则t线程只能立刻终止。
多个线程之间的调度顺序是不确定的,线程之间的执行顺序是调度器无序、随机执行的,有时我们需要控制线程之间的执行顺序,线程等待就是控制线程结束先后顺序的重要方法,哪个线程调用join()方法,哪个线程就会阻塞等待对应线程的run方法执行结束为止。
代码示例1:当被等待的线程尚未执行完毕时:
public class Demo4 {
public static void main(String[] args) {
Thread t = new Thread(()->{
for(int i=0;i<3;i++){
System.out.println("Hello Thread.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
System.out.println("Hello Main");
//让main线程等待t线程的run方法执行完毕
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello Main");
}
}
输出结果为:
注:(1)上文代码是main线程调用join()方法,是针对t这个线程对象调用的,此时就是让main等待t,调用join()方法后,main线程就会进入阻塞状态,直到t的run()方法执行完毕后,main线程才会继续执行;
(2)干预两个线程的执行顺序体现在:通过线程等待控制先让t结束,main后结束;
(3)注意区别干预线程执行顺序与优先级的区别:优先级是操作系统内部内核进行线程调度使用的参考量,在用户层面代码不能完全干预或控制,线程执行顺序是代码中控制的先后顺序;
代码示例2:当被等待的线程已经执行完毕时:
public class Demo4 {
public static void main(String[] args) {
Thread t = new Thread(()->{
for(int i=0;i<3;i++){
System.out.println("Hello Thread.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello Main");
//让main线程等待t线程的run方法执行完毕
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello Main");
}
}
输出结果为:
如果被等待的线程在其他线程尚未执行到等待该线程的语句时该线程已经执行完毕,则join直接返回;
注:join()操作默认情况下是持续等待的,为了避免这种机制带来的麻烦,join提供了另外一个版本,可以设定最长等待时间(等待时间上限),具体内容请看示例3:
代码示例3:指定join的等待上限:
public class Demo4 {
public static void main(String[] args) {
Thread t = new Thread(()->{
for(int i=0;i<5;i++){
System.out.println("Hello Thread.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
System.out.println("Hello Main");
//让main线程等待t线程的run方法执行完毕
try {
t.join(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello Main");
}
}
输出结果为:
上文代码t.join(2000)代表:main线程进入阻塞状态等待t线程run方法执行完毕,如果2s之内t线程执行结束了,此时join直接返回,如果3s之后t线程还未结束,join也直接返回;
Thread.currentThread()是一个静态方法,能获取当前线程实例的引用,哪个线程调用则获取哪个线程实例的引用;
代码示例1:通过继承Thread类创建线程:
public class Demo6 {
public static void main(String[] args) {
Thread t = new Thread("Thread t"){
@Override
public void run(){
System.out.println(Thread.currentThread().getName());
}
};
t.start();
//main线程
System.out.println(Thread.currentThread().getName());
}
}
输出结果为:
由于本方式是通过继承Thread类来创建接口的,故而也可以通过this.getName()方式获取当前类的对象的引用:
public class Demo6 {
public static void main(String[] args) {
Thread t = new Thread("Thread t"){
@Override
public void run(){
//System.out.println(Thread.currentThread().getName());
System.out.println(this.getName());
}
};
t.start();
//main线程
System.out.println(Thread.currentThread().getName());
}
}
代码示例2:通过实现Runnable接口创建接口:
这种方式创建线程,则不能通过this.getName()方法获取当前对象的引用:
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(this.getName());
}
});
由于此时this不是指向Thread类型了,而是指向Runnable,Runnable只是一个任务,没有name属性;
只能通过Thread.currentThread().getName()方式获取当前对象的引用:
Thread t = new Thread(new Runnable() {
@Override
public void run() {
Thread.currentThread().setName("Thread t");
System.out.println(Thread.currentThread().getName());
}
});
t.start();
输出结果为:
在前文已经介绍过:对于一个线程的进程,在系统中是通过PCB描述的,通过双向链表组织的;
对于一个有多个线程的进程,每个线程都有一个PCB,一个进程对应的就是一组PCB,PCB上有一个tgroupId,这个id就相当于是进程的id,同一个进程中的若干个tgroupId是相同的;
PCB即process control block进程控制块,其实在Linux内核是不区分进程与线程的,只有在程序员写应用程序代码时才会进行区分,Linux内核只识别辨认PCB,在内核中线程被称为轻量级进程;
实际在操作系统中调度线程的时候,会从就绪队列中挑选合适的CPB到CPU上运行,而执行了sleep()或join()语句的PCB位于阻塞队列就无法上CPU,只有当休眠结束,再次进入就绪队列才会在后续上CPU被执行;
让线程休眠本质就是让该线程不参与调度了,如令该线程休眠1000ms,其实在该线程从阻塞队列被迁移回就绪队列后并不会被立刻调度,往往令休眠的线程的休眠时间要超过手动设置的时间;