反射,锁与线程

本文最后更新于 2025年8月5日 凌晨

类与对象的建构

在Java中对类进行的查询主要通过SystemDictionary 管理。当需要的类已经被加载了或者通过Class对象查询目标Class对象时,通过SystemDictionary 使用”类名+类加载器”作为类的唯一标识查询底层的原类型指针获取到原类型。

1
2
3
4
5
Class.forName("Person")         

SystemDictionary 哈希查找
Key: (类名 + 类加载器)
Value: Klass* 指针

对于对象来说,所有的对象创建过程都是存在一个创建顺序的,所以其上层的对象必然存储了对它的引用。而对已经被废弃的对象来说,GC通过图遍历完成对所有存活对象的可达性访问,另一方面又通过堆对所有对象的管理来进行遍历堆检查所有对象是否存活。所以对于对象的管理来说,使用性能开销较大的查询方式不如由对象进行相互的引用管理来的直接高效。

类的形式

因为 Java是解释语言,所以它的类以三种形式互相关联,完成不同的功能

  1. 由类创建的实例对象。用于在内存中存储实例数据
  2. 提供给编程人员访问的类对象。以Class对象为例,主要是提供了一个接口用于对运行时的实际元类对象进行访问
  3. 最底层由C++定义的实际上的原类对象InstanceClass 存储了编码中定义的类的完整信息

关系

1
2
3
4
5
编译期Java源码 → 字节码文件(.class)

运行期类加载 → InstanceKlass(C++元类对象) ←→ Class对象(Java镜像)

对象实例化 → 具体Java对象实例

另一方面,根据类所能访问到的资源的不同,还可以分为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
模式1: 纯Java对象 (Pure Java Objects)
├── 完全通过InstanceKlass创建
├── 所有字段都在Java堆中
└── 例子:普通业务对象、集合类

模式2: Java包装器对象 (Java Wrapper Objects)
├── Java对象通过InstanceKlass创建
├── 内部持有native资源的句柄/指针
└── 例子:Thread、Socket、File

模式3: JVM内部对象 (JVM Internal Objects)
├── 完全在C++层创建和管理
├── Java层不可见或只能通过特殊API访问
└── 例子:ObjectMonitor、Parker、GC对象

纯java对象就是平时创建的基本类,定义了一些基本类型和对象信息,它所引用的资源都在堆上创建

1
2
3
ObjectA
Refer Object B ---------> Object B
Refer Object C --------------> Object C

而对于java包装器对象,它的作用是用于在定义时,用户有一个可以操作的目标对象,而这个对象在内存中创建后,调用方法来创建底层资源(创建时机视资源类型而定),在应用与底层中间管理这些资源

1
2
3
4
5
6
7
ObjectA
Refer Object B ---------> ObjectB(存储调用底层资源创建方法的参数)
|
|
|
v
创建资源最终返回对象引用给ObjectB

如 socket 的实际创建过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Java层:通过InstanceKlass创建Java对象
Socket socket = new Socket(); // 正常的Java对象创建流程

// Socket.java内部结构
public class Socket {
SocketImpl impl; // Java对象引用
// ... 其他Java字段
}

// SocketImpl内部持有native句柄
class PlainSocketImpl extends AbstractPlainSocketImpl {
int fd = -1; // 文件描述符 - 这是关键!
InetAddress address; // Java对象
int port; // Java字段
int localport; // Java字段
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1. Java对象创建 - 正常InstanceKlass流程
Socket socket = new Socket();
// ↓ JVM执行new指令
// ↓ 查询Socket的InstanceKlass
// ↓ 分配Java堆内存
// ↓ 初始化Java字段为默认值

// 2. Native资源创建 - 在需要时触发
socket.connect(address, port);
// ↓ 调用native方法
// ↓ 在C++层创建系统socket
// ↓ 将文件描述符存储到Java对象的fd字段

// C++实现
JNIEXPORT void JNICALL
Java_java_net_PlainSocketImpl_socketCreate(JNIEnv *env, jobject this, jboolean stream) {
// 创建系统socket - 这是原生C++创建!
int fd = socket(AF_INET, stream ? SOCK_STREAM : SOCK_DGRAM, 0);

// 将句柄存储到Java对象中
(*env)->SetIntField(env, this, psi_fdID, fd); // fd字段赋值
}

对于JVM内部对象,大部分时间直接对上述两种对象进行管理,如monitor

1
2
3
4
5
6
7
8
9
ObjectA.synchronized_method()

JVM检查ObjectA的对象头

如果无锁 → 尝试轻量级锁(栈上锁记录)

如果竞争 → 创建ObjectMonitor对象 → 关联到ObjectA的mark word

线程进入Monitor的等待队列或获得锁继续执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// ObjectMonitor - 完全C++原生创建
class ObjectMonitor {
volatile markOop _header; // 对象头备份
void* volatile _object; // 关联的Java对象
void* volatile _owner; // 锁拥有者线程
volatile jlong _previous_owner_tid;
volatile intptr_t _recursions; // 重入次数
ObjectWaiter* volatile _cxq; // 竞争队列
ParkEvent* _event; // 阻塞事件

// 构造函数 - 原生创建
ObjectMonitor() {
_header = NULL;
_count = 0;
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_cxq = NULL;
_event = new ParkEvent(); // 创建另一个C++对象
}
};

// 获取ObjectMonitor的过程
ObjectMonitor* ObjectSynchronizer::inflate(Thread* Self, oop object) {
// 不通过InstanceKlass,直接C++分配!
ObjectMonitor* m = omAlloc(Self); // 从对象池分配

// 设置关联
m->set_object(object);
m->set_owner(NULL);

// 通过CAS设置到对象头
markOop cmp = (markOop) Atomic::cmpxchg_ptr(markOopDesc::encode(m),
object->mark_addr(), mark);
return m;
}

反射

基于类的多重实现,我们可以看到反射本身通过接受一个java层的类对象,根据类对象中的指针查询真正的原类信息对象结构的执行器。

其工作过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 三种方式获取Class对象
Class<?> clazz1 = Class.forName("com.example.MyClass");
1. 类加载器查找类文件
2. 读取字节码文件
3. 验证字节码合法性
4. 准备阶段:为静态变量分配内存
5. 解析阶段:符号引用转换为直接引用
6. 初始化阶段:执行静态代码块和静态变量初始化
7. 返回Class对象
Class<?> clazz2 = MyClass.class;
Class<?> clazz3 = new MyClass().getClass();

// 获取构造器
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true); // 如果是私有构造器

// 创建实例
Object instance = constructor.newInstance("参数1", 123);
分配内存空间
调用构造函数
初始化对象头信息
返回对象引用
// 获取方法
Method method = clazz.getDeclaredMethod("methodName", parameterTypes);
method.setAccessible(true); // 如果是私有方法

// 调用方法
Object result = method.invoke(instance, args);
// 调用过程
public Object invoke(Object obj, Object... args) {
// 1. 访问权限检查
if (!override && !Reflection.quickCheckMemberAccess(clazz, modifiers)) {
checkAccess();
}

// 2. 参数校验和装箱
Object[] argsArray = checkAndConvertArgs(args);

// 3. 委托给MethodAccessor
return methodAccessor.invoke(obj, argsArray);
}

// 前15次调用使用native方法
class NativeMethodAccessorImpl extends MethodAccessorImpl {
public Object invoke(Object obj, Object[] args) {
return invoke0(method, obj, args); // native方法
}
}

// 第16次调用后生成专门的字节码类
class GeneratedMethodAccessor1 extends MethodAccessorImpl {
public Object invoke(Object obj, Object[] args) {
// 直接调用目标方法,避免native调用开销
return ((TargetClass)obj).targetMethod((String)args[0]);
}
}

用户代码

Method.invoke()

权限检查 & 参数验证

MethodAccessor.invoke()

