进程VS线程

进程和线程是包含关系,多任务既可以由多进程实现,也可以由单进程多线程实现,还可以混合多进程+多线程。
和多线程相比,多进程缺点在于:

  • 创建进程开销大
  • 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量。

多进程的优点为:

  • 多进程的稳定性比多线程高,在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃

    Java中的多线程

    Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。
    因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。

    创建自定义线程

  1. 方法一:从Thread派生一个自定义类,覆写run()方法,run()方法的内容即自定义的内容。

    1. public class Main {
    2. public static void main(String[] args) {
    3. Thread t = new MyThread();
    4. t.start(); // 启动新线程
    5. }
    6. }
    7. class MyThread extends Thread {
    8. @Override
    9. public void run() {
    10. System.out.println("start new thread!");
    11. }
    12. }
  2. 方法二:创建Thread实例时,传入一个Runnable实例:

    1. public class Main {
    2. public static void main(String[] args) {
    3. Thread t = new Thread(new MyRunnable());
    4. t.start(); // 启动新线程
    5. }
    6. }
    7. class MyRunnable implements Runnable {
    8. @Override
    9. public void run() {
    10. System.out.println("start new thread!");
    11. }
    12. }

    注意:直接调用Thread实例的run()方法是无效的,其相当于调用了一个普通的Java方法,并没有启动新线程(相当于在Main()方法中调用了run()方法)

    线程的状态

  • New:新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。

当线程启动后,它可以在RunnableBlockedWaitingTimed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。
线程终止的原因有:

  • 线程正常终止:run()方法执行到return语句返回;
  • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
  • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

一个线程还可以等待另一个线程运行结束后再运行。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行。当main线程对线程对象t调用join()方法时,主线程将等待变量t表示的线程运行结束,即join就是指等待该线程结束,然后才继续往下执行自身线程。
此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。

请求中断线程

main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。若t线程中没有相关的检测interrupt()的方法,则该中断请求无效。
如果线程处于等待状态,例如,t.join()会让main线程进入等待状态,此时,如果对main线程调用interrupt()join()方法会立刻抛出InterruptedException,因此,目标线程只要捕获到join()方法抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。

线程共享变量

线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。
原理:在Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!
image.png
这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量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进程就不会退出。所以,必须保证所有线程能即使结束。但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程:

  1. class TimerThread extends Thread {
  2. @Override
  3. public void run() {
  4. while (true) {
  5. System.out.println(LocalTime.now());
  6. try {
  7. Thread.sleep(1000);
  8. } catch (InterruptedException e) {
  9. break;
  10. }
  11. }
  12. }
  13. }

通常此类进程由守护进程(Daemon Thread)负责结束
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出(最后一个拿钥匙关门走)。
设置守护线程的方法:setDaemon(true)

  1. Thread t = new MyThread();
  2. t.setDaemon(true);
  3. t.start();