线程、进程、程序基本概念

  • 程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中;简单说,程序就是静态的代码
  • 进程是程序的一次执行过程,是系统运行程序的基本单位。进程是动态的,系统运行一个程序即是一个进程从创建、运行到消亡的过程。简单说,一个进程就有一个执行中的运行程序
  • 线程是进程划分成的运行单位,一个进程在其执行过程中可以产生多个线程。与进程不同的是,同类的多个线程共享同一块内存空间和一组系统资源,故系统在产生一个线程,或是在各个线程之间切换工作时,负担要比进程小得多,所以线程也被称为轻量级进程

    线程具有哪些状态?

    Java 线程在运行生命周期中的指定时刻可能处于以下 6 种不同状态种的某一个状态
    多线程 - 图1
    线程在生命周期种并不是固定处于某一个状态而是虽则代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示:
    多线程 - 图2
    如上图所示的线程生命周期状态项目描述如下:
  1. 线程创建之后就如 NEW 新建状态
  2. 调用 start() 方法后线程开始运行,线程进入 RUNNABLE 运行中状态
  3. 若线程执行 wait() 方法,线程转为 WAITING 等待状态,进入等待状态的线程需要依靠其他线程的通知才能返回到运行状态
    1. TIME_WAITING 超时等待状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis) 方法或者 wait(long millis) 方法可以将线程置于 TIME_WAITING 状态。当超时时间到达后 Java 线程将返回为 RUNNABLE 状态
  4. 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入 BLOCKED 阻塞状态
  5. 线程在执行完 Runnablerun() 方法之后将进入 TERMINATED 终止状态

    操作系统会隐藏 JVM 中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态。Java 系统一般将这两个状态统称为 RUNNABLE 运行中状态

为什么通过 start() 方法执行 run() 方法,而不能直接调用 run() 方法?

当新声明一个线程后,线程会进入初始状态,在调用 start() 方法后,会启动一个线程并且线程进入就绪状态,当分配到时间片后就可以开发运行了。start() 方法会执行线程的相应准备工作,然后自动执行 run() 方法中的内容,这是真正的多线程工作。但是直接调用 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行他,所以就不是真正的多线程工作。

  1. public class MyThread extends Thread {
  2. @Override
  3. public void run() {
  4. log.debug("Thread Name :{}",Thread.currentThread().getName());
  5. }
  6. }
  7. public static void main(String[] args) {
  8. final MyThread myThread = new MyThread();
  9. myThread.start();
  10. Thread thread = new Thread(() -> {
  11. log.debug("Thread Name :{}", Thread.currentThread().getName());
  12. });
  13. thread.run();
  14. }
  15. // 通过打印输出的线程名可以看到是否两者明显区别
  16. 22:27:06.183 [Thread-0] DEBUG com.dev.threads.MyThread - Thread Name Thread-0
  17. 22:27:06.184 [main] DEBUG com.dev.Main - Thread Name main

