多线程基本概念、生命周期、中断机制等小结

文章目录

      • 进程和线程
      • 多线程
        • 创建线程
        • 线程的执行顺序
        • 线程的生命周期
        • 中断线程
        • 守护线程
      • 小结

进程和线程

在计算机中,程序的一次执行或者执行一个任务称为一个进程。好比浏览器就是一个进程,视频播放器是另一个进程

某些进程内部还需要同时执行多个子任务。例如,在看视频一遍看着视频一遍发弹幕,查询分集介绍,我们把子任务称为线程

进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程

操作系统调度的最小任务单位其实不是进程,而是线程。常用的Windows、Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间

因为同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务(多进程)的方法,有以下几种:

  • 多线程模式(一个进程有多个线程)
  • 多进程+多线程模式

具体多任务的实现应该如何选择?
结合进程和线程的特点。和多线程比,多进程缺点在于:

  • 创建进程比创建线程开销大
  • 进程间通信很慢,而线程间通信就是读写同一个变量速度快,进程间的隔离也导致他们想共享数据是很麻烦的
    多进程优点在于:
  • 多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃

多线程

Java 语言内置了多线程支持:一个 Java 程序实际上是一个 JVM 进程,JVM 进程用一个主线程来执行 main() 方法,在 main() 方法内部,又可以启动多个线程。此外,JVM 还负责垃圾回收的其他工作线程。

对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。

和单线程相比:

  • 多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。
  • 多线程的优点:相比单线程只能干一件事,无法并行,直接导致用户体验不好。CPU 快速运算能力、多核这些都被浪费了,多线程却可以利用好

创建线程

前面说了,当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。

要创建一个新线程往往需要两步

  • 实例化一个Thread实例
  • 然后调用它的start()方法

方式一:从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()方法结束时,主线程也结束了
执行顺序:

  1. main 线程肯定是先打印main start,再打印main end
  2. t 线程肯定是先打印thread run,再打印thread end

从 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线程的状态有以下几种:

  • 新建(NEW):线程被创建出来,但尚未启动
  • 运行(RUNNING):运行中的线程,正在执行run()方法的Java代码
  • 阻塞(BLOCKED):运行中的线程,因为某些操作被阻塞而挂起,正在等待监视器锁,比如等待执行 synchronized 代码块或者使用 synchronized 标记的方法
  • 等待(WAITING):运行中的线程,因为某些操作在等待中
  • 计时等待(TIME_WAITING):运行中的线程,因为执行sleep()方法正在计时等待
  • 死亡(TERMINATED):线程已终止,因为run()方法执行完毕
    多线程基本概念、生命周期、中断机制等小结_第1张图片

中断线程

如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行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虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!
多线程基本概念、生命周期、中断机制等小结_第2张图片
这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量a = true,线程1执行a = false时,它在此刻仅仅是把变量a的副本变成了false,主内存的变量a还是true,在JVM把修改后的a回写到主内存之前,其他线程读取到的a的值仍然是true,这就造成了多线程之间共享的变量不一致。

因此,volatile关键字的目的是告诉虚拟机:

  • 每次访问变量时,总是获取主内存的最新值
  • 每次修改变量后,立刻回写到主内存
    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();

注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失

小结

  • Java用Thread对象表示一个线程,通过调用start()启动一个新线程;一个线程对象只能调用一次start()方法;线程的执行代码写在run()方法中
  • 线程调度由操作系统决定,程序本身无法决定调度顺序
  • 对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException
  • 目标线程检测到isInterrupted()为true或者捕获了InterruptedException都应该立刻结束自身线程
  • 通过标志位判断需要正确使用volatile关键字,volatile关键字解决了共享变量在线程间的可见性问题

你可能感兴趣的:(Web)