1️⃣ Java 多线程相关文章
-
创建多线程 🚀
- 探讨Java中如何创建和启动多线程。
-
线程方法 🛠️
- 详细介绍Java线程类中的各种方法及其用途。
-
- 了解如何使用Object类中的wait和notify方法来控制线程的执行流程。
-
线程同步 🔄
- 探索Java中不同的线程同步技巧和策略。
-
锁 🔒
- 介绍Java中的锁机制,以及如何使用它来保证线程安全。
-
线程与集合 📊
java.util.concurrent
包提供了一系列线程安全的集合类,这些集合类被设计用于支持高并发操作,而无需使用外部同步。
-
线程的包装 🎁
- 学习如何包装线程以提供更多的功能或修改其行为。
-
并发与响应式编程工具库 ⚡️
- 学习如何包装线程以提供更多的功能或修改其行为。
-
高并发实战 😎
- 上报高并发处理原理与流程
2️⃣ Java内存结构
📝 堆、栈、方法区
🏢 堆区 (Heap)
- 定义:堆是Java用来存储对象实例的内存区域。
- 特点:
- 📦 对象存储:堆存储的全部是对象。每个对象都包含与之对应的类的信息。
- 🌐 共享区域:JVM只有一个堆区,它被所有线程共享。
📚 栈区 (Stack)
- 定义:栈是用来存储局部变量、方法调用等短期存活的数据。
- 特点:
- 🔒 线程私有:每个线程都有一个栈区。栈中保存的是基本数据类型的对象和自定义对象的引用。真正的对象都存放在堆区中。
- 🚫 数据隔离:每个线程的栈中的数据都是私有的,其他线程的栈无法访问。
📘 方法区 (Method Area)
- 定义:方法区用于存储已被加载的类信息、常量、静态变量等数据。
- 特点:
- 🌐 共享区域:方法区,也称为静态区,与堆一样,被所有的线程共享。
- 🌟 唯一元素:方法区包含整个程序中永远唯一的元素,如类信息和静态变量。
🔍 补充说明
- 基本数据类型变量在栈和堆中都存在的原因是Java使用值传递的方式。当我们传递一个基本数据类型的变量时,Java会在栈中创建这个变量的一个副本。而当我们在堆中创建一个对象并赋值给这个变量时,实际上是引用传递,这意味着栈中存储的是对象在堆中的地址。
📝 以其引用的数据类型的不同来划分
🔢 原始数据类型变量
- 特点:
- 变量与数据的分配:对于原始数据类型(如
int
、float
、char
等),其变量分配和数据分配是紧密关联的,都位于相同的内存区域(如方法区、栈内存)。
- 变量与数据的分配:对于原始数据类型(如
🔗 引用数据类型变量
- 特点:
- 变量与数据的分配:对于引用数据类型(如数组、对象等),其变量分配(通常位于栈内存)和数据分配(通常位于堆内存)可能是分开的。
📝 以其作用范围的不同来区分
📌 变量分类
在Java中,变量可以分为三种:局部变量
、实例变量
和静态变量
。其中,实例变量和静态变量统称为成员变量
。
🏢 成员变量
- 定义:定义在类中,可以在整个类中访问。
- 特点:
- 🚀 生命周期:随着类对象的建立而建立,随着对象的消失而消失。
- 🌍 存储位置:存在于对象所在的堆内存中。
- ✨ 初始化值:具有默认初始化值。
📚 局部变量
- 定义:仅在局部范围内定义,例如:方法内、语句块内等。
- 特点:
- 🚀 生命周期:只在所属的区域有效,当区域执行完毕,变量就会消失。
- 🌍 存储位置:存在于栈内存中。
- ❌ 初始化值:不具有默认初始化值,需要手动初始化才能使用。
📝 变量对线程安全的影响
📌 Java 变量分类及其存储位置
- 🌱 实例变量:存放在堆中。
- 🌍 静态变量:位于方法区。
- 📚 局部变量:存在于栈中。
🔍 深入分析
-
局部变量:
- 🚫 线程安全问题:局部变量永远都不会存在线程安全问题。
- 🚧 原因:局部变量不共享。每个线程都有自己的栈,因此局部变量永远不会共享。
-
实例变量 & 静态变量:
- 🌱 实例变量:存在于堆中,而堆是全局唯一的。
- 🌍 静态变量:位于方法区,方法区同样是全局唯一的。
- ⚠️ 线程安全问题:由于堆和方法区都是多线程共享的,实例变量和静态变量可能存在线程安全问题。
🔐 线程安全分析
- 🟢 局部变量 + 常量:不会有线程安全问题。
- 🔴 成员变量:可能会有线程安全问题。
- 🔴 静态变量:可能会有线程安全问题。
3️⃣ 示例代码
📝 定义bean对象
包含常量,静态变量,成员变量
package com.study.notes.threads.safe.variable;
import lombok.Data;
@Data
public class Point {
// 常量(显示初始化,且不能改变)
public static final int P_CONSTANT_INT = 0;
// 静态变量(可以显示初始化)
public static int P_STATIC_INT;
// 成员变量(可以显示初始化)
public int p_member_int;
{
System.out.println("父类-构造代码块初始化");
p_member_int = P_CONSTANT_INT;
P_STATIC_INT = P_CONSTANT_INT;
}
static {
System.out.println("父类-静态代码块初始化");
}
public Point() {
System.out.println("父类-构造方法初始化");
}
}
package com.study.notes.threads.safe.variable;
import lombok.Data;
@Data
public class PointSon extends Point {
// 常量(显示初始化,且不能改变)
public static final int CONSTANT_INT = 0;
// 静态变量(可以显示初始化)
public static int STATIC_INT;
// 成员变量(可以显示初始化)
public int member_int;
{
System.out.println("子类-构造代码块初始化");
member_int = CONSTANT_INT;
STATIC_INT = CONSTANT_INT;
}
static {
System.out.println("子类-静态代码块初始化");
}
public PointSon() {
System.out.println("子类-构造方法初始化");
}
}
📝 线程类,包含上面的bean为成员对象
package com.study.notes.threads.safe.variable;
import lombok.Data;
import lombok.SneakyThrows;
import org.openjdk.jol.vm.VM;
/**
* 每个方法的参数(形参)和方法里面的局部变量都是私有的,跟外界是没有关系的
* 它们在方法调用的时候初始化,在方法调用之后被回收。
* 1. 值传递是实参直接拷贝一份副本出来给形参,副本的改变不会影响到之前的值
* 2. 引用传递是实参将引用的地址传递给形参,这里形参改变就会影响到实参了,因为他们是共享一份值的
**/
@Data
public class MyRun implements Runnable {
public Point member_point;
public MyRun(){ }
public MyRun(Point p) { member_point = p; }
@SneakyThrows
@Override
public void run() {
Integer integerCount = 1;
Point newPoint = null;
for (int i = 0; i < 8; i++) {
Thread.sleep(1000); // 模拟任务执行时间
//常量不能修改,会编译报错,所以他的是线程安全的
//member_point.CONSTANT_INT = 1;
//修改静态变量的值
member_point.STATIC_INT = member_point.STATIC_INT + 1;
//修改成员变量的值
member_point.member_int = member_point.member_int + 1;
//修改局部变量的值
integerCount = integerCount + 1;
newPoint = member_point;
//打印
System.out.println(
"->线程名称:" + Thread.currentThread().getName() +
"->对象地址:" + Long.toHexString(VM.current().addressOf(member_point)) +
"->成员变量 member_int:" + member_point.member_int +
"->静态变量 STATIC_INT:" + member_point.STATIC_INT +
"->局部变量 integerCount:" + integerCount +
"->局部变量 newPoint:" + Long.toHexString(VM.current().addressOf(newPoint))
);
}
}
/**
* 基本数据类型是值传递
**/
public void add1(int x, double y) {
x = 10000;
y = 10000.0;
}
/**
* 基本数据包装类型和String类型是引用传递,
* 但是由于他们的value都是final修饰的,
* 数据一旦写入就无法更改,
* 所以给人的感觉就像值传递
**/
public void add2(Integer x, Double y, String s) {
x = 10000;
y = 10000.0;
s = s + "add";
}
/**
* 自定义数据类型属于引用传递,
* 引用指向的值被修改
**/
public void add3(Point newPoint) {
newPoint.setMember_int(10000);
}
/**
* 自定义数据类型属于引用传递,
* 但是这里引用指向的地址变了,
* 所以值不变
**/
public void add4(Point newPoint) {
newPoint = new Point();
newPoint.setMember_int(10000);
}
}
📝 多个线程操作一个对象
根据结果可以发现成员变量和静态变量都是线程不安全的,但是局部变量是线程安全的,局部变量在方法栈内,且没有接受外界的引用并修改 ,在方法内的局部变量中需要提一下的:
📌 JVM的原子操作定义
JVM规范定义了以下几种原子操作:
-
基本类型赋值:
- 对于基本类型(除long和double之外)的赋值操作是原子的。
int n = m;
- 对于基本类型(除long和double之外)的赋值操作是原子的。
-
引用类型赋值:
- 引用类型的赋值操作也是原子的。
List<String> list = anotherList;
- 引用类型的赋值操作也是原子的。
-
长整型和双精度浮点数:
- 对于64位的long和double,JVM规范没有明确它们的赋值操作是否原子的。但在x64平台的JVM中,这种赋值是作为原子操作来实现的。
🛠️ 原子操作的实际应用
-
单条原子操作:不需要同步。
public void set(int m) { synchronized(lock) { this.value = m; } }
对于上述方法,由于
this.value = m;
是原子操作,因此实际上不需要synchronized
同步。 -
引用赋值:同样,对于引用的赋值,也不需要同步。
public void set(String s) { this.value = s; }
-
多条操作需要同步:当涉及到多个变量的连续读写时,为了确保程序逻辑的正确性,必须使用同步。
class Point { int x; int y; public void set(int x, int y) { synchronized(this) { this.x = x; this.y = y; } } }
在上述例子中,
this.x = x;
和this.y = y;
连续操作,为了确保其原子性,使用了synchronized
进行同步。
package com.study.notes.threads.safe.variable;
public class ThreadSingleDemo {
public static void main(String[] args) {
//创建MyRun对象,表示多线程要执行的任务
Point p1 = new Point();
MyRun mr1 = new MyRun(p1);
//创建线程对象
Thread t1 = new Thread(mr1);
Thread t2 = new Thread(mr1);
//给线程设置名字
t1.setName("1");
t2.setName("2");
//开启线程
t1.start();
t2.start();
}
}
📝 每个线程操作单独的一个对象
每个线程的对象都是不共享的,根据结果可以发现只有静态变量表现为线程不安全的
package com.study.notes.threads.safe.variable;
public class ThreadMultipleDemo {
public static void main(String[] args) {
//创建MyRun对象,表示多线程要执行的任务
Point p1 = new Point();
MyRun mr1 = new MyRun(p1);
Point p2 = new Point();
MyRun mr2 = new MyRun(p2);
//创建线程对象
Thread t1 = new Thread(mr1);
Thread t2 = new Thread(mr2);
//给线程设置名字
t1.setName("1");
t2.setName("2");
//开启线程
t1.start();
t2.start();
}
}
📝 关于值传递和引用传递
- 基本变量类型
在方法中定义的非全局基本数据类型变量的具体内容是存储在栈中的 - 引用变量类型
只要是引用数据类型变量,其具体内容都是存放在堆中的,而栈中存放的是其具体内容所在内存的地址
public class Main{
public static void main(String[] args){
//基本数据类型
int i=1;
double d=1.2;
//引用数据类型
String str="helloworld";
}
}
📌 Java 数据传递方法
-
📦 值传递:
- 定义: 传递对象的一个副本。
- 特点: 传递的是对象的值,即使在方法内部修改了副本,源对象不会受到任何影响。
- 原理: 值传递时,实际上是将实参的值复制一份给形参。
- 适用类型: 原始类型数据,如整型、浮点型、字符型、布尔型。
-
🔗 引用传递:
- 定义: 传递对象的引用,而不是实际的对象。
- 特点: 对方法内部的对象进行的任何修改都会反映在源对象上。
- 原理: 引用传递时,实际上是将实参的地址值复制一份给形参。
- 适用类型: 对象类型,如数组、类、接口。
🔍 在Java中,当我们传递基础数据类型时,实际上是进行值传递,而当我们传递对象时,是进行引用传递。
/**
* 基本数据类型是值传递
**/
public void add1(int x, double y) {
x = 10000;
y = 10000.0;
}
/**
* 基本数据包装类型和String类型是引用传递,
* 但是由于他们的value都是final修饰的,
* 数据一旦写入就无法更改,
* 所以给人的感觉就像值传递
**/
public void add2(Integer x, Double y, String s) {
x = 10000;
y = 10000.0;
s = s + "add";
}
/**
* 自定义数据类型属于引用传递,
* 引用指向的值被修改
**/
public void add3(Point newPoint) {
newPoint.setMember_int(10000);
}
/**
* 自定义数据类型属于引用传递,
* 但是这里引用指向的地址变了,
* 所以值不变
**/
public void add4(Point newPoint) {
newPoint = new Point();
newPoint.setMember_int(10000);
}
package com.study.notes.threads.safe.variable;
/**
* 值传递:
* 传递对象的一个副本,
* 即使副本被改变,
* 也不会影响源对象,
* 因为值传递的时候,
* 实际上是将实参的值复制一份给形参。
*
* 引用传递:
* 传递的并不是实际的对象,
* 而是对象的引用,
* 外部对引用对象的改变也会反映到源对象上,
* 因为引用传递的时候,
* 实际上是将实参的地址值复制一份给形参。
* 说明:对象传递(数组、类、接口)是引用传递,原始类型数据(整形、浮点型、字符型、布尔型)传递是值传递。
*
* @author: lzq
* @create: 2023-07-20 09:42
*/
public class MainDemo {
public static void main(String[] args) {
MyRun myRun = new MyRun();
Point point1 = new Point();
Point point2 = new Point();
int intX = 1;
double doubleY = 1.0;
Integer integerX = 1;
Double doubleEY = 1.0;
String str = "1";
System.out.println("->线程名称:" + Thread.currentThread().getName() +
"-> 改变前 intX:" + intX +
"-> 改变前 doubleY:" + doubleY +
"-> 改变前 integerX:" + integerX +
"-> 改变前 doubleEY:" + doubleEY +
"-> 改变前 str:" + str +
"-> 改变前 point1:" + point1 +
"-> 改变前 point2:" + point2);
myRun.add1(intX,doubleY);
myRun.add2(integerX,doubleEY,str);
myRun.add3(point1);
myRun.add4(point2);
System.out.println("->线程名称:" + Thread.currentThread().getName() +
"-> 改变后 intX:" + intX +
"-> 改变后 doubleY:" + doubleY +
"-> 改变后 integerX:" + integerX +
"-> 改变后 doubleEY:" + doubleEY +
"-> 改变后 str:" + str +
"-> 改变后 point1:" + point1 +
"-> 改变后 point2:" + point2);
}
}
📝 再来一例,方法里面的引用是方法私有的,引用之间的赋值,只是地址传递,只改变地址,可以不会对堆里面的对象有何影响
public static void main(String[] args) {
User user1 = new User("zhangsan",20);
User user2 = new User("lisi",22);
System.out.println("交换前user1:" + user1 + "-》 user2:" + user2);
swap(user1,user2);
System.out.println("交换后user1:" + user1 + "-》 user2:" + user2);
}
private static void swap(User user1, User user2) {
User tmp = v;
user1 = user2;
user2 = tmp;
}
📌 交换结果:
-
交换前:
user1
: name: zhangsan, age: 20user2
: name: lisi, age: 22
-
交换后:
user1
: name: zhangsan, age: 20user2
: name: lisi, age: 22
🔍 结果分析:
当我们尝试在swap
方法中交换user1
和user2
时,我们实际上交换的是方法内部的形参的引用,而不是方法外部的实际参数。因此,方法外部的user1
和user2
引用并没有发生变化。
swap
方法结束后,临时副本user1
和user2
被回收
4️⃣ 总结
📝 1.加载
-
📚 Class 内容区:
- 位置:位于方法区。
- 描述:将编译完成的
.class
文件加载到此区域。
-
🌍 静态区:
- 位置:也位于方法区。
- 描述:存放类的静态成员变量和静态成员方法。
-
🌱 非静态区:
- 位置:同样位于方法区。
- 描述:存放类的非静态变量、非静态方法以及构造方法。
-
🏢 Class对象:
- 位置:存放在堆内存中。
- 描述:当
.class
文件被加载到内存时,JVM会在堆内存中生成一个代表该类的java.lang.Class
对象。此对象作为在方法区中存储的该类各种数据的访问入口。
📝 2.验证
- 格式验证:验证是否符合
class
文件规范
📝 3.准备
- 为类中的所有静态变量分配内存空间,并为其设置一个初始值。
- 被
final
修饰的static
变量 (常量) 会直接赋值;
📝 4.解析
-
🔄 转换符号引用为直接引用:
- 描述: 将常量池中的符号引用转为实际的内存地址引用。这样,JVM可以得到类、字段或方法在内存中的具体位置,从而直接调用它。
- 时机: 该过程可以在类初始化之后进行。
-
📚 建立静态标记:
- 描述: 创建一个标记来指向方法区中的类静态变量的实际地址。所有这个类的对象共享同一个静态变量。
-
🌍 建立方法标记:
- 描述: 创建一个标记指向这个对象所在类的方法在方法区的实际地址。
-
🔒 解析静态绑定内容:
- 描述: 解析那些不会被重写的方法和域。这些方法和域会被静态绑定,即在编译时就确定了它们的实际地址。
🔍 总结:
- 阶段 2、3 和 4 被统称为链接阶段。
- 链接阶段的主要任务是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中。
📝 5.初始化(先父后子)
-
🔄 为静态变量赋初值:
- 描述: 默认为
null
或0
。
- 描述: 默认为
-
📚 执行静态代码块:
- 描述: 静态代码块只有JVM能够调用,并且只会执行一次。
- 随着类的加载而执行,且优先于构造器和普通代码块。
- 可用于初始化类的静态变量或执行一次性的操作,如加载驱动、注册监听器等。
- 不能访问非静态成员。
- 如果有多个静态代码块,它们会按声明的顺序执行。
- 描述: 静态代码块只有JVM能够调用,并且只会执行一次。
-
🌍 创建对象:
- 描述: 对象的创建过程如下:
- 在堆内存中为对象分配空间。这包括本类和所有父类的实例变量,但不包括静态变量。
- 为所有实例变量赋默认值(
null
或0
)。 - 执行实例初始化代码。初始化时,首先初始化父类,然后初始化子类。执行顺序是先执行构造代码块,然后执行构造方法。
- 描述: 对象的创建过程如下:
📝 6.调用方法
📌 对象方法调用:
- 当对象调用方法时,系统首先根据方法标记在方法区中找到对应的方法。
- 随后,该方法入栈。当方法执行完毕,它以及它的局部变量会被清除。
🔍 特例:
- 例如,考虑 PointSon point1 = new PointSon();
:
- 在栈区定义了 PointSon
类型的引用变量 point1
。
- 然后系统会将堆区中的对象地址赋给 point1
。
🛡️ 基础数据类型在栈中:
- 栈内的基础数据类型为私有的。
- 这些数据的值变化不会影响到外部。
- 它们会在方法出栈时随之消失。
myRun.add1(intX,doubleY);
// 基本数据类型采用值传递。
public void add1(int x, double y) {
x = 10000;
y = 10000.0;
}
🎁 基本数据包装类型与String:
- 对于这些类型,首先会根据栈内的引用变量找到堆内存中的对象。
- 然后,系统会修改堆内存中的值。
- 但是,因为它们的
value
被final
修饰,所以值不会被修改。
myRun.add2(integerX,doubleEY,str);
// 虽然它们是引用传递,但因为value是final的,所以看起来像值传递。
public void add2(Integer x, Double y, String s) {
x = 10000;
y = 10000.0;
s = s + "add";
}
💼 自定义数据类型:
- 对于自定义的数据类型,系统会首先根据栈内的引用变量找到堆内存中的对象。
- 然后,系统会修改堆内存中的值。
myRun.add3(point1);
// 自定义数据类型是引用传递,引用的值会被修改。
public void add3(PointSon newPoint) {
newPoint.setMember_int(10000);
}
- 但是,如果引用地址发生变化,则原始值不会被修改。
myRun.add4(point2);
// 引用地址改变了,所以值保持不变。
public void add4(PointSon newPoint) {
newPoint = new PointSon();
newPoint.setMember_int(10000);
}