进程和线程基础知识

119 阅读8分钟

进程与线程

进程

  • 程序由指令和数据组成,工作时将指令加载至CPU,将数据加载至内存。此外在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令和管理内存的。
  • 将一个程序被执行,相关的代码由硬盘加载至内存,此时就开启了一个新的进程。
  • 因此,进程是程序运行起来的状态。

线程

  • 一个进程可以划分为多个线程。
  • 一个线程就相当于一个指令流,将指令流中的一条条指令一一定顺序交给CPU执行。
  • Java中线程是最小的调度单位,进程是资源分配的最小单位。

进程和线程的区别

  • 进程相互独立,线程存在于进程中,相当于进程的一个子集
  • 进程拥有共享的资源,比如内存空间等,供其内部的线程进行共享
  • 线程通信相对简单,因为它们共享进程内的内存,因此多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般比进程低

并发和并行

并发 (concurrent)

  • 并发一定是单核CPU,各线程串行执行。
  • 操作系统中的任务调度器将CPU的时间片分给不同的线程使用。
  • 由于CPU在线程间的切换非常快,所在带来并行的效果。实际上宏观并行,微观串行

并行

  • 一定是多核CPU,多核,不一定是多个
  • 各线程在各个核上同时运行

创建线程

继承 Thread

Thread t = new Thread() {
    @override
    public void run() {
        // 要执行的任务
    }
};
// 启动线程
t.start();

实现 Runnable

Runnable runnable = new Runnable() { // 此方法可以使用lambda简化,因为这是一个接口的形式
    @override
    public void run() {
        // TODO
    }
};
Thread t = new Thread(runnable);
t.start();

实现 Callable

// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
    @override
    public Integer call() throws Exception {
        return 100;
    }
});
​
new Thread(task,"t").start();
​
// 主线程阻塞,同步等待task执行完毕的结果
Integer result = task.get();

三者区别

  • 继承Thread: 将线程的创建和方法体写在一起,编码简单
  • 实现Runnable:将线程创建和方法体分离,便于调用线程池等api,结构清晰
  • 实现Callable:又增加了返回值

查看进程的方法

windows

  • 任务管理器查看进程和线程数,也可用来杀死进程
  • tasklist指令查看进程
  • taskkill指令杀死进程

Linux

  • ps -fe 查看所有进程
  • ps -fT -p <PID> 查看某个进程的所有线程
  • kill 杀死进程
  • top -H -p <PID> 查看某个进程的所有线程

线程运行原理

栈与栈帧

JVM由堆、栈、方法区组成。每个线程启动后,虚拟机会为其分配一块栈内存。

  • 每个栈由多个栈帧组成,对应每次方法调用时所占用的内存
  • 每个线程只有一个活动栈帧,对应当前正在执行的那个方法

线程上下文切换

CPU不再执行当前的线程,转而执行另一个线程的代码。CPU可能因下面的情况导致不再执行当前的线程:

  • 线程的CPU时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 县城自己调用了sleep、yield、wait、join、park、synchronized、lock等方法

当发生上下文切换时,操作系统保存当前的线程状态,并且恢复另一个线程的状态,也就是Java中的程序计数器,其作用为:记住下一条jvm指令的执行地址,是线程私有的。

相关方法

start 和 run

  • 启动线程应调用start方法,start方法进而去调用run方法
  • 如果直接调用run方法,也可以运行,但运行的是main主线程,并没有启动新的线程,不能起到异步调用的结果
  • 如果连续两次调用start会抛异常

sleep 与 yield

  • sleep让线程休眠,从Running状态进入Timed Waiting状态
  • 其他线程可使用interrupt方法打断正在休眠的线程,此时sleep方法抛出InterruptedException
  • 休眠结束后的线程未必会立刻执行
  • yield会让线程从Running状态变为Runnable状态,然后调度执行其他同优先级的进程。若此时没有同优先级的线程,则当前线程不会暂停。具体实现依赖操作系统的任务调度器

线程优先级

  • 线程优先级会提示调度器区优先调度该线程,但仅仅是一个提示,调度器可以忽略它
  • 如果CPU忙,则优先级高的线程会获得更多的时间片,但CPU闲时,优先级不起作用

join

  • 等待线程运行结束,可设置等待时间(传参,传入毫秒值)
  • 哪个线程对象调用join方法,就等待哪个线程运行结束
  • 底层原理就是wait