sleep() 方法和 wait() 方法的区别?

  • 两者最主要的区别在于:sleep() 方法没有释放锁,而 wait() 方法释放了锁
  • 两者都可以暂停线程的执行
  • wait() 通常用于线程间交互/通信,sleep() 通常用于暂停执行
  • wait() 方法调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行结束后,线程会自动苏醒,或者使用 wait(long timeout) 超时后线程会自动苏醒

    守护线程

    Java 程序入口就是由 JVM 启动 main 线程,mian 线程又可以启动其他线程。当所有线程都结束运行时,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. }

    如果上述无限循环线程不结束,JVM 进程就无法结束。但是这类线程一般不会显示结束他们,但是当其他线程结束时,JVM 线程又必须要结束。针对这种情况,Java 定义了守护线程。
    守护线程 Daemon Thread 是指为其他线程服务的线程,在 JVM 中,所有非守护线程都执行结束后,无论有没有守护线程,JVM 都会自动退出。

    定义守护线程

    定义守护线程和定义普通线程一样,只是在调用 start() 方法前,调用 setDaemon(true) 将该线程标记为守护线程:

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

    线程池

    为什么要使用多线程?

    池化技术应用非常广泛,线程池,数据库连接池,HTTP 连接池都是池化思想的应用。池化技术的主要作用是为了减少每次获取资源的消耗,提高对资源的利用率。
    《Java 并发编程的艺术》提到使用线程池的好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗

  • 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

    使用 TheadPoolExecutor 声明线程池

    如何使用?

    《阿里巴巴 Java 开发手册》要求不使用 Executors 声明线程池。故一般通过 ThreadPoolExecuter 的构造函数来创建线程池,然后提交任务给线程池执行就可以了。

    1. /**
    2. * 用给定的初始参数创建一个新的ThreadPoolExecutor。
    3. */
    4. public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
    5. int maximumPoolSize,//线程池的最大线程数
    6. long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
    7. TimeUnit unit,//时间单位
    8. BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
    9. ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
    10. RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
    11. ) {
    12. if (corePoolSize < 0 ||
    13. maximumPoolSize <= 0 ||
    14. maximumPoolSize < corePoolSize ||
    15. keepAliveTime < 0)
    16. throw new IllegalArgumentException();
    17. if (workQueue == null || threadFactory == null || handler == null)
    18. throw new NullPointerException();
    19. this.corePoolSize = corePoolSize;
    20. this.maximumPoolSize = maximumPoolSize;
    21. this.workQueue = workQueue;
    22. this.keepAliveTime = unit.toNanos(keepAliveTime);
    23. this.threadFactory = threadFactory;
    24. this.handler = handler;
    25. }

    为什么手册要求不使用 Executors 声明线程池?

  • FixedThreadPoolSingleThreadExecuter:允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM

  • CachedThreadPoolScheduledThreadPool:允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致 OOM
  • 实际使用中需要根据机器的硬件性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等。
  • 实际使用中需要给线程池显式命名,方便排查定位问题。

    检测线程池运行状态

    ThreadPoolExecutor 提供了获取线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等。如下所示:
    image.png
    下面是一个简单的 Demo。printThreadPoolStatus() 会每隔一秒打印出线程池的线程数、活跃线程数、完成的任务数和队列中的任务数 ```java public static void printThreadPoolStatus(ThreadPoolExecutor executor) {

    1. final ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
    2. r -> {
    3. final Thread thread = new Thread(r);
    4. thread.setDaemon(true);
    5. thread.setName("thread-pool-status-monitor");
    6. return thread;
    7. });
    8. executorService.scheduleAtFixedRate(() -> {
    9. log.debug("=============");
    10. log.debug("ThreadPool Size: [{}]", executor.getPoolSize());
    11. log.debug("Number of Tasks : {}", executor.getActiveCount());
    12. log.debug("Number of Tasks in Queue: {}", executor.getQueue().size());
    13. }, 0, 1, TimeUnit.SECONDS);

    }

  1. <a name="QKOj5"></a>
  2. ### 建议不同类别的业务使用不同的线程池
  3. 一个真实的事故案例如下图:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/348884/1616507827122-bd94e171-cf4c-4ff6-b224-a6250b9dfa9c.png#align=left&display=inline&height=1992&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1992&originWidth=1414&size=408473&status=done&style=none&width=1414)<br />先上结论:上述代码可能会存在死锁的情况。试想一种极端情况:<br />假设线程池的核心线程数为 n,父任务的数量也为 n,父任务下有两个子任务,其中一个已经执行完成,另外一个被放在了任务队列中。由于父任务把线程池核心线程资源用完,所以子任务因为无法获取到线程资源无法正常执行,一直被阻塞在队列中。父任务等待子任务执行完成,而子任务等待父任务释放线程资源,这时就会造成“死锁”<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/348884/1616508109133-65070351-8b0e-4451-9c42-fca35130b194.png#align=left&display=inline&height=551&margin=%5Bobject%20Object%5D&name=image.png&originHeight=551&originWidth=631&size=41453&status=done&style=none&width=631)<br />解决方法也很简单,就是新增加一个用于执行子任务的线程池专门为其服务。
  4. <a name="zoT39"></a>
  5. ### 正确给线程池命名
  6. 在初始化线程时显式命名线程,有利于定位问题。
  7. <a name="wPFgS"></a>
  8. #### 使用 Guava 的 ThreadFactoryBuilder
  9. ```java
  10. ThreadFactory threadFactory = new ThreadFactoryBuilder()
  11. .setNameFormat(threadNamePrefix + "-%d")
  12. .setDaemon(true).build();
  13. ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
  14. TimeUnit.MINUTES, workQueue, threadFactory)

自定义实现 ThreadFactory

  1. import java.util.concurrent.Executors;
  2. import java.util.concurrent.ThreadFactory;
  3. import java.util.concurrent.atomic.AtomicInteger;
  4. /**
  5. * 线程工厂,它设置线程名称,有利于我们定位问题。
  6. */
  7. public final class NamingThreadFactory implements ThreadFactory {
  8. private final AtomicInteger threadNum = new AtomicInteger();
  9. private final ThreadFactory delegate;
  10. private final String name;
  11. /**
  12. * 创建一个带名字的线程池生产工厂
  13. */
  14. public NamingThreadFactory(ThreadFactory delegate, String name) {
  15. this.delegate = delegate;
  16. this.name = name; // TODO consider uniquifying this
  17. }
  18. @Override
  19. public Thread newThread(Runnable r) {
  20. Thread t = delegate.newThread(r);
  21. t.setName(name + " [#" + threadNum.incrementAndGet() + "]");
  22. return t;
  23. }
  24. }

正确配置线程池参数

线程池的大小设置的过大或者过小都可能出现问题,合适的才是最好。如果设置的太小,当同一时间有大量任务需要处理,可能会导致任务出现大面积阻塞排队执行,甚至可能因为大量任务堆积导致 OOM。但是如果设置的太大,大量线程可能会同时在争夺 CPU 资源,这样会引起频繁的“上下文切换”,从而增加线程的执行时间,影响整体的执行效率。

上下文切换: 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

有一个简单且适用面比较广的公式(N 为 CPU 核数):

  • CPU 密集型任务设置 N+1:这种任务主要消耗 CPU 资源,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其他原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。如大量数据需要排序、数学运算
  • I/O 密集型任务设置 2N:这种任务执行起来,大部分运行时间都是在处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 资源,这时就可以将 CPU 交出给其他线程使用。如大量网络读取、文件读取操作

    线程池与 CountDownLatch 组合使用

    CountDownLatch 是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。
    image.png ```java @Slf4j @AllArgsConstructor public class Worker implements Runnable { private final CountDownLatch countDownLatch;

    private final String name;

    @Override public void run() {

    1. log.debug("{} is running", name);
    2. try {
    3. TimeUnit.SECONDS.sleep(new Random().nextInt(10));
    4. } catch (InterruptedException e) {
    5. e.printStackTrace();
    6. }
    7. log.debug("{} is done", name);
    8. countDownLatch.countDown();

    } }

@Slf4j @AllArgsConstructor public class Boss implements Runnable { private final CountDownLatch countDownLatch;

  1. @Override
  2. public void run() {
  3. log.debug("wait all worker done");
  4. try {
  5. countDownLatch.await();
  6. } catch (InterruptedException e) {
  7. e.printStackTrace();
  8. }
  9. log.debug("all worker is done");
  10. }

}

public static void main(String[] args) { final int corePoolSize = 5; final int maxPoolSize = 100; final int queueCapacity = 100; final long keepTimeAlive = 1L; final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( corePoolSize, maxPoolSize, keepTimeAlive, TimeUnit.SECONDS, new ArrayBlockingQueue<>(queueCapacity), new ThreadPoolExecutor.CallerRunsPolicy() ); ThreadUtil.printThreadPoolStatus(threadPoolExecutor); AtomicInteger index = new AtomicInteger(); CountDownLatch countDownLatch = new CountDownLatch(500); for (int i = 0; i < 500; i++) { Worker worker = new Worker(countDownLatch, “zhang” + index.incrementAndGet()); threadPoolExecutor.execute(worker); }

  1. Boss boss = new Boss(countDownLatch);
  2. threadPoolExecutor.execute(boss);
  3. threadPoolExecutor.shutdown();

} ```

synchronized 关键字

synchronized