并发学习之synchronized,JVM内存图,线程基础知识
文章目录
- Java内存图
- 内存图区域介绍
- 执行流程
- 进程和线程
- 概念解释
- 线程的6种状态
- 简述等待队列和同步队列(阻塞队列)
- 线程之间是独立的
- synchronized
- 静态方法
- 非静态方法
- 代码块
知识总结:
- 方法区存储类信息
- 正在执行的程序叫进程,进程会开辟内存空间
- 程序的执行过程 :线程
- 线程的执行-----》方法的入栈出栈
- Java创建线程-----》Java创建一个又一个栈
Java内存图
Java内存的主要组成部分:
- 虚拟机栈
- 堆区
- 方法区
- 本地方法栈
- 程序计数器
内存图区域介绍
- 虚拟机栈:每个线程都会有自己的虚拟机栈,是线程私有的区域。
- 虚拟机栈主要用于存储栈帧,每个方法被调用时都会创建一个表栈帧。栈帧包含方法的局部变量,操作数栈和其他等。
- 操作数栈:主要用于 方法执行过程中的数据计算和临时存储。它的行为类似于传统CPU的 运算寄存器,但采用 栈结构(LIFO) 来存储和操作数据。
- 虚拟机栈主要用于存储栈帧,每个方法被调用时都会创建一个表栈帧。栈帧包含方法的局部变量,操作数栈和其他等。
- **堆:**堆是一个线程共享区域,是Java内存管理的核心区域。
- 通过new关键字创建的对象实例和数组都存储在堆中。
- 方法区:方法区是一个线程共享区域,主要用于存储类的相关信息,比如类的字段,方法的定义,常量等。
- 常量池:常量池位于方法区中,用于存放编译期生成的各种字面量和符号引用。
- 字面量:指的是在源代码中直接给出的数据值,比如直接写在源代码中的数字,字符串,字符,布尔值等。他们是常量的直观表达方式,在编译阶段就会确定下来。例如:在int num = 10;10为整形字面量。在String str = "hello"中,“hello”为字符串字面量。
- 符号引用:符号引用是一种间接引用,它在编译时用于表示类、方法、字段等在常量池中的引用。符号引用主要包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等信息。这些信息在编译阶段被存储在常量池中,用于在运行时解析和定位实际的类、方法和字段等实体。(符号引用是在编译阶段放到常量池的。)
- 常量池:常量池位于方法区中,用于存放编译期生成的各种字面量和符号引用。
- **本地方法栈:**是线程的私有区域,主要为Native方法(使用非Java语言编写的方法,如C/C++)提供服务,用于存储Native方法的调用栈信息,支持Java程序对Native的调用。
- **程序计数器:**每个线程都有一个程序计数器,是线程的私有区域。主要用于存储线程正在执行的字节码指令的地址,使得线程能够按照正确的顺序执行字节码指令,当线程执行Java方法时,程序计数器记录当前执行的字节码地址;执行Native方法时,程序计数器的值为undefined。
执行流程
-
编译:.java 到.class 的过程叫编译 。在这个过程中,Java 编译器(如
javac
命令 )对.java 源文件进行词法分析、语法分析、语义分析等操作,将符合 Java 语法规则的代码转换为字节码形式的.class 文件。字节码是一种与平台无关的中间代码,可被 Java 虚拟机(JVM)识别并执行,从而实现 “一次编写,到处运行” 的特性 。 -
**类加载:**Java 虚拟机(JVM)将.class 字节码文件加载到内存中,此过程由类加载器完成。
- 加载时会检查字节码文件的格式等是否正确,然后在方法区创建对应的类模板,存放类的结构信息,比如字段、方法等。例如
Demo.class
会被加载,其类信息放入方法区。
- 加载时会检查字节码文件的格式等是否正确,然后在方法区创建对应的类模板,存放类的结构信息,比如字段、方法等。例如
-
连接
- 验证:确保被加载的类字节码文件符合 JVM 规范,比如文件格式、语义等方面的校验,保障程序运行安全。
- 准备:为类的静态变量分配内存并设置默认初始值,如静态变量
static int num;
此时会被赋予默认值 0 。 - 解析:将常量池中的符号引用转换为直接引用,使 JVM 能直接定位到类、方法、字段等实体。
-
初始化
对类的静态变量赋予程序员设定的初始值,执行静态代码块。若有多个静态变量和静态代码块,按代码中出现的顺序执行初始化操作。
-
执行
- 程序从
main
方法开始执行,在虚拟机栈中为main
方法创建栈帧。栈帧里存放局部变量、操作数栈等信息。执行过程中,遇到对象创建指令,会在堆中分配内存创建对象。比如执行到new
关键字时,就在堆中开辟空间构建对象实例。 - 当调用其他方法时,为被调用方法创建新栈帧并压入虚拟机栈,方法执行完,对应栈帧弹出,返回上一层调用方法继续执行。
- 程序从
进程和线程
概念解释
- 进程:进程是正在执行的程序的实例。
- 进程是资源分配的基本单位。
- 不同进程之间内存隔离(一个进程崩溃通常不会影响其他进程)。
- 启动一个Java程序时,JVM就是一个进程。
- 线程:线程是进程内的执行单元,负责执行程序代码。一个进程可以包含多个线程,所有线程共享进程的内存空间(如堆、方法区),但每个线程有自己的栈。
- Java创建一个线程就是在虚拟机栈中创建一个又一个栈。
- 线程是CPU调度的基本单位。
- 线程共享进程资源,但有自己的程序计数器、栈和局部变量。
- 线程的执行 → 方法的入栈出栈
- 栈(Stack):每个线程拥有独立的栈,用于存储方法调用的上下文(局部变量、方法参数、返回地址等)。
- 入栈(Push):调用方法时,JVM会创建一个栈帧(Stack Frame)并入栈。
- 出栈(Pop):方法执行完毕后,栈帧被弹出,控制权返回给调用者。
线程的6种状态
-
新建
-
描述:线程创建但未被启动。
-
触发条件:
Thread thread = new Thread(); // 此时状态为 NEW(新建)
-
-
可运行/就绪
-
线程已启动,正在 JVM 中执行或等待 CPU 调度。
-
包含两种情况:
- Ready:等待操作系统分配 CPU 时间片。
- Running:正在执行。
-
触发条件:
thread.start(); // 进入 RUNNABLE 状态
-
-
BLOCKED(阻塞)
- 描述:线程因竞争 同步锁 被阻塞(如
synchronized
)。是被动的。 - 触发条件:
- 线程尝试获取一个已被其他线程持有的锁。
- 描述:线程因竞争 同步锁 被阻塞(如
-
WAITING(无限等待)
- 描述:线程主动进入等待状态,直到被其他线程显式唤醒。
- 触发方法:
Object.wait()
(未指定超时)Thread.join()
(未指定超时)LockSupport.park()
-
TIMED_WAITING(超时等待)
- 描述:线程主动进入限时等待状态,超时后自动唤醒。
- 超时后进入就绪态
- 触发方法:
Thread.sleep(long millis)
Object.wait(long timeout)
Thread.join(long millis)
LockSupport.parkNanos()
-
TERMINATED(终止)
- 描述:线程执行完毕或异常退出。
- 触发条件:
run()
方法执行结束。- 线程抛出未捕获异常。
简述等待队列和同步队列(阻塞队列)
特性 | 等待队列(Wait Queue) | 同步队列(Sync Queue) |
---|---|---|
触发条件 | 调用 Object.wait() | 竞争锁失败(如 synchronized 、lock.lock() ) |
是否释放锁 | 是(wait() 会释放锁) | 否(线程仍持有竞争资格) |
唤醒机制 | 需 notify() /notifyAll() | 锁释放后自动唤醒队头线程 |
典型应用 | 生产者-消费者模型 | ReentrantLock 、synchronized |
线程之间是独立的
- 线程之间是相互独立的,main,t1,t2相互独立,结果会立马输出0,不会管t1,t2线程的任务是否执行完
- 输出结果为0
- 加上t1.join()和t2.join()方法,就会等待t1,t2线程执行完毕再输出,结果将不再是0。由于两个线程之间会相互覆盖,最终结果也会小于2000000。
synchronized
- 加在静态方法上:锁定的是类
- 加在非静态方法:锁定的是方法的调用者,当前实例。
- 修饰代码块:锁定的是传入的对象
静态方法
public class Test {public static void main(String[] args) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {SyncTest.test();}});Thread thread1 = new Thread(new Runnable() {@Overridepublic void run() {SyncTest.test();}});thread.start();thread1.start();}
}
public class SyncTest {public static void test(){System.out.println("进入静态方法锁"+Thread.currentThread().getName());//模拟耗时操作try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("静态方法锁结束"+Thread.currentThread().getName());}
}
结果(执行顺序不定):
非静态方法
两个线程操作不同实例时不会阻塞,操作同一实例时会阻塞。
下面这个例子Thread-2必须等待Thread-0释放锁。
public class Test {public static void main(String[] args) {SyncTest syncTest1 = new SyncTest();SyncTest syncTest2 = new SyncTest();Thread thread = new Thread(new Runnable() {@Overridepublic void run() {syncTest1.test1();}});Thread thread1 = new Thread(new Runnable() {@Overridepublic void run() {syncTest2.test1();}});Thread thread2 = new Thread(new Runnable() {@Overridepublic void run() {syncTest1.test1();}});thread.start();thread1.start();thread2.start();}
}
public synchronized void test1()
{System.out.println("进入同步方法锁"+Thread.currentThread().getName());//模拟耗时操作try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("同步方法锁结束"+Thread.currentThread().getName());
}
输出:
进入同步方法锁Thread-0
进入同步方法锁Thread-1
同步方法锁结束Thread-1
同步方法锁结束Thread-0
进入同步方法锁Thread-2
同步方法锁结束Thread-2
代码块
锁的是传入的对象,两个线程使用不同锁对象时不会阻塞,使用相同锁对象时会阻塞。
下面这个例子Thread-2必须等待Thread-0释放锁。
public class SyncTest {private final Object lock1 = new Object();private final Object lock2 = new Object();public void customLock1() {synchronized (lock1) {System.out.println(Thread.currentThread().getName() + " 进入 lock1 代码块");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 离开 lock1 代码块");}}public void customLock2() {synchronized (lock2) {System.out.println(Thread.currentThread().getName() + " 进入 lock2 代码块");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 离开 lock2 代码块");}}}public class Test {public static void main(String[] args) {SyncTest instance = new SyncTest();Thread thread = new Thread(new Runnable() {@Overridepublic void run() {instance.customLock1();}});Thread thread1 = new Thread(new Runnable() {@Overridepublic void run() {instance.customLock2();}});Thread thread2 = new Thread(new Runnable() {@Overridepublic void run() {//锁住的是同一个对象,会阻塞instance.customLock1();}});thread.start();thread1.start();thread2.start();}
}
输出:
Thread-1 进入 lock2 代码块
Thread-0 进入 lock1 代码块
Thread-0 离开 lock1 代码块
Thread-1 离开 lock2 代码块
Thread-2 进入 lock1 代码块
Thread-2 离开 lock1 代码块