interrupt

打断阻塞状态的线程或正在运行状态的线程

  • interrupt() : 打断线程,并清除打断标记
  • isInterrupted():判断是否被打断,不会清除打断标记
  • interrupted():静态方法,判断当前线程是否被打断,会清除打断标记

两阶段终止模式 interrupt

若使用 stop 方法停止线程,则线程被真正杀死。如果线程锁住了共享资源,那么在杀死线程后就无法释放锁,其他线程无法获取锁

若使用 System.exit 方法停止线程,则会让整个程序都停止。因此,使用两阶段终止模式。

两阶段终止模式:

  • 若被打断,执行相应处理,结束循环
  • 若未被打断,睡眠2s,有异常则设置打断标记,继续循环
  • 若无异常,则执行监控记录(一般记录到日志中),继续循环

interrupt 打断 park

parkunpark均为LockSupport类中的方法

LockSupport.park(); // 暂停当前线程
LockSupport.unpark(被暂停的线程对象); // 恢复某个线程

示例:

Thread thread = new Thread(() -> {
      System.out.println("start.....");
      try {
          Thread.sleep(1000);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      System.out.println("park....");
      LockSupport.park();
      System.out.println("resume.....");
});
thread.start();
Thread.sleep(2000);
System.out.println("unpark....");
LockSupport.unpark(thread);

运行结果:

start.....
park....
unpark....
resume.....
  • park中的线程处于wait状态
  • unpark既可在park之前调用或之后调用,都是用来恢复某个线程的运行。调用unpark后再调用park,线程不会暂停

原理:每个线程都有自己的Parker对象,由_counter,_cond,_mutex组成

  • _cond:资源
  • _counter:资源的数量,0耗尽,1充足
  • _mutex:互斥信号量

调用park:

  • 当前线程调用park方法
  • 检查_counter,若为0则获得_mutex互斥锁
  • 线程进入_cond条件变量阻塞
  • 设置_counter=0

调用unpark:

  • 调用unpark方法,设置_counter=1
  • 唤醒_cond条件变量中的线程
  • 该线程恢复运行
  • 设置_counter=0

先调用unpark再调用park

  • 调用unpack方法,设置_counter=1
  • 当前线程调用park方法
  • 检查_counter,值为1,则线程无需阻塞,继续运行
  • 设置_counter=0

几个不推荐使用的方法

  • stop():停止线程运行,
  • suspend():挂起(暂停)线程
  • resume():回复线程运行

会破坏同步代码块,造成对象的锁得不到释放,造成死锁。应使用两阶段终止模式(interrupt)

守护线程

Java进程需要等待所有线程结束后才会结束。有一种特殊的线程叫守护线程,只要其他非守护线程结束了,即使守护线程未执行完毕,也会结束。

Thread t = new Thread();
t.setDaemon(true); // 设为守护线程
  • 垃圾回收器就是一种守护线程
  • tomcat中的Acceptor和Poller线程(用于发送和接收)都是守护线程,tomcat接收到shutdown命令后不会等待它们处理完当前请求

线程的状态

线程五种状态

五种状态是从操作系统层面来描述的

  • 初始状态:仅在语言层面上创建了线程对象,还未与操作系统线程关联(new了一个Thread对象,但未调用start方法)
  • 就绪状态:该线程已被创建,与操作系统线程相关联,可由CPU调度执行,但还未获得CPU时间片
  • 运行状态:获取了CPU时间片运行中的状态。若时间片用完,则转回就绪状态
  • 阻塞状态:线程让出CPU使用权,导致进程上下文切换。后期操作系统可唤醒阻塞的线程,转为就绪状态
  • 终止状态:线程已执行完毕,生命周期结束,不会再转为其他状态

线程的六种状态

六种状态是从Java API层面来描述的,根据Thread.State枚举类

  • NEW:线程刚创建,未调用start,跟“初始状态”一样
  • RUNNABLE:调用了start方法,涵盖了“就绪状态”、“运行状态”和“阻塞状态”。由BIO导致的线程阻塞,在Java中无法区分,仍认为就绪
  • BLOCKED:(阻塞),线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态
  • WAITING:(等待),处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态
  • TIME_WAITING:(超时等待),处于这种状态的线程不会被分配CPU执行时间,在达到一定时间后会被自动唤醒
  • TERMINATED:相当于“终止状态”