┌─────────────────┬─────────────────┐
│ 前15次调用 │ 第16次开始 │
│ ↓ │ ↓ │
│ Native调用 │ 字节码生成调用 │
│ ↓ │ ↓ │
│ JNI → C++ │ 直接方法调用 │
│ ↓ │ │
│ 目标方法 │ 目标方法 │
└─────────────────┴─────────────────┘

在其他内容中已经讨论过临界区与锁的使用。这里主要讨论锁的设计以及锁的关联关系

volatile关键字

在java 中 volatile 具有两个功能

  1. 可见性

被volatile修饰的对象,在获取时,会先刷新缓存,获取最新的对象状态信息,在写入完毕后会所有工作缓存信息。即可见性,意味着对该对象操作会立即得到更新。但是不能保证原子操作因为一个执行过程并不一定只对volatile修饰的对象产生影响

  1. 有序性

通过volatile关键字提供了内存屏障,所有在volatile 执行前的指令必须在volatile前执行完毕,所有在volatile执行后执行的指令必须等待volatile执行后执行。

通过对象头中markword字段完成的资源控制

定义在对象头部的markword 字段是一个动态结构,用于描述当前对象的访问状态。

存储内容 标志位 状态
对象哈希码,对象分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 膨胀
一般被设置为非法模式或者转发指针 11 GC标记
偏向线程ID,偏向事件戳,对象分代年龄 01 可偏向

当对象被创建后,markword中 的标志位为01 , 这个时候是未被锁定的状态。

当对象被标记为(synchronized)后,markword中的标志位会被重新设置,用于存储当前请求该对象的线程ID。

处于偏向锁状态下的对象,会在被使用时只判断调用线程是否和存储的ID一致,如果一致则可以直接获取对象,不需要再进行其他检查。

当该对象再次被其他线程请求后,其他线程会先尝试设置线程ID信息,若设置失败,则目标对象的markword会升级为轻量级锁定,这时会在栈空间设置一条锁记录,将markword设置为指向锁记录。这样当其他线程请求时,同样会设置一条锁记录,并尝试对该对象进行CAS操作(向markword中尝试将记录替换为自己的锁记录)

当该对象被持续请求到阈值时,或者存在多个线程请求该对象时,则将轻量级锁升级为重量级锁,在堆中创建Objectmonitor对象,这样后续线程请求对象时,会通过markword查询Objectmonitor,如果当前的Objectmontior属于其他线程,则将请求线程加入等待队列。阻塞等待。

ReentrantLock

ReentrantLock 锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Java对象创建 - 完全通过InstanceKlass
ReentrantLock lock = new ReentrantLock();

// ReentrantLock内部结构 - 都是Java对象
public class ReentrantLock implements Lock {
private final Sync sync; // Java对象

static abstract class Sync extends AbstractQueuedSynchronizer {
// 继承AQS的state字段和队列
}
}

// AbstractQueuedSynchronizer - 纯Java实现
public abstract class AbstractQueuedSynchronizer {
private volatile int state; // Java int字段 - 锁状态!
private transient volatile Node head; // Java对象引用 - 等待队列
private transient volatile Node tail; // Java对象引用
}
  • 可重入性 :同一线程可以多次获取同一把锁
  • 可中断性 :支持响应中断
  • 可超时性 :支持尝试获取锁的超时机制
  • 公平性 :支持公平锁和非公平锁
  • 条件变量 :支持多个条件等待
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.concurrent.locks.ReentrantLock;

public class BasicExample {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;

public void increment() {
lock.lock(); // 获取锁
try {
count++; // 临界区代码
} finally {
lock.unlock(); // 释放锁(必须在finally块中)
}
}
}

可重入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ReentrantExample {
private final ReentrantLock lock = new ReentrantLock();

public void outerMethod() {
lock.lock();
try {
System.out.println("外层方法");
innerMethod(); // 同一线程再次获取锁
} finally {
lock.unlock();
}
}

public void innerMethod() {
lock.lock(); // 重入成功,计数器+1
try {
System.out.println("内层方法");
} finally {
lock.unlock(); // 计数器-1
}
}
}

