在计算机中,程序的一次执行或者执行一个任务称为一个进程。好比浏览器就是一个进程,视频播放器是另一个进程
某些进程内部还需要同时执行多个子任务。例如,在看视频一遍看着视频一遍发弹幕,查询分集介绍,我们把子任务称为线程
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程
操作系统调度的最小任务单位其实不是进程,而是线程。常用的Windows、Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间
因为同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务(多进程)的方法,有以下几种:
具体多任务的实现应该如何选择?
结合进程和线程的特点。和多线程比,多进程缺点在于:
Java 语言内置了多线程支持:一个 Java 程序实际上是一个 JVM 进程,JVM 进程用一个主线程来执行 main() 方法,在 main() 方法内部,又可以启动多个线程。此外,JVM 还负责垃圾回收的其他工作线程。
对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。
和单线程相比:
前面说了,当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。
要创建一个新线程往往需要两步
方式一:从Thread派生一个自定义类,然后覆写run()方法
start()方法会在内部自动调用实例的run()方法
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start(); // 启动新线程
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("start new thread!");
}
}
方法二:创建Thread实例时,传入一个Runnable实例
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}
// 用Java8引入的lambda语法进一步简写为
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("start new thread!");
});
t.start(); // 启动新线程
}
}
使用线程执行的打印语句,和直接在main()方法执行有区别?
public class Main {
public static void main(String[] args) {
System.out.println("main start...");
Thread t = new Thread() {
public void run() {
System.out.println("thread run...");
System.out.println("thread end.");
}
};
t.start();
System.out.println("main end...");
}
}
执行过程:
main 线程执行的是 3、4、10、11。首先打印 “main start”,然后创建对象 t,接着调用 start() 方法启动线程。
当 start() 被调用时,JVM 就创建一个新线程,通过实例变量 t 表示这个新线程对象,并执行
接着,main线程继续执行打印 “main end” 语句,而t线程在main线程执行的同时会并发执行,打印 “thread run” 和 “thread end” 语句
当run()方法结束时,新线程就结束了。而main()方法结束时,主线程也结束了
执行顺序:
从 t 线程开始运行以后,两个线程就开始同时运行了,并且由操作系统调度,程序本身无法确定这两个线程的调度顺序。所以 “main end 和 thread run” 顺序无法确定
线程的优先级:
// 可以对线程设定优先级,设定优先级的方法是:
Thread.setPriority(int n) // 1~10, 默认值5
优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。
另外要注意 run() 和 start()
run() 方法只是封装了线程要执行的代码,直接用对象调用run()方法,相当于调用了一个普通的Java方法,当前线程并没有任何改变,也不会启动新线程
必须调用 Thread 实例的start()方法才能启动新线程,如果我们查看Thread类的源代码,会看到start()方法内部调用了一个 private native void start0() 方法,native修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的
在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行
好比从网络下载一个 1G 的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。
中断一个线程只需要在其他线程中对目标线程调用 interrupt() 方法,目标线程需要反复检测自身状态是否是 interrupted 状态,如果是,就立刻结束运行
中断线程方式一:
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1); // 暂停1毫秒
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}
class MyThread extends Thread {
public void run() {
int n = 0;
while (! isInterrupted()) {
n ++;
System.out.println(n + " hello!");
}
}
}
main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。而t线程的while循环会检测isInterrupted(),所以上述代码能正确响应interrupt()请求,使得自身立刻结束运行run()方法
如果线程处于等待状态,例如,t.join()会让main线程进入等待状态,此时,如果对main线程调用interrupt(),join()方法会立刻抛出InterruptedException,因此,目标线程只要捕获到join()方法抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行
中断线程方式二:
public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false; // 标志位置为false
}
}
class HelloThread extends Thread {
public volatile boolean running = true;
public void run() {
int n = 0;
while (running) {
n ++;
System.out.println(n + " hello!");
}
System.out.println("end!");
}
}
HelloThread的标志位boolean running是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值
为什么要对线程间共享的变量用关键字volatile声明?
这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!
这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量a = true,线程1执行a = false时,它在此刻仅仅是把变量a的副本变成了false,主内存的变量a还是true,在JVM把修改后的a回写到主内存之前,其他线程读取到的a的值仍然是true,这就造成了多线程之间共享的变量不一致。
因此,volatile关键字的目的是告诉虚拟机:
如果我们去掉volatile关键字,运行上述程序,发现效果和带volatile差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟
Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。如果有一个线程没有退出,JVM进程就不会退出。必须保证所有线程都能及时结束,这时就出现 守护线程
它是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。因此,JVM退出时,不必关心守护线程是否已结束
// 创建守护线程
// 在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程
Thread t = new MyThread();
t.setDaemon(true);
t.start();
注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失