1. Java中如何进行多线程编程?
线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户层提供了一些 API 供用户使用(如 Linux 中的 pthread 库)。
所以本身关于线程的操作,是依赖操作系统提供的的 API,而 Java 的 JVM 已经把很多操作系统提供的功能封装好了,我们就不需要学习系统原生的 API,只需要学习 Java 提供的 API 就好了。
在 Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进一步的抽象和封装!
可以认为,Java 操作多线程最核心的类就是 Thread 类!
2. 简单使用多线程
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 方法执行完结束了,这个新的线程也会随之销毁。
3. start 和 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 方法创建一个线程罢了!
4. jconsole 工具
我们也可以通过 jdk 自带的工具 jconsole 查看当前的 java 进程中的所有线程(bin 目录下):
因为进程和线程之间是包含关系,当要查看线程的时候,需要先连接上指定的进程,才能看指定进程中所拥有的线程。
此处可以看到一个是 main 线程,也就是主线程,还有一个是我们创建的线程这个默认起了个名字 Thread-0,除了这两个线程之外,其他的线程都是 JVM 自带的,这里我们不用过多关心,后续还会使用这个工具进行查看线程的阻塞状态等
5. Java 中创建线程的写法
● 继承 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 里的任务是啥,只是语法规则的不同,本质上都是一样的方法,这些方法创建出来的线程都是一样的,随着后面学习的深入,会见识到其他创建线程的方法但大体都是大同小异。
6. Thread 类方法介绍
6.1 构造方法
方法 | 说明 |
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 线程的继续执行!
6.2 Thread 类常见属性
属性 | 对应获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
● ID 是线程的唯一标识,不同的线程 ID 都不同
● 名称 是线程的名字,创建线程对象通过构造方法指定的名称,如果没指定就是默认的名字
● 状态 是线程所处的状态,有很多种,具体我们后续讲解
● 优先级 理论上优先级越高的线程越容易被调度到
● 是否是后台线程(是否是守护线程) 后面会讲解
● 是否被中断,可以通过一些手段中断线程,我们后续讲解
上述的方法获取线程对应的属性,大家可以下来自行尝试一下,这里就不做过多演示了!
6.3 什么是守护线程?
这里守护线程就是后台线程,为什么叫做守护呢?这个是历史遗留翻译的问题,守护这个词语从字面意思确实不好理解,这里更习惯把守护线程叫作后台线程。
后台线程(守护线程),不会阻止进程的结束,即使后台线程的工作没有做完,进程也是可以结束的!
前台线程(非守护线程),会阻止进程的结束,如果前台线程的工作没有做完,进程是不能结束的!
注意:我们默认创建的线程都是前台线程!包括 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 !大家可以自行下去测试!
6.5 什么情况线程才是存活的?
上述介绍 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,所以线程存在与否,与线程对象无关!
6.6 什么是线程中断?
中断的意思是,不是让线程立即就结束,而是通知线程应该要结束了,是否真的结束还取决于线程这里代码的具体写法,这里我们简单来举一个例子:
6.6.1 自定义一个标志位
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 循环外再加上其他代码,此时也就不会立马就结束了!
6.6.2 Thread 自带标志位
可以自定义标志位的同时,也可也使用当前线程自带的标志位:
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() 方法,手动清除标志位,这个了解即可。
下期预告:【多线程】认识线程的状态