可中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class InterruptibleExample {
private final ReentrantLock lock = new ReentrantLock();

public void interruptibleTask() throws InterruptedException {
lock.lockInterruptibly(); // 可被中断的锁获取
try {
// 长时间运行的任务
Thread.sleep(10000);
} finally {
lock.unlock();
}
}

public void testInterrupt() {
Thread t1 = new Thread(() -> {
try {
interruptibleTask();
} catch (InterruptedException e) {
System.out.println("线程被中断了");
}
});

t1.start();
// 1秒后中断线程
try {
Thread.sleep(1000);
t1.interrupt(); // 中断等待锁的线程
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

尝试获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.util.concurrent.TimeUnit;

public class TryLockExample {
private final ReentrantLock lock = new ReentrantLock();

public boolean tryDoSomething() {
// 尝试获取锁,最多等待3秒
try {
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑
return true;
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁超时");
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}

public boolean tryImmediately() {
// 立即尝试获取锁,不等待
if (lock.tryLock()) {
try {
// 执行业务逻辑
return true;
} finally {
lock.unlock();
}
} else {
System.out.println("锁被其他线程持有");
return false;
}
}
}

公平锁与非公平锁

非公平锁

  • 新来的线程可能直接获取锁,无需排队
  • 性能更好 ,吞吐量更高
  • 可能导致某些线程长时间等待(饥饿现象)
1
2
3
4
// 默认创建非公平锁
ReentrantLock unfairLock = new ReentrantLock();
// 或显式指定
ReentrantLock unfairLock2 = new ReentrantLock(false);

公平锁

  • 严格按照请求顺序分配锁
  • 避免线程饥饿
  • 性能较低 ,因为需要维护队列
1
2
// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true);

其他层面上的锁区分

悲观锁与乐观锁

悲观锁

悲观锁优先控制临界区资源,并在处理过程中一直保持锁定状态,等到线程释放锁后其他线程才能获取。

乐观锁

乐观锁默认其他线程不会对资源进行修改,只需要对比版本号变动来决定自己的行为

公平锁与非公平锁

公平锁

公平锁会在线程请求锁资源时,将线程直接加入等待队列。优点是线程不会被饿死,缺点是整体效率较低,因为当执行速度较慢的线程在执行时,其他线程必须等待,导致线程堆积

非公平锁

非公平锁则在线程请求锁资源时,先去尝试获取所所资源,如果未获取到锁,则加入等待队列等待。优点是整体执行效率较高,缺点是可能会出现因为一个线程一直获取不到锁资源导致,线程饥饿的问题

可重入锁与不可重入锁

可重入锁

可重入锁,指同一个线程可以多次占用同一个锁,在解锁是需要相同次数的解锁

不可重入锁

不可重入锁,指线程只能获取一次锁

读/写锁

读锁

能够对共享资源进行读操作

写作

只有一个线程允许进行写入

自旋锁

自旋锁指某个线程没有获取到锁时,不会进入阻塞状态,而是不断尝试获取锁,直到释放锁。

优点是 对于可以快速执行的任务,能够提高效率,因为不需要进入阻塞状态

缺点是 对于执行效率较低的任务,持续的请求会白白占用CPU资源

死锁问题

出现原因

  1. 互斥条件

标识一段时间内,某个或某些资源只能被一个线程占有,此时如果有其他线程需要访问,则只能等待

  1. 不可剥夺条件

不可剥夺条件表示线程所有的资源在使用完毕之前,不能被其他线程强行夺走,只能由获取到资源的线程主动释放

  1. 请求与保持条件

请求与保持条件表示当线程占有至少一个资源,又需要抢占新的资源,而需要抢占的资源已经被其他线程占有时,需要抢占新资源的线程被阻塞,但是又不会释放其已占的资源

  1. 循环等待条件

循环等待条件表示发生死锁时,必然存在一个线程与资源的循环等待链


反射,锁与线程
http://gadoid.io/2025/08/05/反射-锁与线程/
作者
Codfish
发布于
2025年8月5日
许可协议