进程与线程

基础概念

并发(concurrent):是同一时间应对(dealing with)多件事情的能力
并行(parallel):是同一时间动手做(doing)多件事情的能力

同步:需要等待结果返回,才能继续运行

异步:不需要等待结果返回,就能继续运行

进程

概念

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐等)。

线程

概念

  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器北
  • 与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈

创建和运行线程

  1. 直接使用Thread

    1
    2
    3
    4
    5
    6
    7
    8
    // 创建线程对象
    Thread t = new Thread() {
    public void run() {
    // 要执行的任务
    }
    };
    // 启动线程
    t.start();
  2. 使用 Runnable 配合 Thread

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Runnable runnable = new Runnable() {
    public void run(){
    // 要执行的任务
    }
    };
    // 创建线程对象
    Thread t = new Thread( runnable );
    // 启动线程
    t.start();

    把线程和任务分开了更灵活,更容易与线程池等高级 API 配合

  3. FutureTask 配合 Thread

    1
    2
    3
    4
    5
    6
    7
    8
    // 创建任务对象
    FutureTask<Integer> task = new FutureTask<>(() -> {
    return 100;
    });
    // 参数1 是任务对象; 参数2 是线程名字,推荐
    new Thread(task, "t1").start();
    // 主线程阻塞,同步等待 task 执行完毕的结果
    Integer result = task.get();

    FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

  4. 使用线程池获取线程

重点方法

  • Sleep(n):让当前线程Running 进入 Timed Waiting 状态,并且不释放锁(阻塞)
    • 其它线程可以使用这个线程的interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出InterruptedException
    • 睡眠结束后,会进入就绪状态等待cpu调度
    • 相比于Thread.sleep(n)来说TimeUnit.SECONDS.sleep(n)可读性更好
  • yield():调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  • join([long n]):等待线程运行结束,最多等待 n毫秒(该参数可选)
    • 例如:t1.join()主要是让本线程与t1同步,等待其执行完毕
  • interrupt():打断线程的阻塞状态并让其抛出InterruptedException,并且打断标记(在sleep、wait、join状态下记为false,正常运行状态的线程会从false变为true)
    • isInterrupted():判断是否被打断,不会清除打断标记
    • interrupted():判断是否被打断,会清除打断标记
  • setDaemon(true):设置线程为守护线程,守护线程只要其它非守
    护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
  • LockSuport.park()/LockSuport.unpark(线程对象):暂停当前线程/恢复某个线程(unpark可以放在park之前同样能恢复,原理是因为底层每个线程关联了一个Parker对象,里面有一个_counter,调用unpark让其变为1,只要其为1,park之后线程任然会被恢复)

线程状态

image-20230331211517778

  • 初始状态:仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 可运行状态:(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 运行状态:指获取了 CPU 时间片运行中的状态,当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 阻塞状态:如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入阻塞状态,等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至可运行状态
  • 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

Java中有6种状态:

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的
    【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
  • TERMINATED 当线程代码运行结束

转换流程:

  • NEW --> RUNNABLE:调用 t.start() 方法

  • RUNNABLE <--> WAITING

    • wait()/notify()[notifyAll()、interrupt()]

      • wait():RUNNABLE --> WAITING
      • notify()[notifyAll()、interrupt()]:如果竞争锁成功则WAITING --> RUNNABLE,否则WAITING --> BLOCKED
    • join():调用join()**时状态变为RUNNABLE --> WAITING,当线程结束或者调用interrupt()**会从 WAITING --> RUNNABLE

    • LockSupport.park()/LockSupport.unpark(目标线程):

      • LockSupport.park():RUNNABLE --> WAITING
      • LockSupport.unpark(目标线程)/interrupt():WAITING -->RUNNABLE
  • RUNNABLE <--> TIMED_WAITING

    • obj.wait(long n)
    • t.join(long n)
    • Thread.sleep(long n)
    • LockSupport.parkUntil(long millis)

    其中wait会释放锁,sleep和park不会

  • RUNNABLE <--> BLOCKED:竞争锁失败RUNNABLE --> BLOCKED,反之BLOCKED --> RUNNABLE

  • RUNNABLE <--> TERMINATED:当前线程所有代码运行完毕或者异常中断,进入TERMINATED

两者区别

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂
    • 同一台计算机的进程通信称为 IPC(Inter-process communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

查看进程线程的方法

windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist | findstr (进程名,例如java) 查看进程 或者 netstat -aon|findstr "8080" 查看端口
  • taskkill /pid 进程号 -t -f杀死进程

linux

  • ps -fe 查看所有进程
  • ps -fT -p 查看某个进程(PID)的所有线程
  • kill 杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p 查看某个进程(PID)的所有线程

专门查看Java进程:

  • jps 命令查看所有 Java 进程
  • jstack 查看某个 Java 进程(PID)的所有线程状态
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

总结

  • 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用cpu ,不至于一个线程总占用 cpu,别的线程没法干活。
  • 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的:
    • 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分。
    • 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义。
  • IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化。

image-20230329210336024