1.1 进程与线程
1.1.1 什么是进程
进程:系统调度分配资源的最小或基本单位。
- 资源分配:操作系统通过进程来管理内存、CPU、I/O等资源。
- 任务调度:操作系统根据进程的状态和优先级,决定哪个进程应该获得CPU时间。
系统内核中 进程 就是一段记录专有资源和状态的 task_struct 结构体,就是一个数据结构或者理解为一个存储资源信息的对象。其存储的信息主要包括:
- 标识符:与进程相关的唯一标识符。
- 状态:描述进程的状态(新建、就绪、运行、阻塞、终止、睡眠、挂起、僵尸、等待)。
- 优先级:多个进程执行的先后顺序。
- 程序计数器:与进程页表相关的计数器。
- 内存指针:程序代码和进程相关数据的指针。
- 上下文数据:进程执行时处理器的寄存器中的数据。
- I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和进程使用的文件列表等。
- 记账信息:包括处理器的时间总和,记账进程号等。
这个 task_struct 结构体有个专门的名字:PCB --> PROCESS control block,也叫进程控制块。
PCB 数据保存在操作系统4G内存虚拟地址中的内核态中,也就是 3-4G 内存这一段内,显然用户态时是无法访问的,想要访问就必须从用户态切换到内核态。
进程在内核中就是这么个东西,就是一个叫 PCB 的 task_struct 结构体,系统内核中有一个 PCB TABLE 用来存储、调度进程。
1.1.2 什么是线程
线程:进程的执行单元,是CPU调度和分派的基本单位(竞争 CPU 资源的基本单位) 。
线程在内核中同 进程 一样,也是一个 task_struct 结构体,线程在 new 出来之后就是把 进程的 PCB 复制一份给自己,然后加上 PCB 没有的 PC程序计数器,SP堆栈,State状态,寄存器 这些信息。线程的 task_struct 结构体 叫做:TCB --> thread control block。
TCB 数据保存在 进程4G内存虚拟地址中的用户态中,也就是 0-3G 内存这一段内。线程本质上就是多了一个任务列表,就是 栈帧 这个东西。
大家想 线程为什么能共享堆内存资源呢,就是把 进程资源信息 PCB 复制一份给 自己的 TCB 就行了。
1.2 使用多线程的3种方式
1.2.1 继承 Thread
继承 Thread 类然后重写 run() 方法,在 main() 方法中调用该类实例对象的 start() 方法。
注意:调用run()方法并不会开启多线程,还是同步执行。
public class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("执行子线程...");
}
}
public class TestMain {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println("主线程...");
}
}
1.2.2 实现 Runnable 接口
实现 Runnable 接口,然后重写 run() 方法,在 main() 方法中调用该类实例对象的 start() 方法。
public class MyRunnable extends Runnable {
@Override
public void run() {
super.run();
System.out.println("执行子线程...");
}
}
public class TestMain {
public static void main(String[] args) {
Runnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
System.out.println("主线程...");
}
}
1.2.3 实现 Callable
Callable 解决了以上两种方法的弊端:
- 无法获取子线程的返回值
- run方法不可以抛出异常
实现 Callable 接口,然后重写 call() 方法,在 main() 方法中调用该类实例对象的 start() 方法。
public class MyCallable extends Callable {
int i = 0;
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName() + " i的值:" + i);
return i++; //call方法可以有返回值
}
}
public class TestMain {
public static void main(String[] args) {
Callable myCallable = new MyCallable();
for (int i = 0; i < 10; i++) {
FutureTask task = new FutureTask(myCallable);
new Thread(task, "子线程"+i).start();
try {
//获取子线程的返回值
System.out.println("子线程返回值:" + task.get() + "\n");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
提供了 Future 接口来代表 Callable#call() 方法的返回值,并为 Future 接口提供了一个实现类 FutureTask。
1.2.3.1 FutureTask 类
从下面的 FutureTask 类图中可以看出,FutureTask 实现了 RunnableFuture 接口,RunnableFuture 接口继承了 Runnable 接口和 Future 接口,所以 FutureTask 兼备 Runnable 和 Future 两种特性。
(1)FutureTask 类中常用方法
1)构造方法
- public FutureTask(Callable callable) 创建一个 FutureTask ,它将在运行时执行给定的 Callable 。 参数: callable表示可调用任务 。
- public FutureTask(Runnable runnable, V result) 创建一个 FutureTask ,将在运行时执行给定的 Runnable ,并安排 get将在成功完成后返回给定的结果。 参数:runnable 表示可运行的任务 ;result 表示成功完成后返回的结果。
2)常用的方法
- public boolean isCancelled() 如果此任务在正常完成之前取消,则返回 true 。
- public boolean isDone() 返回true如果任务已完成。
- public V get() 阻塞等待计算完成,然后检索其结果。
- public V get(long timeout, TimeUnit unit) 如果需要等待最多在给定的时间计算完成,然后检索其结果(如果可用)。
- public boolean cancel(boolean mayInterruptIfRunning) 尝试取消执行此任务。
- protected void set(V v) 将此未来的结果设置为给定值,除非此未来已被设置或已被取消。
(2) FutureTask类的使用示例
案例场景:通过示例进行多任务计算,通过get()方法可以异步获取执行结果。
//创建一个计算任务类,实现Callable接口,重写call方法
public class ComputeTask implements Callable<Integer> {
private String taskName;//任务名称
//任务构造器
public ComputeTask(String taskName) {
this.taskName = taskName;
System.out.println("创建【计算任务】开始,计算任务名称:" + taskName);
}
//计算任务的方法
@Override
public Integer call() throws Exception {
Integer result = 0;
for (int i = 1; i <=5; i++) {
result += i;
} //15
System.out.println("【计算任务】"+taskName +"执行完成。");
return result;
}
}
//创建测试类
public class TestMain {
public static void main(String[] args) {
//任务集合
List<FutureTask<Integer>> futureTasks = new ArrayList<>();
//创建固定长度的线程池
ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 1; i <= 10; i++) {
//实例化FutureTask,传入计算任务类
FutureTask<Integer> futureTask = new FutureTask<>(new ComputeTask(i + ""));
//添加到任务集合中
futureTasks.add(futureTask);
//提交任务到线程池
pool.submit(futureTask);
}
System.out.println("所有【计算任务】提交完毕,主线程开始执行");
System.out.println("【主线程任务】开始============");
//主线程睡眠5秒,模拟主线程做某些任务
try {
Thread.sleep(5000);
System.out.println("【主线程任务】开始执行某些任务============");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("【主线程任务】结束============");
//用于打印任务执行结果
Integer result = 0;
for (FutureTask<Integer> task : futureTasks) {
try {
//FutureTask的get()方法会自动阻塞,知道得到任务执行结果为止
result += task.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
//关闭线程池
pool.shutdown();
System.out.println("多线程多任务执行结果:" + result);
}
}
//结果打印
创建【计算任务】开始,计算任务名称:1
创建【计算任务】开始,计算任务名称:2
创建【计算任务】开始,计算任务名称:3
创建【计算任务】开始,计算任务名称:4
创建【计算任务】开始,计算任务名称:5
创建【计算任务】开始,计算任务名称:6
创建【计算任务】开始,计算任务名称:7
创建【计算任务】开始,计算任务名称:8
创建【计算任务】开始,计算任务名称:9
创建【计算任务】开始,计算任务名称:10
所有【计算任务】提交完毕,主线程开始执行
【主线程任务】开始============
【计算任务】1执行完成。
【计算任务】2执行完成。
【计算任务】6执行完成。
【计算任务】7执行完成。
【计算任务】9执行完成。
【计算任务】10执行完成。
【计算任务】8执行完成。
【计算任务】4执行完成。
【计算任务】3执行完成。
【计算任务】5执行完成。
【主线程任务】开始执行某些任务============
【主线程任务】结束============
多线程多任务执行结果:150
1.3 线程的状态
5种线程状态_操作系统中线程的生命周期以及线程状态:
-
新建状态(New) :当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
-
就绪状态(READY) :当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
-
运行状态(Running) :当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
-
阻塞状态(Blocked) :处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
- 等待阻塞: 运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
- 同步阻塞: 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
- 其他阻塞: 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、 join()等待线程终止或者超时、 或者I/O处理完毕时,线程重新转入就绪状态。
-
死亡状态(Dead) :线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
Java线程的6种状态_定义在 Thread#State 中(每个Java线程对应一个操作系统线程) :
- 初始(NEW) :新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
- 阻塞(BLOCKED) :表示线程阻塞于锁。线程进入阻塞状态是被动的, 而线程进入等待状态是主动的。
- 等待(WAITING) :进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING) :该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED) :表示该线程已经执行完毕。
1.4 线程一些重要的方法
1.4.1 wait() 和 sleep()
- object.wait(long millis) : 定义在 Object 类,用于使当前线程进入等待状态,并释放对象的锁,调用前必须先获得对象的锁。直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,当前线程被唤醒进入“阻塞状态”。wait(long timeout) 带参情况下当超出时间限制后自动唤醒。wait(0) 就是让其一直等待。当调用 wait 方法的是线程对象时,当该线程终止的时候,会调用线程自身的notifyAll()方法,会通知所有等待在该线程对象上的线程。
- Thread.sleep(long millis) : 定义在 Thread 类上的静态方法,用于使当前线程进入等待状态,不释放对象的锁;millis后线程自动苏醒进入就绪状态(因为sleep方法不释放锁,所以不像wait方法一样进入阻塞状态)。作用:给其它线程执行机会的最佳方式。
1.4.2 interrupt()、interrupted() 和 isInterrupted()
- interrupt() :向调用该方法的线程发出信号—>线程中断状态已被设置。该方法不能真正中断线程,只是打了个中断标记。
- isInterrupted() :用来判断调用该方法的线程的中断状态(true or false)。线程若死亡,isInterrupted()方法返回false。
- interrupted() :是个Thread的static方法,用来判断当前线程的中断状态并且恢复中断状态标记(重新变为未中断)。
class TestInterrupted {
public static void main(String[] args) throws InterruptedException {
Thread mt = new Thread(() -> {
while (true) { //线程若死亡,isInterrupted返回false
}
}, "t1");
mt.start();
mt.interrupt();
System.out.println("第一次调用isInterrupted()方法,值为:" + mt.isInterrupted());
System.out.println("第二次调用isInterrupted()方法,值为:" + mt.isInterrupted());
//判断的是当前线程,也就是main线程
System.out.println("调用interrupted()方法,值为:" + Thread.interrupted());
System.out.println("调用interrupted()方法,值为:" +Thread.interrupted());
Thread.currentThread().interrupt(); //给当前线程打上中断标记
System.out.println("调用interrupted()方法,值为:" + Thread.interrupted());
System.out.println("调用interrupted()方法,值为:" +Thread.interrupted());
System.out.println("thread是否存活:" + mt.isAlive());
}
}
//结果打印
第一次调用isInterrupted()方法,值为:true
第二次调用isInterrupted()方法,值为:true
调用interrupted()方法,值为:false
调用interrupted()方法,值为:false
调用interrupted()方法,值为:true
调用interrupted()方法,值为:false
thread是否存活:true
在Java中,当一个线程处于等待状态(如sleep、wait或join)时,如果它被中断,那么它会抛出一个 InterruptedException 异常:
public class ThreadExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000); // 线程进入等待状态
} catch (InterruptedException e) {
System.out.println("线程被中断了");
// 处理中断逻辑
}
});
thread.start(); // 启动线程
thread.interrupt(); // 中断线程
}
}
1.4.3 join()
作用:让 "主线程" 等待 "子线程" millis 毫秒后再继续运行,若millis为0,则一直等待到 "子线程" 执行结束。
- 主线程:执行 threadA.join() 的线程;
- 子线程:threadA.join() 中的 threadA。
1)源码解析
当我们在 main() 调用 threadA.join() 时,从源码中,我们可以发现。当millis==0时,会进入while(isAlive())循环;即只要子线程是活的,主线程就会在join方法调用 wait() 方法进入等待状态并释放主线程对 threadA 对象的锁。
注:join() 方法被 Synchronized 修饰,锁的是 this 对象,在 join() 方法内 wait(0) 表示释放 this(threadA) 对象的锁并进入等待状态。又因为 threadA 是线程实例,上文我们知道如果 调用 wait() 方法的是线程对象时,会在子线程执行完毕后自动唤醒主线程。所以 threadA.join() 执行完毕后 main() 会继续执行。
2)测试用例
public class TestThreadMethod {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
System.out.println("等3s再执行");
Thread.sleep(3000);
System.out.println("3s后了可以执行了");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程执行了");
});
thread.start();
thread.join();
System.out.println("主线程执行了");
}
}
//执行结果
等3s再执行
3s后了可以执行了
子线程执行了
主线程执行了
1.4.4 其他
- isAlive() :判断调用该方法的线程是否处于运行状态
- setPriority(int newPriority) : 设置线程优先级(越大越高)
- suspend() : 暂停线程,不释放锁(独占),该方法已过时
- resume() : 恢复因suspend方法而暂停的线程
1.5 Java线程阻塞的代价_内核态与用户态切换
Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与内核态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
- 如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
- 如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。
1.6 Java对象的内存结构
一个Java类在JVM中被拆分为了两个部分:数据和描述信息,分别对应OOP和Klass。OOP表示java对象应该承载的数据,而Klass表示描述对象有多大,函数地址,对象大小,静态区域大小。
1.6.1 Ordinary Object Pointer(OOP_普通对象指针)
Ordinary Object Pointer (普通对象指针),它用来表示对象的实例信息,看起来像个指针实际上是藏在指针里的对象。Klass 是在class文件在加载过程中创建的,OOP 则是在Java程序运行过程中new对象时创建的。
在HotSpot虚拟机中,OOP对象保存在堆内存中,由3个部分组成:
-
对象头 :包括了堆对象的类型、GC状态、锁状态和哈希码等基本信息。
- Mark Word :包含一系列的标识,例如锁的标记、对象年龄等。在32位系统占4字节,在64位系统中占8字节;
- klass Pointer :指向对象所属的 Class 在方法区的内存指针,通常在32位系统占4字节,在64位系统中占8字节,64位 JVM 在 1.6 版本后默认开启了压缩指针,那就占用4个字节;
- Length :如果对象是数组,还需要一个保存数组长度的空间,占 4 个字节;
-
实例数据 :主要是存放对象的字段数据信息,JVM 为了内存对齐在这部分会执行一个字段重排列的动作。
-
字段重排列 :JVM 在分配内存时不一定是完全按照类定义的字段顺序去分配,而是根据 JVM 选项 -XX:FieldsAllocationStyle 来进行排序,排序遵守以下规则:
- 如果一个字段的大小为 N 字节,则从对象开始的内存位置到该字段的位置偏移量一定满足:字段的位置 - 对象开始位置 = mN (m >=1), 即是 N 的整数倍 ;
- 子类继承的父类字段的偏移量必须和父类保持一致 ;
-
-
对齐填充数据 :虚拟机规范要求每个对象所占内存字节数必须是 8N(8 的倍数),对齐填充的存在就是为了满足规范要求。对齐填充数据不是必须的,另外填充数据可能在实例数据末尾,也可能穿插在实例数据各个属性之间。JVM 要求内存对齐是为了计算机高效寻址,快速读取对象数据。
1.6.1.1 MarkWord
markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容。
1)参数解析
- lock: 2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
- biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有启偏向锁。
- age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
- identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
- thread:持有偏向锁的线程ID。
- epoch:偏向时间戳,涉及到了偏向锁中非常重要的2个优化(批量重偏向和批量撤销)。通过epoch,jvm可以知道这个对象的偏向锁是否过期了,过期的情况下允许直接试图抢占,而不进行撤销偏向锁的操作。
- ptr_to_lock_record:指向栈中锁记录的指针。
- ptr_to_heavyweight_monitor:指向管程Monitor的指针。
2)32位 MarkWord 解析
3)64位 MarkWord 解析
*【如何分析打印的对象头】 *:
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
public class JOLExample2 {
public static void main(String[] args) {
A a = new A();
//没有计算HashCode之前的对象头
out.println("before hash");
out.println(ClassLayout.parseInstance(a).toPrintable());
//jvm计算HashCode
out.println("jvm----------" + Integer.toHexString(a.hashCode()));
//当计算完HashCode之后,我们可以查看对象头的信息变化
out.println("after hash");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
运行结果如下:
可以看到我们在没有进行 hashcode 运算的时候,所有的值都是空的。当我们计算完了hashcode,对象头就是有了数据。因为是小端存储,所以你看的值是倒过来的。前25bit没有使用所以都是0,后面31bit存的hashcode,所以第一个字节中八位存储的分别就是分代年龄、偏向锁信息、对象状态,这8bit分别表示的信息如下图所示:
无锁状态 和 偏向锁状态 不能共存,即如果开启了”偏向锁模式“,那么对象在初始化的时候,其对象头的 MarkWord 部分就是偏向锁状态(101),而不是无锁状态(001),其中 thread id = 0。偏向锁状态没法保存 hashcode,所以:
-
当一个对象加锁前已经计算过 identity hashcode,它就无法进入偏向锁状态,而是直接升级为轻量级锁;
-
当一个对象当前正处于偏向锁状态,然后计算其identity hashcode的话,则它的偏向锁会被撤销,并且锁会直接膨胀为重量级锁。
- 如果对象调用hashcode方法,会自动禁用偏向锁,是因为偏向锁的对象头中没办法存储hashcode,
- 轻量级锁的hashcode存放在获得锁的线程中的栈帧的LockRecord对象中,当释放锁时会将hashcode、age等数据恢复给锁对象,对象头状态恢复为普通状态。
- 重量级锁的hashcode存放在 Monitor(监视器)对象中。
1.6.2 Klass(方法区)
Klass包含元数据和方法信息,用来描述Java类。其主要有两个功能:
- 实现语言层面的Java类 ;
- 实现Java对象的分发功能。
一般JVM在加载class文件时,会在方法区创建 InstanceKlass,表示其元数据,包括常量池、字段、方法等。
这里着重介绍 InstanceKlass 的两个字段:
- _prototype_header:原型头,用于用于标识 Mark Word 原型,在对象被创建出来以后,会从_prototype_header拷贝数据到对象头的 Mark Word 中。
- revocation_count:撤销计数器,每次该class的对象发生偏向锁撤销操作时,计数器会自增1,当达到批量重偏向阈值(默认20)时,会执行批量重偏向;当达到批量撤销的阈值(默认40)时,会执行批量撤销。
1)代码示例
class Model
{
public static int a = 1;
public int b;
public Model(int b) {
this.b = b;
}
}
public static void main(String[] args) {
int c = 10;
Model modelA = new Model(2);
Model modelB = new Model(3);
}
上述代码的OOP-Klass模型如下所示: