JUC——进程与线程

2. 进程与线程

2.1 进程与线程

进程
  • 进程是用来加载指令、管理内存、管理IO的,可以视为程序的一个实例,大部分程序可以同时运行多个实例进程。

  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。

线程
  • 一个进程之内可以有多个线程

  • 一个线程就是一个指令流,是Java中的最小调度单位。在Windows中进程不活动,作为线程的容器。

二者对比:
  • 进程相互独立,拥有共享的资源,如内存空间,供内部线程共享。

  • 进程间通信较复杂,不同计算机的进程通信需要通过网络,如HTTP

  • 线程通信因为共享内存而相对简单,上下文切换成本比进程低。

2.2 并行与并发

  • 线程轮流使用CPU称为并发(concurrent),指同一时间应对(dealing with)多件事情的能力

  • 并行(parallel)是同一时间动手做(doing)多件事情的能力,即真正的同时运行

2.3 应用

应用之异步调用
  • 从方法的调用角度来讲,如果需要等待结果返回,才能继续进行就是同步,反之是异步。

  • 多线程可以让方法执行变为异步,如在读取磁盘文件时,可以避免阻塞主线程。

应用之提高效率
  • 可以充分利用多核CPU的优势,将串行计算改为并行计算,花费时间只取决于最长线程运行时间,以及最后加上汇总时间。

  • 单核虽然不能通过多线程提高效率,但可以让不同的线程轮流使用CPU ,避免被一个线程长时间占用。

  • IO操作不占用CPU,但阻塞IO(如拷贝文件)需要一直等待IO结束,不能充分利用线程。因此我们需要非阻塞IO和异步IO进行优化。

3. Java线程

3.1 创建和运行线程

Java默认有一个主线程在运行,如果还想创建线程,有三种方法。

方法一,直接使用Thread

例如:

// 构造方法的参数是给线程指定名字
Thread t1 = new Thread("t1") {
  @Override
  // run方法内实现了要执行的任务
  public void run() {
    log.debug("hello");
  }
};
t1.start();
方法二,使用Runnable配合Thread

把线程和任务分开,Runnable表示可运行的任务(线程要执行的代码)

// 创建任务对象
Runnable task2 = new Runnable() {
  @Override
  public void run(){
    log.debug("hello");
  }
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

Java 8 以后可以使用lambda精简代码,(有@FunctionInterface注解的接口)

Runnable task2 = () -> log.debug("hello");

用Runnable更容易与线程池等高级API配合,让任务类脱离了Thread继承体系,变为了组合关系。

方法三,FutureTask配合Thread

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

// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
 log.debug("hello");
 return 100;
});
​
new Thread(task3, "t3").start();
​
// get()会使主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);

输出

19:22:27 [t3] c.ThreadStarter - hello
19:22:27 [main] c.ThreadStarter - 结果是:100

3.2 查看进程线程的方法

Windows

  1. 任务管理器

  2. tasklist查看进程

  3. taskkill杀死进程

Linux

  1. ps -fe 查看所有进程,输出的是静态的进程信息

  2. ps -ft -p <PID> 查看某个进程的所有线程

  3. kill 杀死进程

  4. top 交互式的实时进程查看工具,按大写H切换是否显示进程

  5. top -H -p <PID> 查看某个进程的所有线程

Java

  1. jps 命令查看所有 Java 进程

  2. jstack <PID> 查看某个 Java 进程(PID)的所有线程状态

  3. jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

3.4 原理之线程运行

栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)

每个线程启动后,虚拟机就会为其分配一块栈内存。 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

线程上下文切换

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码:

线程的 cpu 时间片用完 垃圾回收 有更高优先级的线程需要运行

线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等 Context Switch 频繁发生会影响性能

3.5 常见方法

