并发编程基础

并发编程基础



一、Java线程

创建和运行线程的方式

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

直接重写了 Thread 类中的 run 方法。

  • Runnable 配合 Thread 创建
// 创建任务对象
Runnable runnable = new Runnable() {
	public void run(){
	// 要执行的任务
	}
};
// 创建线程对象
Thread t = new Thread(runnable, "t1");
// 启动线程
t.start();

将 Runnable 传入 Thread 类,赋值给成员变量 target ,run方法会调用 target.run() 。

  1. 方法1 是把线程和任务合并在了一起
  2. 方法2 是把线程和任务分开了
  3. 用 Runnable 更容易与线程池等高级 API 配合
  4. 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
  • FutureTask 配合 Thread 创建
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
	 log.debug("hello");
	 return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);

FutureTask 间接实现了 Runnable 接口,还额外实现了 Future 接口。

二、栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)
JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

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

一个线程对应一个 JVM Stack。JVM Stack 中包含一组 Stack Frame。线程每调用一个方法就对应着 JVM Stack 中 Stack Frame 的入栈,方法执行完毕或者异常终止对应着出栈(销毁)。

当 JVM 调用一个 Java 方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入 JVM 栈中。

在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。
在这里插入图片描述
一个栈帧包含了局部变量表(存储方法的参数和局部变量,由基本数据类型或对象引用组成)、返回地址(记录方法结束后控制流应返回的位置)、操作数栈(后进先出(LIFO)的栈结构,用于存储操作数和中间计算结果)、动态链接(关联到方法所属类的常量池,支持动态方法调用)以及帧数据区。
在这里插入图片描述
链接:深入理解 JVM 的栈帧结构——二哥


三、线程上下文切换(Thread Context Switch)

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

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

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态。

状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等。

每个线程都有一个程序计数器(记录要执行的下一条指令),一组寄存器(保存当前线程的工作变量),堆栈(记录执行历史,其中每一帧保存了一个已经调用但未返回的过程)。

寄存器 是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。

程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置。

Context Switch 频繁发生会影响性能。


四、并发常见方法

  • start()

启动一个新线程,在新的线程运行 run 方法中的代码。

start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException。

  • run()
    新线程启动后会调用的方法。

如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为。
注意:代码会在调用线程运行

  • sleep() / sleep(long n)

让当前执行的线程休眠n毫秒,休眠时让出 cpu的时间片给其它线程。

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

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

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

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

  • yield()

提示线程调度器让出当前线程对CPU的使用。

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

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

  • join() / join(long n)

等待线程运行结束

  • interrupt()

打断线程

如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park 的线程被打断,也会设置打断标记。

  • setPriority()

设置优先级

线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。

如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用。


五、守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

垃圾回收器线程就是一种守护线程

Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求


五、线程状态

1. 五种状态

操作系统 层面来描述。在这里插入图片描述

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

2. 六种状态

Java API 层面来描述,根据 Thread.State 枚举,分为六种状态。
在这里插入图片描述

  • NEW
    线程刚被创建,但是还没有调用 start() 方法。
  • RUNNABLE
    调用了 start() 方法之后。
    包含了操作系统层面的可运行状态、运行状态以及阻塞状态( BIO 读写文件导致的阻塞在 java 里无法区分,认为是可运行状态)。
  • BLOCKED、WAITING、TIMED_WAITING
    阻塞状态的细分。
  • TERMINATED
    代码运行结束。

NEW -> RUNNABLE

  1. 调用 t.start() 后变为 RUNNABLE

RUNNABLE < - > WAITING

  1. 线程获得 obj 的对象锁后调用 obj.wait() 变为 WAITING
  2. 调用 obj.notify() 、obj.notifyAll() 、t.interrupt() 后锁竞争成功变为 RUNNABLE ,失败变为 BLOCKED

  1. 当前线程调用 t.join() ,当前线程变为 WAITING
  2. t 线程运行结束或者调用了当前线程的 interrupt() 变为 RUNNABLE

  1. 当前线程调用 LockSupport.park() ,当前线程变为 WAITING
  2. 调用了LockSupport.unpark(目标线程) 或者当前线程的 interrupt() 变为 RUNNABLE

RUNNABLE < - > TIMED_WAITING

  1. 线程获得 obj 的对象锁后调用 obj.wait(long n) 变为 TIMED_WAITING
  2. 当前线程等待时间超过了n毫秒、或者调用 obj.notify() 、obj.notifyAll() 、t.interrupt() 后锁竞争成功变为 RUNNABLE ,失败变为 BLOCKED

  1. 当前线程调用 t.join(long n) ,当前线程变为 TIMED_WAITING
  2. 当前线程等待时间超过了n毫秒、或者 t 线程运行结束、或者调用了当前线程的 interrupt() 变为 RUNNABLE

  1. 当前线程调用 t.sleep(long n) ,当前线程变为 TIMED_WAITING
  2. 当前线程等待时间超过了n毫秒,变为 RUNNABLE

  1. 当前线程调用 LockSupport.parkNanos(long nanos) 或者 LockSupport.parkUntil(long millis) ,当前线程变为 TIMED_WAITING
  2. 调用了LockSupport.unpark(目标线程) 或者当前线程的 interrupt() 或者是等待超时,变为 RUNNABLE

RUNNABLE < - > BLOCKED

  1. 线程持有obj对象锁,如果竞争失败则会变成 BLOCKED
  2. 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 **BLOCKED ->RUNNABLE **,其它失败的线程仍然 BLOCKED

RUNNABLE < - > TERMINATED

当前线程所有代码运行完毕,进入 TERMINATED


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值