线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户层提供了一些 API 供用户使用(如 Linux 中的 pthread 库)。
所以本身关于线程的操作,是依赖操作系统提供的的 API,而 Java 的 JVM 已经把很多操作系统提供的功能封装好了,我们就不需要学习系统原生的 API,只需要学习 Java 提供的 API 就好了。
在 Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进一步的抽象和封装!
可以认为,Java 操作多线程最核心的类就是 Thread 类!
class MyThread extends Thread {
@Override
public void run() {
System.out.println("hello world");
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
上述就是我们第一个多线程代码,使用多线程打印 "hello world"
这里是第一个创建线程的方式,继承 Thread 类,重写 run 方法!
上述代码中的 t.start(); 这里的工作就是创建了一个新的线程,而这个线程负责执行 t 对象中的 run 方法.
start 方法创建一个新的线程,本质上就是调用操作系统的API,通过操作系统内核创建新线程的 PCB,并且把要执行的指令交给这个 PCB,当 PCB 调度到 CPU 上执行的时候,也就执行到了线程的 run 方法中的代码了!
注意:这里可能有个让人误解的地方,start 方法里是没有调用 run 方法的,start 只是创建了一个线程,由新创建的线程去调用 run 方法!
上述我们代码的执行流程就是:主线程(main线程) 中调用 t.start(); 创建了一个新线程,这个新线程调用 t.run(); 如果 run 方法执行完结束了,这个新的线程也会随之销毁。
start 方法是真正创建了一个线程(从系统这里创建的),线程是一个独立的执行流.
run 方法只是描述了线程要干什么样的活,如果直接在 main 方法调用 run,此时是不会创建新线程的,这个 run 方法会在 main 线程中执行:
public static void main(String[] args) {
MyThread t = new MyThread();
t.run();
}
上述这种情况,只是单纯在 main 线程中执行 t 对象里的 run 方法罢了!
提问:new 一个 Thead 对象是在干嘛呢?
其实也就是创建一个对象罢了,只不过这个对象能够通过 start 方法创建一个线程罢了!
我们也可以通过 jdk 自带的工具 jconsole 查看当前的 java 进程中的所有线程(bin 目录下):
因为进程和线程之间是包含关系,当要查看线程的时候,需要先连接上指定的进程,才能看指定进程中所拥有的线程。
此处可以看到一个是 main 线程,也就是主线程,还有一个是我们创建的线程这个默认起了个名字 Thread-0,除了这两个线程之外,其他的线程都是 JVM 自带的,这里我们不用过多关心,后续还会使用这个工具进行查看线程的阻塞状态等
● 继承 Thread 重写 run
这里上述我们简单使用多线程的时候已经见过了,这里就不过多讲述了。
● 实现 Runnable 接口 重写 run
class MyThread implements Runnable {
@Override
public void run() {
System.out.println("hello world");
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 描述一个任务
Runnable runnable = new MyThread();
// 把任务交给线程通过 start 方法来执行
Thread t = new Thread(runnable);
t.start();
}
}
上述的 runnable 对象,只是描述了一个任务,这里的写法最主要就是解耦合,目的让线程和线程要干的活之间分离开。
● 使用匿内部类 继承 Thread
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
System.out.println("hello world");
}
};
t.start();
}
}
这里创建了一个 Thread 的子类,但是是没有名字(匿名)的,Thrad() 后面大括号中表示子类重写父类 Thread 的 run 方法,最后让 t 引用指向该实例。
● 使用匿名内部类 实现 Runnable
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
});
t.start();
}
}
这样的写法本质上和上一个写法相同,此处只是创建了一个匿名内部类,实现了 Runnable 接口重写了 run 方法,同时创建了类的实例,把这个匿名的 Runnable 对象作为参数传递给了 Thread 的构造方法。
● 使用 Lambda 表达式
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello world");
});
// Thread t = new Thread(() -> System.out.println("hello world")); 等价上面
t.start();
}
}
此处是通过 Lambda 表达式来描述任务,直接把 Lambda 传给 Thread 构造方法,这里跟上种方法没有啥区别,只是语法的不同而已,因为 Runnable 这个接口就是一个函数式接口,才能使用这种语法,具体内容见 Lambda 章节。
上述介绍的几种写法,离不开 Thread 类,只不过是使用了不同的方法来描述 Thread 里的任务是啥,只是语法规则的不同,本质上都是一样的方法,这些方法创建出来的线程都是一样的,随着后面学习的深入,会见识到其他创建线程的方法但大体都是大同小异。
方法 |
说明 |
Thread() |
创建线程对象 |
Thread(Runnable target) |
使用 Runnable 对象创建线程对象 |
Thread(String name) |
创建线程对象,并命名 |
Thread(Runnable target, String name) |
使用 Runnable 对象创建线程对象,并命名 |
如果使用中直接 Thread t = new Thread(); t.start(); 这样的话相当于执行了一个空的 run 方法:
这里是 Thread 源码中的 run,此处的 target 就是一个 Runnable 类型的,所以要想创建的线程能正常的执行 run方法,要不继承 Thread 类重写 run,要不实现 Runnable 接口重写 run。
上述介绍的构造方法中,最后一个方法,是可以给线程起个名字,取名是为了方便调试,线程默认的名字叫做 Thread-0,Thread-1....
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hell world");
try {
Thread.sleep(10_0000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "myThread");
t.start();
}
此处加上 Thread.slepp(),让创建的线程进行休眠,为了是让我们通过 jconsole 工具更好的观察:
这里通过查看,确实发现给我们创建的线程取名为 myThread
提问:这里main线程为什么没了呢?
注意看上述代码,main 线程执行完 t.start() 之后,后面就没有任何需要执行的代码了,对于主线程来说,main 方法执行完了也就被销毁了,而且每个线程是一个独立的执行流,main线程的销毁,不影响 myThread 线程的继续执行!
属性 |
对应获取方法 |
ID |
getId() |
名称 |
getName() |
状态 |
getState() |
优先级 |
getPriority() |
是否后台线程 |
isDaemon() |
是否存活 |
isAlive() |
是否被中断 |
isInterrupted() |
● ID 是线程的唯一标识,不同的线程 ID 都不同
● 名称 是线程的名字,创建线程对象通过构造方法指定的名称,如果没指定就是默认的名字
● 状态 是线程所处的状态,有很多种,具体我们后续讲解
● 优先级 理论上优先级越高的线程越容易被调度到
● 是否是后台线程(是否是守护线程) 后面会讲解
● 是否被中断,可以通过一些手段中断线程,我们后续讲解
上述的方法获取线程对应的属性,大家可以下来自行尝试一下,这里就不做过多演示了!
这里守护线程就是后台线程,为什么叫做守护呢?这个是历史遗留翻译的问题,守护这个词语从字面意思确实不好理解,这里更习惯把守护线程叫作后台线程。
后台线程(守护线程),不会阻止进程的结束,即使后台线程的工作没有做完,进程也是可以结束的!
前台线程(非守护线程),会阻止进程的结束,如果前台线程的工作没有做完,进程是不能结束的!
注意:我们默认创建的线程都是前台线程!包括 main 方法也是一个前台线程(非守护线程)!
像我们上面通过 jconsole 工具看到的线程,除了自己创建的线程和 main 线程,剩下的都是 JVM 自带的线程,而 JVM 自带的线程都是后台线程(守护线程)
这里也可以使用 setDaemon() 这个方法来将创建的线程设置成后台线程(守护线程):
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello world");
});
t.setDaemon(true); // 将创建的线程对象对应的线程设置为后台线程
t.start();
}
这里将线程 t 设置成守护线程后,此时进程的结束与 t 就无关了!
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
System.out.println("hello");
}
});
t.setDaemon(true); // 将创建的线程对象对应的线程设置为后台线程
t.start();
System.out.println("main 方法执行结束!");
}
这段代码,已经将 t 线程设置成后台线程了,此时这个 t 线程要执行的任务是一个死循环,但是真的会一直执行吗?
通过打印不难发现,t 线程并不会永无止境的循环下去,因为将线程设置成守护线程后,我们启动的进程是否结束,就与 t 线程没有关系了,而进程的结束,进程里面对应的线程也会直接结束!
如果没有将 t 设置成守护线程呢?此时就会永无止境的打印 hello !大家可以自行下去测试!
上述介绍 Thread 类常见属性的时候,有一个属性是通过调用 isAlive 方法判断线程是否存活,那么线程存活到底是什么意思呢?
简单来说,在线程执行 run 方法的时候,就是存活的,执行 run 方法之前,或者执行完 run 方法之后,线程就不是存活的了!
那么这里我们就要弄清楚,线程是什么时候去执行 run 方法的?
其实在之前就讲到过,只有当线程对象,调用 start 方法后,才会真正的创建一个线程,然后线程去执行对应的 run 方法!
至于线程是否存活,那么就从三个点进行分析,start 之前,是肯定没有存活的,start 之后线程就会执行 run 方法,所以此时线程肯定是存活的,run 方法结束后,线程肯定是没有存活的!
下面就通过一段代码来验证下上述的结论:
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 100_0000; i++) {
}
});
System.out.println("start之前: " + t.isAlive());
t.start();
System.out.println("start之后: " + t.isAlive());
Thread.sleep(100);
System.out.println("run 方法执行完毕后 : " + t.isAlive());
}
上述代码就是让 t 线程干一件事,执行一百万次空循环,启动线程之后,令 main 线程等待 100 毫秒,此处的 100 毫秒足够执行完 run 方法中的内容了!
通过打印结果能发现,只有在执行 run 方法的时候,isAlive() 结果才是 true。
此处需要注意,线程把 run 方法执行完了,此时线程销毁,对应的 PCB 随之释放,但是 t 这个对象还不一定被释放,此时 isAlive() 也是 false,所以线程存在与否,与线程对象无关!
中断的意思是,不是让线程立即就结束,而是通知线程应该要结束了,是否真的结束还取决于线程这里代码的具体写法,这里我们简单来举一个例子:
public class ThreadDemo01 {
public static Boolean flag = false; // false表示不终止
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!flag) {
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
flag = true;
}
}
上述代码,执行完 t.start() 之后,main 线程休眠 30 秒后,将 flag 变量修改为 ture,表示告诉线程 t 应该要终止了,线程 t 下次循环发现 !flag 为假,就会退出这个循环!
上述这种自定义的方式,有一个缺点,如果上述代码线程中的 sleep 休眠时间太久,就可能不能及时感知到外面的 flag 已经发生改变了!
这里只是通过修改 flag 的方式,告诉线程,应该要结束了,但是这个线程会立马结束吗?其实还是取决于这个线程内部执行的代码,比如上述 t 线程执行的代码中,在 while 循环外再加上其他代码,此时也就不会立马就结束了!
可以自定义标志位的同时,也可也使用当前线程自带的标志位:
Thread.currentThread().isInterrupted();
前面的是 Thread 类的静态方法,获取线程对象的引用,在哪个线程中调用的,就获取对应的线程的实例,后面的 isInterrupted 则相当于是获取标志位的值,如果为 true 则表示线程该终止了,如果为 false 则表示不用终止,线程继续执行!
同时可以通过 interrupt() 这个方法,就可以通知对应线程该终止了!
下面就把上述的代码改成自带的标志位的模式:
public class ThreadDemo01 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt(); //通知线程该终止了
}
}
此时发现了一个奇怪现象,调用 interrupt 方法后,居然触发了 t 线程的 sleep 方法的异常?而且 t 线程并没有终止,这是怎么一回事呢?要想了解清楚,首先要弄清楚 interrupt 方法背后做了哪些事情!
interrupt 会做两件事:
把线程内部的标志位给设置成 true,告诉线程该终止了!
如果线程在 sleep,则会触发 sleep 的异常,把 sleep 提前唤醒!
但是 sleep 在被唤醒的时候,还会把标志位设置成 false!
扩展:像 wait,join 等类似造成线程 "阻塞挂起" 的方法,都有类似清除标志位的设定。
这样一来,就 interrupt 就白忙活了,如果没有没有 sleep,则是会正常终止上述线程的。
那么这样有什么好处呢?
就举个简单的例子,假设张三在打游戏,张三的女朋友让张三放下游戏陪她去逛街,那么张三就有三种选择:
立刻放下游戏,陪女朋友
忽略女朋友,不管她,当作没听到
等过一会游戏打完,再去陪女朋友
此时我们就可以修改上述的代码了:
立刻放下手机陪女朋友版本:
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("打游戏!!!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break; //马上放下手机陪女朋友
}
}
});
忽略女朋友版本:
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("打游戏!!!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("接着打!");
}
}
});
过一会再陪女朋友版本:
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("打游戏!!!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 此处可以写任意代码
System.out.println("等游戏打完!");
try {
Thread.sleep(5000);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
}
}
});
为什么 interrupt 不设定成立刻终止线程呢?而是让线程自己做选择呢?
因为 CPU 是随机调度线程的,所以当 interrupt 方法执行后,并不确定对应线程执行到哪里了,如果对应线程活还没干完,直接啪一下终止了,这样是很危险的行为!把是否真的终止线程的选择权交给程序猿,这才是一个很好的选择!
同时 Thread 类中还有一个 Thread.interrupted() 方法,手动清除标志位,这个了解即可。
下期预告:【多线程】认识线程的状态