方法名static功能说明注意
start()启动一个新线 程,在新的线程 运行 run 方法 中的代码start 方法只是让线程进入就绪,里面代码不一定立刻 运行(CPU 的时间片还没分给它)。每个线程对象的 start方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException
run()新线程启动后会 调用的方法如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象, 来覆盖默认行为
join()等待线程运行结 束
join(long n)等待线程运行结 束,最多等待 n 毫秒
getId()获取线程长整型 的 idid唯一
getState()获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
setPriority(int)修改线程优先级java中规定线程优先级是1~10 的整数,较大的优先级 能提高该线程被 CPU 调度的机率
isInterrupted()判断是否被打 断,不会清除 打断标记
isAlive()线程是否存活 (还没有运行完 毕)
interrupt()打断线程如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记 ;如果打断的正在运行的线程,则会设置打断标记 ;park 的线程被打断,也会设置打断标记
interrupted()static判断当前线程是 否被打断会清除 打断标记
currentThread()static获取当前正在执 行的线程
yield()static提示线程调度器 让出当前线程对 CPU的使用主要是为了测试和调试
Start与run的区别

直接调用 run 是在主线程中执行了 run,没有启动新的线程。

使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码。

sleep 与 yield

sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)

  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException

  3. 睡眠结束后的线程未必会立刻得到执行

  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程。

  2. 具体的实现依赖于操作系统的任务调度器。

3.6 join和interrupt方法详解

join() 方法用于等待线程的结束。当一个线程调用另一个线程的 join() 方法时,它会被阻塞,直到被调用的线程执行完成,或者达到了指定的超时时间。

作用:
  1. 等待其他线程完成任务:当一个线程需要依赖另一个线程的结果时,可以使用 join() 方法等待另一个线程执行完成,然后再继续执行。

  2. 协调线程执行顺序:通过 join() 方法可以协调多个线程的执行顺序,保证在某个线程执行完成后再执行下一个线程。

  3. 等待线程结束后进行资源回收:有时候需要在主线程等待子线程执行完成后再进行资源回收等操作,可以使用 join() 方法实现。

interrupt() 方法是 Java 中的一个线程方法,用于中断线程的执行。它会将目标线程的中断标志位设置为 true,通常用于请求线程在合适的时候终止执行。

作用:
  1. 请求线程中断interrupt() 方法向目标线程发送一个中断请求,通常用于请求线程在合适的时候终止执行。

  2. 唤醒被阻塞的线程:如果目标线程因为阻塞状态而被挂起,调用 interrupt() 方法会唤醒它并抛出 InterruptedException 异常。抛出异常后,会清空打断标记,即设为false。例如打断 sleep,wait,join 等线程。

  3. 检查线程是否被中断:可以通过调用 Thread.currentThread().isInterrupted() 方法来检查当前线程的中断状态,以确定是否需要中断线程的执行。

打断 park 线程

打断 park 线程, 不会清空打断状态

private static void test3() throws InterruptedException {
  Thread t1 = new Thread(() -> {
  log.debug("park...");
  LockSupport.park();
  log.debug("unpark...");
  log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
  }, "t1");
  t1.start();
  
  sleep(0.5);
  t1.interrupt();
}

输出

21:11:52.795 [t1] c.TestInterrupt - park... 
21:11:53.295 [t1] c.TestInterrupt - unpark... 
21:11:53.295 [t1] c.TestInterrupt - 打断状态:true 

如果打断标记已经是 true, 则 park 会失效。可以使用 Thread.interrupted() 清除打断状态,将标记设为false。

3.7 不推荐的方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁

  1. stop() 停止线程运行

  2. suspend() 挂起(暂停)线程运行

  3. resume() 恢复线程运行

3.8 主线程与守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。

有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。垃圾回收器线程就是一种守护线程

3.8 线程状态

操作系统层面的五种状态:

【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联

【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行

【运行状态】指获取了 CPU 时间片运行中的状态 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换

【阻塞状态】 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们

【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

Java API层面的六种状态(Thread.State)

NEW 线程刚被创建,但是还没有调用 start() 方法

RUNNABLE 当调用了start() 方法之后,Java API 层面的 RUNNABLE状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行) BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分

TERMINATED 当线程代码运行结束

  • 21
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值