【后端之旅】多线程 Thread 篇

120 阅读12分钟

多线程是 Java 最基本的并发模型。而后面的 IO、网络均依赖该模型。毕竟,多线程是 Java 实现多任务的基础。

线程创建

  1. 继承 Thread

    /**
     * Thread 实体类
     */
    public class FirstThread extends Thread {
        @Override
        public void run() {
            System.out.println("Hello, Thread 01!");
        }
    }
    
    /**
     * 入口
     */
    public class Application {
        public static void main(String[] args) {
            FirstThread t = new FirstThread();
            t.start();
        }
    }
    

    优点:可使用 this 访问当前线程

    缺点:在单继承的 Java 中,继承了 Thread 类后便不能继承其他类

  2. 实现 Runnable 接口

    /**
     * Runnable 实体类
     */
    public class SecondRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("Hello, Runnable 02!");
        }
    }
    
    /**
     * 入口
     */
    public class Application {
        public static void main(String[] args) {
            Thread t = new Thread(new SecondRunnable());
            t.run();
        }
    }
    

    优点:可继承其他类

    缺点:只能使用 Thread.currentThread() 方法来访问当前线程

  3. 实现 Callable 接口

    /**
     * Callable 实体类
     */
    public class ThirdCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("Hello, Callable 03!");
    
            return "Callable result";
        }
    }
    
    /**
     * 入口
     */
    public class Application {
        public static void main(String[] args) {
            ThirdCallable task = new ThirdCallable();
            FutureTask<String> futureTask = new FutureTask<>(task);
            Thread t = new Thread(futureTask);
            t.start();
    
            try {
                String result = futureTask.get();
                System.out.println("The result is: " + result);
            } catch (ExecutionException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    优点:可继承其他类;可直接获得线程的运行返回值

    缺点:只能使用 Thread.currentThread() 方法来访问当前线程;须与 FutureTask 类结合使用,创建过程较为复杂

  4. 引入 Executors 线程池

    /**
     * 任务实体类
     */
    public class Task implements Runnable {
        private int id;
        public Task(int id) {
            this.id = id;
        }
    
        @Override
        public void run() {
            System.out.println("Hello, Task " + id + "!");
        }
    }
    
    /**
     * 入口
     */
    public class Application {
        public static void main(String[] args) {
            // 创建固定十个大小的线程池
            ExecutorService executor = Executors.newFixedThreadPool(10);
            for (int i = 0; i < 100; i++) {
                // 将 Runnable 实例提交到线程池执行
                executor.submit(new Task(i));
            }
            // 彻底关闭线程池
            executor.shutdown();
        }
    }
    

    优点:拥有 Runnable 的全部优点;线程的创建细节交由线程池实现,实现线程的重复利用,大大提高了程序的性能;线程池可预先创建线程,从而可以快速响应并发请求;线程池控制了线程的最大数量,从而避免线程过多创建导致系统崩溃的情况

    缺点:拥有 Runnable 的全部缺点;增加了程序的复杂度,也增加了排错的难度

线程启动与停止

线程启动

通过上一小节的内容,我们知道通过 Thread 创建的线程,必须使用 start() 实例方法来启动线程;而通过 Executors 创建的线程池,则只需要把 Runnable 实例提交给 ExecutorService 实例即可,无需额外调用启动方法。

线程停止

首先应该说明:run() 方法执行完毕后,线程会自然地停止。只有在该方法内部出现诸如 while (true) { ... } 这样的代码时,线程才会一直执行下去。

  • 返回停止法

    线程实例在外部调用 interrupt() 方法为线程打上中断状态后,在 run() 方法内部可以通过 isInterrupted() 判断当前线程的中断状态,如果为 true 则执行 return,也可达到中断线程的效果。

    /**
     * Thread 实体类
     */
    public class FinalThread extends Thread {
        @Override
        public void run() {
            System.out.println("Start!");
            while (!this.isInterrupted()) {
                System.out.println("Still running...");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    System.out.println("End!");
                    return;         // 或者 break; 也可以
                }
            }
        }
    }
    
    /**
     * 入口
     */
    public class Application {
        public static void main(String[] args) throws InterruptedException {
            FinalThread t = new FinalThread();
            t.start();
            Thread.sleep(1000);
            t.interrupt();
        }
    }
    
  • 标记停止法

    线程实体类定义一个布尔属性 running (使用 volatile 修饰),用来标记当前线程是否退出。而 run() 方法内部的业务代码放置 于 while (running) { ... } 代码块内。当外部设置了 runningfalse 后,线程不再执行下一轮循环,从而达到停止线程的效果。

    /**
     * Thread 实体类
     */
    public class FinalThread extends Thread {
        public volatile boolean running = true;
    
        @Override
        public void run() {
            System.out.println("Start!");
            while (running) {
                System.out.println("Still running...");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    break;
                }
            }
            System.out.println("End!");
        }
    }
    
    /**
     * 入口
     */
    public class Application {
        public static void main(String[] args) throws InterruptedException {
            FinalThread t = new FinalThread();
            t.start();
            Thread.sleep(1000);
            t.running = false;
        }
    }
    
  • 暴力停止法

    直接调用 stop() 实例方法终止后续代码的执行,即使后续代码是 finally 块。此方法已过期,不再推荐使用。

    /**
     * Thread 实体类
     */
    public class FinalThread extends Thread {
        @Override
        public void run() {
            System.out.println("Start!");
            while (true) {
                System.out.println("Still running...");
            }
        }
    }
    
    /**
     * 入口
     */
    public class Application {
        public static void main(String[] args) throws InterruptedException {
            FinalThread t = new FinalThread();
            t.start();
            Thread.sleep(50);
            t.stop();
            t.join(); // 等待线程 t 结束后再执行后续代码
            System.out.println("End!");
        }
    }
    

线程状态

状态就是生命周期中某一阶段的特征。下列状态图的 t 表示线程实例,并且双向的 t 都是同一个引用。

  • NEW - 线程已创建,但未调用 start() 方法

    graph LR
       A[NEW] -- "&nbsp;t.start()&nbsp;" --> B[RUNNABLE]
       style A fill:#A9DEF9
       style B fill:#D3F8DA
    
  • RUNNABLE - 线程已调用 start() 方法,但操作系统未调度该线程

    请注意:

    1. RUNNABLE 其实还可以细分为 READYRUNNING,但由于这两个状态完全是由操作系统调度决定,所以这里不做更多介绍。
  • BLOCKED - 因为某些操作或外部原因而阻塞,被操作系统挂起

    graph LR
       B[RUNNABLE] -- "&nbsp;执行 synchronized 方法或块等待锁&nbsp;" --> C[BLOCKED]
       C[BLOCKED] -- "&nbsp;获得锁&nbsp;" --> B[RUNNABLE]
       style B fill:#D3F8DA
       style C fill:#EDE7B1
    
  • WAITING - 等待中,需要另一线程执行特定的方法来唤醒

    请注意:

    1. 调用 obj.wait()t.join()LockSupport.park(),是调用该方法的线程进入 WAITING 状态,而非 t 进入等待状态!
    2. obj.wait() 的具体作用是释放 obj 的同步锁,当前线程进入等待状态;而 t.join() 则是当前线程进入等待状态,直到 t 线程执行完毕。
    3. obj.notify() 通常是从等待池中把最早等待 obj 锁的那个线程移至锁竞争池。而 obj.notifyAll() 则是把等待池中全部的线程都移至锁竞争池。如果等待池没有等待 obj 锁的线程,则调用此方法没有任何效果。
    4. 如果 t 本身已结束,则调用 t.join() 会立即返回,并继续执行后续代码。
    5. LockSupport.park() 只会令当前进程进入阻塞状态,但不会释放持有的锁。
    graph LR
       B[RUNNABLE] -- "&nbsp;obj.wait()&nbsp;" --> D[WAITING]
       B[RUNNABLE] -- "&nbsp;t.join()&nbsp;" --> D[WAITING]
       B[RUNNABLE] -- "&nbsp;LockSupport.park()&nbsp;" --> D[WAITING]
       D[WAITING] -- "&nbsp;LockSupport.unpark(thread)&nbsp;" --> B[RUNNABLE]
       D[WAITING] -- "&nbsp;t 内部的 run() 执行完毕&nbsp;" --> B[RUNNABLE]
       D[WAITING] -- "&nbsp;obj.notify() 或 obj.notifyAll()&nbsp;" --> B[RUNNABLE]
       style B fill:#D3F8DA
       style D fill:#E3D4E7
    
  • TIMED_WAITING - 等待中,但超过给定时间后会自动返回就绪状态

    请注意:

    1. LockSupport.parkNanos(long) 的参数是指阻塞等待的最大时长,单位为毫秒;LockSupport.parkUntil(long) 的参数是指阻塞等待的最久时间点,即时间戳。
    graph LR
       B[RUNNABLE] -- "&nbsp;obj.wait(long)&nbsp;" --> E[TIMED_WAITING]
       B[RUNNABLE] -- "&nbsp;t.join(long)&nbsp;" --> E[TIMED_WAITING]
       B[RUNNABLE] -- "&nbsp;LockSupport.parkNanos(long) 或 LockSupport.parkUntil(long)&nbsp;" --> E[TIMED_WAITING]
       E[TIMED_WAITING] -- "&nbsp;LockSupport.unpark(thread)&nbsp;" --> B[RUNNABLE]
       E[TIMED_WAITING] -- "&nbsp;t 内部的 run() 执行完毕或超时&nbsp;" --> B[RUNNABLE]
       E[TIMED_WAITING] -- "&nbsp;obj.notify() 或 obj.notifyAll()&nbsp;" --> B[RUNNABLE]
       style B fill:#D3F8DA
       style E fill:#EAD0EE
    
  • TERMINATED - 线程的 run() 方法已执行结束并返回

    graph LR
       B[RUNNABLE] -- "&nbsp;run() 结束&nbsp;" --> F[TERMINATED]
       style B fill:#D3F8DA
       style F fill:#F9CECD
    

守护线程

用途:某些线程在启动后需要无限循环,但当非守护线程全部结束后,JVM 需要无感知退出。

示例

/**
 * Thread 实体类
 */
public class ForeverThread extends Thread {
    @Override
    public void run() {
        System.out.println("Start!");
        while (true) {
            System.out.println("Still running...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

/**
 * 入口
 */
public class Application {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new ForeverThread();
        t.setDaemon(true);    // 只需要在 start() 方法调用前执行该方法即可
        t.start();
        // 执行到这里后,t 还是结束了
    }
}

线程安全

如果一个类被设计为允许多线程正确访问,我们就称该类是线程安全的。而一个非线程安全的类,则由于 Java 的线程可以直接修改线程外变量的值,在多个线程同时访问时容易出现竞态现象。要消除该现象,实现多线程编程时需要做到以下三点:

  • 原子性 - 提供互斥访问,即同一时刻只能由一个线程对数据进行一组操作,其他线程不能打扰
    • atomic 系列类
      /**
       * 实体类
       */
      public class AtomicCounter {
          // JVM 使用 CAS 机制确保无锁同步
          private AtomicInteger n = new AtomicInteger(0);
      
          public void increment() {
              // 自加并返回,相当于 ++n
              n.incrementAndGet();
          }
      
          public void decrement() {
              // 自减并返回,相当于 --n
              n.decrementAndGet();
          }
      
          public int getValue() {
              return n.get();
          }
      }
      
      /**
       * 入口
       */
      public class Application {
          public static void main(String[] args) throws InterruptedException {
              AtomicCounter counter = new AtomicCounter();
              Thread t1 = new Thread(() -> {
                  for (int i = 0; i < 100; i++) {
                      counter.increment();
                  }
              });
              Thread t2 = new Thread(() -> {
                  for (int i = 0; i < 100; i++) {
                      counter.increment();
                  }
              });
      
              t1.start();
              t2.start();
      
              t1.join();
              t2.join();
      
              // 将输出:Result = 200
              System.out.println("Result = " + counter.getValue());
          }
      }
      
    • synchronized 关键字
      /**
       * 实体类
       */
      public class SynchronizedCounter {
          private int n = 0;
      
          // 被加锁的,永远是对象。当前被加锁的,就是实例本身
          public synchronized void increment() {
              ++n;
          }
      
          public synchronized void decrement() {
              --n;
          }
      
          public synchronized int getValue() {
              return n;
          }
      }
      
      /**
       * 入口
       */
      public class Application {
          public static void main(String[] args) throws InterruptedException {
              SynchronizedCounter counter = new SynchronizedCounter();
              Thread t1 = new Thread(() -> {
                  for (int i = 0; i < 100; i++) {
                      counter.increment();
                  }
              });
              Thread t2 = new Thread(() -> {
                  for (int i = 0; i < 100; i++) {
                      counter.increment();
                  }
              });
      
              t1.start();
              t2.start();
      
              t1.join();
              t2.join();
      
              // 将输出:Result = 200
              System.out.println("Result = " + counter.getValue());
          }
      }
      
  • 可见性 - 一个线程修改了共享变量,其他线程能立即感知该变量已被修改
    • volatile 关键字
      /**
       * 实体类
       */
      public class VolatileEntity {
          // volatile 关键字保证了访问 flag 的有序性(阻止了指令的重排)
          private volatile boolean flag = false;
      
          public boolean getFlag() {
              return flag;
          }
      
          public void setFlag(boolean flag) {
              this.flag = flag;
          }
      }
      
      /**
       * 入口
       */
      public class Application {
          public static void main(String[] args) {
              VolatileEntity entity = new VolatileEntity();
      
              Thread t1 = new Thread(() -> {
                  System.out.println("t1 is starting...");
                  while (!entity.getFlag());
                  System.out.println("t1 is ended.");
              });
              Thread t2 = new Thread(() -> {
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      Thread.currentThread().interrupt();
                  }
                  entity.setFlag(true);
              });
      
              t1.start();
              t2.start();
          }
      }
      
    • synchronized 关键字
      /**
       * 实体类
       */
      public class SynchronizedEntity {
          private boolean flag = false;
      
          // synchronized 确保了线程间的可见性(当某线程进入本方法,它会读取共享变量的最新值)
          public synchronized boolean getFlag() {
              return flag;
          }
      
          // synchronized 确保了线程间的可见性(当某线程退出本方法,它会将共享变量的更新值写回主存)
          public synchronized void setFlag(boolean flag) {
              this.flag = flag;
          }
      }
      
      /**
       * 入口
       */
      public class Application {
          public static void main(String[] args) {
              SynchronizedEntity entity = new SynchronizedEntity();
      
              Thread t1 = new Thread(() -> {
                  System.out.println("t1 is starting...");
                  while (!entity.getFlag());
                  System.out.println("t1 is ended.");
              });
              Thread t2 = new Thread(() -> {
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      Thread.currentThread().interrupt();
                  }
                  entity.setFlag(true);
              });
      
              t1.start();
              t2.start();
          }
      }
      
  • 有序性 - 确保重排序后的执行结果与重排序前一致

线程池

在前面的线程创建部分,我们已经知道线程池为多个线程提供了一种统一的管理方式。也知道了线程的主要好处包括:

  • 降低系统资源消耗
  • 控制并发数量以防止服务器过载
  • 便于统一管理和维护线程

线程池作为管理多个线程的载体,与线程一样,也有自身的生命周期,即不同的状态:

  • RUNNING —— 线程池创建后默认处于此状态,能接受新提交的任务,并且也能处理阻塞队列中的任务。
  • SHUTDOWN —— 调用 shutdown() 方法后进入此状态,不再接受新提交的任务,但可以处理存量任务。
  • STOP —— 调用 shutdownNow() 方法后变为此状态,不仅不接收新任务,还会尝试中断正在执行的任务,并且不会处理尚未开始执行的任务。
  • TIDYING —— 当所有的任务都已经终止并且 workerCount(活动工作线程数)为 0 时,线程池将转换到此状态,并触发 terminated() 钩子方法。
  • TERMINATED —— terminated() 方法执行完毕后,线程池最终进入此状态,表明线程池已经彻底关闭,无法再进行任何操作。

前面的代码中,我们使用了定长线程池。事实上,线程池的种类非常多,下面将介绍最常用的 5 种:

  • 定长线程池

    创建指定数量的线程,当所有线程全部处于活动状态时,再提交的任务会在队列中等待,直到有线程可用。特点是被创建线程会一直存在,直到线程池调用了 shutdown()

    ExecutorService es = Executors.newFixedThreadPool(10);
    
  • 单个线程池

    线程池只会用唯一的工作线程来执行任务。会确保在任何情况下都不会有超过一个任务处于活动状态。主要特点该线程池无法再通过其他方法修改线程的数量,这是与 newFixedThreadPool(1) 不同的地方。

    ExecutorService es = Executors.newSingleThreadExecutor();
    
  • 无限线程池

    扩容大小为 (int) 的最大值,即可理解为无限大。而具体大小则根据传递进去的线程数量决定。当然,调用 execute() 时会优先重用以前构造的线程。而被构造或释放的线程,默认 60 秒不适用即被终止和移除。

    ExecutorService es = Executors.newCachedThreadPool();
    
  • 计划线程池

    创建指定数量的线程,任务的运行则可以指定在延迟时间后执行,或者定期执行。而任务的添加,与其他线程池使用 submit() 方法不一样的是,计划线程池使用 schedule() 方法(这样才可以设置时间参数)。空闲的线程会进行保留,直到线程池调用了 shutdown()

    ScheduledExecutorService es = Executors.newScheduledThreadPool(10);
    
  • 并行线程池

    获取当前可用的线程数量进行创建作为并行级别。并行级别决定了同一时刻最多有几个线程在执行,不传参则默认为 CPU 核心数目。主要用途是处理可并行化且计算密集型的任务。当某个线程执行完自己队列中的任务后,它会尝试从其他线程的队列中“窃取”任务来执行,从而实现了负载均衡。

    // Runtime.getRuntime().availableProcessors() 可查看本机 CPU 核心数目
    ExecutorService es = Executors.newWorkStealingPool();
    

ThreadLocal

如果把线程看作一个语句块,那么,在这个块中声明局部变量就是最基本的需求。Java 使用 ThreadLocal 来将一个类的属性声明为线程的局部变量。

可把 ThreadLocal 看成一个全局的 Map<Thread, Object>,即每个线程内部获取 ThreadLocal 变量时,永远是以 Thread 自身作为 key。而各个线程的 ThreadLocal 关联的 Object 实例互不干扰。

/**
 * 抽象控制类
 */
public abstract class AbstractController implements AutoCloseable {
    // request 和 response 只在所在线程可访问
    // 并且被所有当前实例的方法及继承实例的方法所共享
    protected ThreadLocal<HttpServletRequest> request = new ThreadLocal<>();
    protected ThreadLocal<HttpServletResponse> response = new ThreadLocal<>();

    public void setRequestAndResponse(HttpServletRequest request, HttpServletResponse response) {
        this.request.set(request);
        this.response.set(response);
    }

    @Override
    public void close() throws Exception {
        request.remove();
        response.remove();
    }
}

/**
 * 业务控制类
 */
public class HomeController extends AbstractController {
    public void getHomePage() throws IOException {
        String name = request.get().getParameter("name");
        String pass = request.get().getParameter("pass");
        PrintWriter out = response.get().getWriter();

        String result = "You cannot access home page!";

        if (name.equals("ego") && pass.equals("ego")) {
            result = "This is the home page!";
        }

        out.println(result);
        out.close();
        
        response.get().flushBuffer();
    }
}

/**
 * 模拟访问
 */
public class Application {
    public static void main(String[] args) {
        try (HomeController hc = new HomeController()) {
            // TODO
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}

线程销毁后,ThreadLocal 关联的实例会自动被内存回收。然而,由于我们使用线程通常都是通过创建线程池的方式。而线程在关闭后并没有释放,而是放回到线程池中了。因此,我们必须在线程关闭时清除 ThreadLocal 关联的实例,以避免数据被下一个任务读取到:

/**
 * 任务实体类
 */
public class SimpleTask implements AutoCloseable {
    static final ThreadLocal<String> ctx = new ThreadLocal<>();

    public SimpleTask(String str) {
        ctx.set(str);
    }

    public static String currentCtx() {
        return ctx.get();
    }

    @Override
    public void close() throws Exception {
        ctx.remove();
    }
}

/**
 * 模拟请求入口
 */
public class Application {
    public static void main(String[] args) {
        MockHttpServletRequest mockRequest = new MockHttpServletRequest();
        MockHttpServletResponse mockResponse = new MockHttpServletResponse();

        mockRequest.addParameter("name", "ego");
        mockRequest.addParameter("pass", "ego");

        try (HomeController hc = new HomeController()) {
            hc.setRequestAndResponse(mockRequest, mockResponse);
            hc.getHomePage();

            System.out.println(mockResponse.getContentAsString());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}

小结

本文简单地介绍了一些关于 Java 多线程的基础知识,包括线程的完整生命周期及其附带的各种状态,也包括线程的各种注意事项,最后聊了一些关于线程池和 ThreadLocal 的简单知识。内容不多,希望能帮助到入门的你。后续有机会,会更深入地探讨线程在计算机中的本质。