一、Thread 类
作用:Thread 类代表一个线程,用于创建和控制一个新的执行流(即“子线程”)。
定义:java.lang.Thread 类实现了 Runnable 接口。
1.1 使用方式
方法一:继承 Thread 类
步骤:
-
自定义类继承
Thread。 -
重写
run()方法。 -
创建线程对象并调用
start()方法。
示例代码:
class MyThread extends Thread {@Overridepublic void run() {System.out.println("线程运行:" + Thread.currentThread().getName());}
}public class Demo {public static void main(String[] args) {MyThread t1 = new MyThread();t1.start(); // 启动线程,会自动调用 run()}
}
注意:不要直接调用 run(),否则不会启动新线程,只是普通方法调用。
方法二:传入 Runnable 对象
优点:
-
避免单继承限制。
-
将“线程”与“任务”解耦,更符合职责分离。
示例代码:
class MyRunnable implements Runnable {public void run() {System.out.println("Runnable 线程执行:" + Thread.currentThread().getName());}
}public class Demo {public static void main(String[] args) {Thread thread = new Thread(new MyRunnable());thread.start();}
}
1.2 Thread 主要方法详解
| 方法名 | 作用 |
|---|---|
start() | 启动线程,执行 run(),异步执行 |
run() | 线程实际要做的事情,用户重写 |
sleep(long millis) | 让当前线程休眠指定时间,不释放锁 |
join() | 等待其他线程执行完再继续当前线程 |
interrupt() | 中断线程(设置中断标志) |
isInterrupted() | 判断是否被中断 |
setName(String name) | 设置线程名 |
getName() | 获取线程名 |
setPriority(int) | 设置优先级(1~10) |
setDaemon(true) | 设置为守护线程(随主线程退出) |
1.3 线程状态(Thread State)
Java 中线程有 6 种状态,可通过 getState() 查看:
| 状态 | 说明 |
|---|---|
| NEW | 线程已创建但未启动 |
| RUNNABLE | 正在运行或准备运行 |
| BLOCKED | 等待获取锁 |
| WAITING | 等待 wait()、join() |
| TIMED_WAITING | sleep()、wait(timeout) |
| TERMINATED | 执行完毕或异常退出 |
1.4 线程执行顺序 & 异步特性
start() 和 run() 区别:
Thread t = new MyThread();
t.start(); // 开启新线程,异步执行 run()
t.run(); // 同步调用 run(),没有新线程
1.5 Thread 在逆向中的使用场景
在逆向时,尤其是 Android APP 里,经常用到线程来执行异步任务,比如后台加密、网络请求、日志上传等。
在逆向时识别 Thread 的方法:
- 查看 smali / Java 代码中是否出现:
new Thread(...).start()
- 找到匿名内部类或 lambda 表达式中
run()方法:
new Thread(() -> {// 加密、上传、收集信息等
}).start();
- 动态分析时 Hook
Thread.start()或Thread.run(),可以获得关键的“异步行为入口”。
1.6 线程 vs 进程
| 对比项 | 线程(Thread) | 进程(Process) |
|---|---|---|
| 定义 | 程序执行中的一个执行流 | 程序运行中的一个实例 |
| 开销 | 小 | 大 |
| 通信 | 共享内存(易出错) | IPC 机制 |
| 创建速度 | 快 | 慢 |
| 应用场景 | 高并发任务、异步操作 | 系统级隔离、多任务 |
1.7 设置守护线程 + 自定义名称
Thread t = new Thread(() -> {System.out.println("守护线程");
});
t.setDaemon(true); // 设置为守护线程(主线程退出,它也退出)
t.setName("EncryptThread"); // 设置线程名称,方便调试和逆向时识别
t.start();
1.8 小结
| 核心点 | 内容 |
|---|---|
| 创建线程方式 | 继承 Thread、实现 Runnable |
| 启动线程 | 用 start(),不能直接 run() |
| 核心方法 | sleep, join, interrupt 等 |
| 状态管理 | Thread.State、isAlive() |
| 逆向重点关注点 | start()、匿名线程、线程任务中的加密或网络请求 |
| 动态分析策略 | Hook start() 和 run(),查看异步行为 |
二、Runnable 接口
作用:Runnable 是 Java 中的一个函数式接口,用于定义线程执行的任务。
它只包含一个方法:
public interface Runnable {void run();
}
2.1 与 Thread 类的关系
Thread 类实际上是实现了 Runnable 接口的。
public class Thread implements Runnable {private Runnable target;public Thread(Runnable target) {this.target = target;}public void run() {if (target != null) {target.run(); // 执行用户传入的任务}}
}
本质上:创建线程时传入一个 Runnable,实际 Thread.start() 会触发 Thread.run(),进而调用传入的 target.run()。
2.2 使用方式
方式一:创建实现类
class MyTask implements Runnable {public void run() {System.out.println("任务开始执行!");}
}public class Demo {public static void main(String[] args) {Thread thread = new Thread(new MyTask());thread.start();}
}
方式二:使用匿名内部类
Thread thread = new Thread(new Runnable() {public void run() {System.out.println("匿名任务执行");}
});
thread.start();
方式三:使用 Lambda 表达式(Java 8+)
Thread thread = new Thread(() -> {System.out.println("Lambda 任务执行");
});
thread.start();
2.3 与继承 Thread 的对比
| 特点 | 实现 Runnable | 继承 Thread |
|---|---|---|
| 是否支持多继承 | 支持(更灵活) | 不支持(Java 单继承限制) |
| 任务与线程是否解耦 | 解耦(任务是独立的) | 耦合(任务写在线程类中) |
| 推荐程度 | 推荐(现代通用方式) | 只在需要自定义线程行为时使用 |
2.4 源码调用链解析
Runnable task = new MyRunnable();
Thread thread = new Thread(task);
thread.start();
执行顺序如下:
-
thread.start()—— 创建新线程。 -
新线程调用
Thread.run()。 -
Thread.run()中调用了target.run()(就是你传的Runnable)。 -
执行你在
Runnable.run()中写的业务逻辑。
2.5 实际应用场景
分离任务逻辑与线程控制
Runnable readFileTask = () -> {// 读取文件
};
Runnable networkTask = () -> {// 网络请求
};new Thread(readFileTask).start();
new Thread(networkTask).start();
配合线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.execute(() -> {System.out.println("线程池中的任务");
});
用于回调、事件处理
很多异步框架如 RxJava、Netty、Android 中,都会用 Runnable 传递任务。
2.6 逆向时的线索与识别
在逆向工程中,Runnable 是“异步逻辑”的典型线索。
能在哪里看到 Runnable?
smali 中:
new-instance v0, Ljava/lang/Thread;
invoke-direct {v0, new Runnable$1()}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V
Java 中:
new Thread(new Runnable() {public void run() {// 异步逻辑,比如加密、网络请求}
}).start();
动态 Hook(Frida)时:
可以 Hook 这些关键点:
-
Thread.start():打印Thread.currentThread().getName()+ 调用栈。 -
Runnable.run():Hook 各类实现类,抓异步行为起点。
常见逆向场景:
| 线索 | 含义 |
|---|---|
Runnable.run() 中有加密函数 | 说明可能是数据加密任务 |
Runnable.run() 发起网络请求 | 上传设备信息/心跳包等异步逻辑 |
Thread 调用异步 Runnable | 后台异步任务起点(如埋点统计) |
2.7 常见问题整理
直接调用 run() 行不行?
可以运行,但不会创建新线程,代码仍在主线程中执行。
Runnable r = () -> System.out.println(Thread.currentThread().getName());
r.run(); // 主线程执行
new Thread(r).start(); // 新线程执行
Runnable 有没有返回值?
没有!如果需要返回值,可用 Callable<T> 接口配合 Future<T> 使用。
2.8 小结
| 项目 | 内容 |
|---|---|
| 定义 | 表示可执行的任务,定义在 Runnable.run() 中 |
| 与 Thread 关系 | Thread 实现了 Runnable,可以组合使用 |
| 使用方式 | 1)实现类 2)匿名内部类 3)Lambda 表达式 |
| 推荐原因 | 避免继承限制、解耦任务与线程、可重用性强 |
| 逆向分析线索 | 在 Runnable.run() 中找异步逻辑,如加密、网络、日志 |
| 动态分析手段 | Hook Thread.start() + 拿到 Runnable 对象和堆栈 |
三、synchronized
synchronized 是 Java 提供的 内置锁(monitor lock) 机制。
目的:当多个线程访问共享资源(变量、方法、对象)时,防止出现 并发冲突 或 数据不一致 的问题。
3.1 synchronized 的三种使用方式
1)同步代码块(推荐)
synchronized (lockObject) {// 临界区代码:同一时刻只有一个线程能进入
}
lockObject 是锁对象,必须是引用类型(不能是 null)。
2)同步实例方法
public synchronized void doWork() {// 相当于 synchronized(this)
}
每个实例对象有一个锁,不同实例互不影响。
3)同步静态方法
public static synchronized void staticWork() {// 相当于 synchronized(ClassName.class)
}
类锁,对类级别的资源加锁(所有对象共用一把锁)。
3.2 锁对象与锁粒度
| 类型 | 锁对象 | 应用场景 |
|---|---|---|
| 同步代码块 | 显式指定对象 | 推荐:灵活控制锁粒度、避免死锁 |
| 实例方法 | this | 同步访问对象级别的成员变量 |
| 静态方法 | ClassName.class | 同步访问类级别的静态变量 |
注意:如果两个线程用的不是同一个锁对象,那么它们是不会互斥的。
3.3 底层原理
synchronized 的实现机制:
-
JVM 会为每个对象维护一个 Monitor(监视器)。
-
每次进入同步块或方法时,线程尝试获取对象的 Monitor 锁。
-
进入后会将该锁标记为“占用”,其他线程进入时会被阻塞。
-
退出同步块或方法后,释放锁。
3.4 可重入锁(Reentrant Lock)
synchronized 是 可重入的,即同一线程可以重复获得同一个锁不会阻塞自己。
public synchronized void a() {b(); // 同一线程再次进入锁
}public synchronized void b() {// 可执行
}
3.5 可见性与原子性
-
原子性(Atomicity): 进入
synchronized的临界区代码,只能被一个线程执行,确保操作不可分割。 -
可见性(Visibility): 一个线程对变量的修改,对其他线程可见(解锁时会强制刷回主内存)。
3.6 synchronized 与死锁
如果两个线程持有彼此需要的锁,且不释放,就可能产生死锁。
示例:
synchronized (A) {synchronized (B) {// do something}
}
如果另一个线程是:
synchronized (B) {synchronized (A) {// do something}
}
两个线程互相等待就会死锁。
实战建议:
-
尽量按固定顺序加锁。
-
推荐使用细粒度锁或显式锁(如
ReentrantLock)处理复杂场景。
3.7 synchronized VS ReentrantLock
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 写法简单 | ✔ | ✘(需要 try-finally) |
| 可重入性 | ✔ | ✔ |
| 中断响应锁获取 | ✘ | ✔(lockInterruptibly) |
| 公平锁 | ✘(非公平) | ✔(可设为公平锁) |
| 超时获取锁 | ✘ | ✔(tryLock) |
| 条件变量 | ✘ | ✔(newCondition) |
3.8 逆向工程中如何识别 synchronized
在 Java / Smali 层的表现:
Java:
synchronized (this) {// 异步加密、缓存写入、日志处理、文件操作等常见目标
}
smali 反编译中:
monitor-enter v0 # 加锁
... # 临界区代码
monitor-exit v0 # 解锁
线索:
-
多线程竞争资源时使用了
monitor-enter。 -
在 Hook 分析或 Frida 动态调试时关注这个锁作用范围。
3.9 典型应用场景
| 应用场景 | 原因 |
|---|---|
| 单例模式(懒汉式) | 保证多线程只创建一个实例 |
| 缓存访问 | 读写共享缓存必须加锁 |
| 日志系统 | 多线程同时写文件需同步 |
| 并发计数器 | 保证 ++count 操作的原子性 |
| Android 加密模块 | 共享密钥加解密需同步 |
3.10 实践建议
-
只锁必要代码块,减小锁的范围,提高性能。
-
避免锁对象为
this或全局变量,防止外部代码误用锁。 -
多线程下需要 共享的资源才需要加锁,不共享就不必同步。
-
能用
ConcurrentHashMap、AtomicXXX替代锁的场景,尽量不用锁。
3.11 小结
| 项目 | 说明 |
|---|---|
| 本质 | 基于 JVM Monitor 的内置锁 |
| 用法 | 代码块、实例方法、静态方法 |
| 锁对象 | 任意非 null 对象 |
| 是否可重入 | 是 |
| 是否可见性/原子性 | 有可见性 有原子性 |
| 是否阻塞其他线程 | 是 |
| 逆向识别点 | monitor-enter/exit,配合分析共享资源逻辑 |
四、volatile
定义:volatile 是 Java 提供的轻量级同步机制,用于修饰变量,保证线程间的 可见性 和一定程度的 有序性。
4.1 volatile 解决了什么问题?
背景:在 Java 中,每个线程有自己的工作内存(CPU 缓存),线程可能不会直接从主内存读取变量的值。
这就会导致:
// 主线程设置 running = false
// 工作线程却还在执行 while(running)
volatile 的作用:
-
可见性:一个线程修改变量,其他线程能立即看到。
-
禁止指令重排(有序性):保证写操作发生在读操作之前。
4.2 volatile 的核心特性
| 特性 | 是否支持 | 说明 |
|---|---|---|
| 可见性 | ✔ | 主内存值修改后立即通知其他线程 |
| 有序性 | ✔ | 禁止与 volatile 相关操作的重排序 |
| 原子性 | ✘ | 不支持复合操作(如 i++) |
4.3 volatile 示例分析
示例一:线程终止标志
public class MyThread extends Thread {private volatile boolean running = true;public void run() {while (running) {// do something}System.out.println("线程退出");}public void stopRunning() {running = false;}
}
如果没有 volatile:
-
running=false只修改了主内存; -
子线程仍从 CPU 缓存中读旧值,导致死循环。
4.4 volatile vs synchronized 对比
| 特性 | volatile | synchronized |
|---|---|---|
| 原子性 | 不保证 | 保证 |
| 可见性 | 保证 | 保证 |
| 是否加锁 | 无锁 | 加锁(性能低) |
| 性能 | 高(无阻塞) | 低(涉及上下文切换) |
| 使用难度 | 中等(易误用) | 简单 |
4.5 volatile 不能做什么?
示例:i++ 不是原子操作
volatile int count = 0;public void add() {count++; // 实际上是:读取 -> 加一 -> 写入(非原子)
}
多线程下可能造成丢数据!
正确方式:使用 AtomicInteger 或 synchronized:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
4.6 适合 volatile 的场景
| 场景类型 | 示例说明 |
|---|---|
| 状态标志 | 控制线程退出、任务是否开始等 |
| 配置信息 | 热更新配置字段 |
| 单例双重检查锁 DCL | 保证初始化顺序和可见性 |
4.7 volatile 内存语义和 happens-before
在 JMM(Java 内存模型)中,volatile 有如下语义:
-
写操作: 把当前线程的工作内存变量刷新到主内存;
-
读操作: 清空当前线程的工作内存,并从主内存读取最新值。
happens-before 原则:
| 原则 | 含义 |
|---|---|
对一个 volatile 变量的写操作先行发生于后续对该变量的读操作 | 保证有序 & 可见 |
4.8 volatile 的底层实现机制(JVM 层)
字节码层面
使用 volatile 修饰字段后,JVM 字节码会加入:
| 字节码指令 | 说明 |
|---|---|
volatile-read | 加入 loadload 屏障 |
volatile-write | 加入 storestore + storeload 屏障 |
CPU 层面
-
x86 平台上,
volatile使用lock前缀的汇编指令,如lock xadd。 -
保证:
-
内存写入顺序
-
刷新缓存行
-
让其他 CPU 核读取主存数据
-
4.9 volatile 在逆向中的识别方式
常见用法线索:
| 类型 | 表现形式 |
|---|---|
| 控制标志变量 | volatile boolean flag、volatile int status 等 |
| 线程通信变量 | 某变量决定线程是否继续、是否初始化完成 |
| 双重检查单例 | if (instance == null) 的场景中用 volatile 修饰 instance |
逆向定位技巧
1)使用 jadx、Fernflower 查看 Java 源码:
private static volatile Config config;
2)在 smali 中没有明确 volatile 关键词,但你可以结合使用场景判断:
-
变量只写一次,但被多个线程读;
-
变量与线程循环、状态切换有关;
-
多线程之间通过某字段控制行为;
3)动态 Hook volatile 变量的 getter/setter,可识别线程状态切换时机。
4.10 常见误区
| 错误理解 | 正确认知 |
|---|---|
volatile 保证线程安全 | 仅保证可见性,不保证原子性 |
用 volatile 修饰的变量可以替代锁 | 仅在极少数“写少读多”的场景可以 |
多线程访问共享变量,加上 volatile 就万无一失了 | 对于复合操作,仍需 synchronized 或 CAS |
4.11 小结
| 特性 | volatile |
|---|---|
| 保证可见性 | ✔ |
| 保证有序性 | ✔(部分) |
| 保证原子性 | ✘ |
| 是否加锁 | ✘ |
| 使用场景 | 状态标志、配置信息、单例懒加载 |
| 不适用场景 | i++、count++、多个字段一起更新 |
五、wait/notify
背景:synchronized 实现的是 互斥 —— 保证同一时刻只有一个线程进入临界区。但多个线程之间常常还需要 通信 —— 一个线程执行完某个条件,通知另一个线程继续执行。
wait/notify 就是线程间通信机制:
| 方法 | 含义 |
|---|---|
wait() | 当前线程等待并释放锁,进入“等待队列” |
notify() | 唤醒一个等待该对象锁的线程 |
notifyAll() | 唤醒所有等待该对象锁的线程 |
5.1 使用前提
只能在 synchronized 代码块或方法中使用
否则抛出异常:
IllegalMonitorStateException
因为这些方法依赖于“监视器锁(monitor)”,必须在持有锁对象的线程中调用。
5.2 工作原理
synchronized(obj) {obj.wait(); // 当前线程阻塞,释放 obj 锁// 或obj.notify(); // 通知等待 obj 的线程
}
-
wait():线程进入等待队列,释放锁。 -
notify():将等待队列中的一个线程移动到 锁池中。 -
被唤醒的线程必须重新获得锁,才会从
wait()处继续执行。
5.3 经典应用:生产者-消费者模型
class SharedQueue {private final Queue<Integer> queue = new LinkedList<>();private final int MAX = 5;public synchronized void produce(int value) throws InterruptedException {while (queue.size() == MAX) {wait(); // 队列满了,等待消费者消费}queue.offer(value);System.out.println("生产:" + value);notify(); // 通知消费者}public synchronized int consume() throws InterruptedException {while (queue.isEmpty()) {wait(); // 队列空了,等待生产者}int val = queue.poll();System.out.println("消费:" + val);notify(); // 通知生产者return val;}
}
5.4 常见使用模式:while + wait
while (!条件成立) {wait();
}
不用 if 的原因是:线程被唤醒后不一定立刻获得锁,即便获得锁,也要重新检查条件是否成立(可能被其他线程抢先修改)。
5.5 方法语义与注意点
| 方法名 | 作用与注意事项 |
|---|---|
wait() | 当前线程等待,释放锁,进入“等待队列”,直到被唤醒(notify) |
notify() | 唤醒一个等待线程,谁被唤醒不确定(不公平) |
notifyAll() | 唤醒所有等待线程(推荐使用) |
| wait/notify 必须配合 | synchronized,否则抛出异常 |
| wait 会释放锁 | 唤醒后重新竞争锁才能继续 |
5.6 线程状态图解
+--------------+| 运行状态 |+--------------+|wait()调用|V+--------------+| 等待(waiting) |+--------------+|notify()调用|V+--------------+| 阻塞(锁池) |+--------------+|获得锁|V+--------------+| 运行状态 |+--------------+
5.7 wait/notify 与 synchronized 的配合关系
| 操作 | 说明 |
|---|---|
wait() | 释放当前锁,线程挂起(暂停) |
notify() | 唤醒一个等待锁的线程 |
notifyAll() | 唤醒所有等待锁的线程 |
| 唤醒后必须重新获得锁 | 否则不能继续执行 |
synchronized 是必要条件 | wait/notify 只能在锁保护的代码块中用 |
5.8 与 Object 的关系
-
wait()/notify()/notifyAll()是Object类的方法。 -
因为每个对象都可以作为锁对象(monitor),所以 Java 把这几个方法放到了
Object里,而不是放在Thread类里。
5.9 常见问题解析
wait 和 sleep 区别?
| 比较点 | wait() | sleep() |
|---|---|---|
| 所在类 | Object | Thread |
| 是否释放锁 | 是 | 否 |
| 是否需要同步块 | 是 | 否 |
| 唤醒方式 | notify()/notifyAll() | 自动唤醒或中断 |
| 使用目的 | 多线程通信 | 暂停线程 |
5.10 逆向工程中识别 wait/notify 的线索
在 smali 中识别:
invoke-virtual {v0}, Ljava/lang/Object;->wait()V
invoke-virtual {v0}, Ljava/lang/Object;->notify()V
invoke-virtual {v0}, Ljava/lang/Object;->notifyAll()V
在 Java 中常见用法:
-
多线程间任务协调(例如上传完成后触发清理)
-
APP 启动后等待某些条件初始化完成再继续运行
-
加解密线程等“异步执行 + 阻塞等待”模式
Hook 点建议(Frida):
-
Hook
wait():追踪哪些线程会阻塞等待 -
Hook
notify():找出谁在控制状态切换 -
Hook
Thread.currentThread().getName()+ 栈回溯:找出阻塞点
5.11 小结
| 方法 | 是否释放锁 | 是否阻塞线程 | 唤醒方式 | 使用场景 |
|---|---|---|---|---|
wait() | 是 | 是 | notify() 唤醒 | 阻塞等待 |
notify() | 否 | 否 | 唤醒一个线程 | 通信 |
notifyAll() | 否 | 否 | 唤醒所有线程 | 通信 |
六、ConcurrentHashMap
定义:ConcurrentHashMap<K, V> 是 Java 提供的线程安全的 哈希表容器,适合高并发环境下的读写操作。它是 java.util.concurrent 包的一部分,在 Java 多线程中用于 代替 HashMap + synchronized 的组合。
6.1 为什么不能用 HashMap?
| 容器类型 | 线程安全? | 问题 |
|---|---|---|
HashMap | 否 | 多线程读写可能数据错乱、死循环 |
Hashtable | 是 | 整体加锁,效率低 |
ConcurrentHashMap | 是 | 细粒度锁,性能高,推荐使用 |
6.2 ConcurrentHashMap 的版本演进
| JDK版本 | 实现结构 |
|---|---|
| JDK 1.7 | Segment + HashEntry + 锁分段(类似分桶加锁) |
| JDK 1.8+ | 数组 + 链表/红黑树 + CAS + synchronized(无 Segment) |
目前主流使用的是 JDK 1.8+ 的实现方式。
6.3 JDK1.8 实现结构详解
整体结构:
ConcurrentHashMap<K, V>:└── table:Node<K,V>[] 数组(核心结构)└── 每个桶是一个 Node 链表(冲突时链式存储,链表长了会转红黑树)
插入流程核心步骤:
-
根据 key 计算 hash 值;
-
定位到 table 数组的桶位置;
-
如果为空,使用 CAS(无锁)写入新节点;
-
如果已有节点,进入链表/树结构:
-
同步加锁(使用
synchronized); -
插入节点;
-
若链表太长,转为红黑树。
-
高并发性能的来源:
-
空桶插入使用 CAS(无需加锁);
-
桶内冲突才使用
synchronized,锁粒度更细; -
读操作不加锁,使用 volatile + 内存语义 保证可见性;
-
分段并发,支持多个线程同时写不同桶。
6.4 常用方法
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();map.put("a", "1"); // 插入
map.get("a"); // 获取
map.remove("a"); // 删除
map.containsKey("a"); // 判断 key 是否存在
map.size(); // 获取大小
6.5 与 HashMap 的区别
| 特性 | HashMap | ConcurrentHashMap |
|---|---|---|
| 是否线程安全 | 否 | 是 |
| 并发性能 | 差 | 高(细粒度锁 + CAS) |
| null key/value 支持 | key 和 value 都能为 null | key/value 都不能为 null |
| 适用场景 | 单线程 / 无并发需求 | 多线程 / 并发环境(爬虫、服务端缓存等) |
6.6 源码级核心关键点(JDK 1.8)
put 方法核心片段:
final V putVal(K key, V value, boolean onlyIfAbsent) {...if (tabAt(tab, i) == null) {if (casTabAt(tab, i, null, new Node<>(...)))break; // CAS 插入成功} else {synchronized (f) {// synchronized 锁住桶头节点...}}
}
get 方法无锁实现(高性能):
Node<K,V> e = tabAt(tab, i);
while (e != null) {if (e.hash == hash && (e.key.equals(key))) return e.val;e = e.next;
}
替代 Segment 的方式:
-
使用 数组 + 节点结构
-
抛弃
ReentrantLock,使用synchronized+ volatile + CAS,减少内存占用 & 提升性能
6.7 常见问题
ConcurrentHashMap 的 key 或 value 能为 null 吗?
不能!否则会抛 NullPointerException。
-
防止因为 null 值而产生非预期的
Null == Null等逻辑错误; -
保证内部算法的正确性。
ConcurrentHashMap 如何保证线程安全?
-
写操作: 通过 CAS +
synchronized实现原子性; -
读操作: 无锁,使用 volatile 保证可见性;
-
并发性: 细粒度锁(桶级别),避免整表加锁。
6.8 在逆向工程中的使用线索
常见用途:
-
多线程任务缓存(如 WebSocket 多连接 Map)
-
异步回调 ID-Callback 映射表
-
加密模块中维护 session/token 映射表
-
上传文件任务的状态表等
逆向时识别方式:
1)jadx 反编译:
private static final ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>();
2)smali 反编译:
invoke-direct {v0}, Ljava/util/concurrent/ConcurrentHashMap;-><init>()V
3)hook 时判断:
-
类中存在名为
ConcurrentHashMap的成员; -
多线程访问共享资源时没有加锁,仍能运行正确,说明用了并发容器。
6.9 使用建议与最佳实践
-
适合多线程读多写少的缓存场景;
-
不要手动加锁再访问 ConcurrentHashMap;
-
不要滥用
.compute()之类复杂操作(可能加锁较多); -
不要用它来实现精确计数器(建议使用
LongAdder等更适合高并发写的结构)。
6.10 小结
| 项目 | 说明 |
|---|---|
| 线程安全性 | 是 |
| 底层结构(JDK1.8) | 数组 + 链表 + 红黑树 + CAS + synchronized |
| 插入是否加锁 | 空桶用 CAS,不空则 synchronized |
| 读取是否加锁 | 无锁 |
| 是否支持 null | key 和 value 都不支持 |
| 用于逆向中分析什么 | 多线程共享任务、回调注册、状态同步 Map 等 |
七、BlockingQueue
BlockingQueue 是 Java 提供的 支持阻塞式读写 的 线程安全队列,它广泛用于生产者-消费者模式、任务队列、异步日志、线程池任务缓存等场景。
属于: java.util.concurrent 包
接口定义:
public interface BlockingQueue<E> extends Queue<E> { ... }
7.1 核心特性
| 特性 | 说明 |
|---|---|
| 线程安全 | 内部自动加锁(ReentrantLock) |
| 支持阻塞操作 | 插入或获取元素时,可自动等待资源就绪 |
| 有界 / 无界 | 支持设置队列最大容量 |
| 多线程通信 | 常用于生产者-消费者模型 |
| 常见实现类 | ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue 等 |
7.2 核心方法分类
| 操作 | 抛出异常 | 返回特殊值 | 阻塞等待 | 限时等待 |
|---|---|---|---|---|
| 插入元素 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
| 获取元素 | remove() | poll() | take() | poll(time, unit) |
| 查看队首元素 | element() | peek() | ✘ | ✘ |
示例:put() 和 take() 的使用
BlockingQueue<String> queue = new LinkedBlockingQueue<>(5);queue.put("A"); // 若满则阻塞
String item = queue.take(); // 若空则阻塞
7.3 常见实现类对比
| 类名 | 结构 | 有界? | 特点说明 |
|---|---|---|---|
ArrayBlockingQueue | 数组结构 | 是 | 有界队列,性能高,读写分离锁 |
LinkedBlockingQueue | 链表结构 | 可设置 | 默认容量为 Integer.MAX_VALUE,适合任务量不确定 |
PriorityBlockingQueue | 优先队列(堆) | 否 | 按优先级排序出队,任务调度常用 |
SynchronousQueue | 无缓冲队列 | 是 | 每个 put() 必须等待 take(),用于线程交替协作 |
DelayQueue | 延迟队列(按时间排序) | 否 | 支持定时任务延迟执行 |
7.4 生产者消费者模型完整示例
import java.util.concurrent.*;public class ProducerConsumer {public static void main(String[] args) {BlockingQueue<String> queue = new LinkedBlockingQueue<>(3);// 生产者线程new Thread(() -> {try {for (int i = 0; i < 5; i++) {queue.put("任务" + i); // 阻塞插入System.out.println("生产了:" + i);Thread.sleep(500);}} catch (InterruptedException ignored) {}}).start();// 消费者线程new Thread(() -> {try {while (true) {String task = queue.take(); // 阻塞获取System.out.println("消费了:" + task);Thread.sleep(1000);}} catch (InterruptedException ignored) {}}).start();}
}
7.5 线程安全机制分析
以 LinkedBlockingQueue 为例
-
内部有两个锁:
putLock(用于写),takeLock(用于读),读写分离锁 -
使用
Condition实现阻塞等待 / 唤醒机制(类似wait/notify)
// 插入元素时
final ReentrantLock putLock;
final Condition notFull;// 获取元素时
final ReentrantLock takeLock;
final Condition notEmpty;
-
当队列满时,
put()会调用notFull.await()挂起; -
take()会在队列空时调用notEmpty.await()挂起; -
插入/删除后用
signal()唤醒对方线程。
7.6 阻塞与唤醒机制流程图
线程A执行 queue.put(x)↓(队列满了)
挂起等待 → notFull.await()↓
线程B执行 queue.take()↓
队列不满 → notFull.signal() → 唤醒线程A
7.7 在逆向工程中的作用与识别方法
典型使用场景:
| 场景 | 说明 |
|---|---|
| 线程间任务投递 | 多线程模块解耦:异步任务、解密队列、上报、日志队列等 |
| 线程池工作队列 | ThreadPoolExecutor 内部使用 BlockingQueue |
| 网络请求 + UI 更新 | 网络线程生产数据,UI 线程消费(主线程 Handler 机制) |
| 回调处理、事件流转 | 防止回调线程阻塞主流程 |
逆向识别线索:
Java 层:
private BlockingQueue<Task> taskQueue = new LinkedBlockingQueue<>();
taskQueue.put(task); // 写入任务
taskQueue.take(); // 阻塞处理任务
Smali 层:
invoke-interface {v0, v1}, Ljava/util/concurrent/BlockingQueue;->put(Ljava/lang/Object;)V
Frida Hook 建议:
-
Hook
put()→ 获取所有入队任务信息(可打印加密任务、回调参数等) -
Hook
take()→ 判断异步逻辑开始执行的时间点 -
可结合
Thread.getName()输出线程名称
7.8 实践建议
| 场景 | 建议 |
|---|---|
| 任务队列(写多读少) | 用 LinkedBlockingQueue |
| 固定任务容量控制(如 100 个) | 用 ArrayBlockingQueue(100) |
| 高频实时通信(线程对线程) | 用 SynchronousQueue |
| 定时任务 / 延迟任务调度 | 用 DelayQueue |
| 优先级任务调度 | 用 PriorityBlockingQueue |
7.9 小结
| 特性 | BlockingQueue |
|---|---|
| 是否线程安全 | 是 |
| 是否支持阻塞 | put/take 阻塞 |
| 是否可设置容量 | 有界队列(推荐) |
| 内部实现机制 | ReentrantLock + Condition |
| 用于逆向中的识别点 | put/take、任务缓存、解密队列、线程间通信 |
| 常见实现类 | LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue 等 |
八、ExecutorService
定义:ExecutorService 是 java.util.concurrent 包下的接口,表示一个可以执行提交的任务并管理其生命周期的线程池服务。它是 Java 并发框架中“任务提交者”和“线程池执行器”之间的桥梁。
和以前的区别:
传统方式:
new Thread(() -> {// 任务逻辑
}).start();
不推荐!因为:
-
无法复用线程,频繁创建线程开销大;
-
无法控制线程数;
-
无法追踪任务执行状态;
推荐方式 :
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.execute(() -> {// 任务逻辑
});
8.1 ExecutorService 的核心方法
| 方法名 | 说明 |
|---|---|
execute(Runnable task) | 提交无返回值任务(类似 submit,但不能拿结果) |
submit(Callable/Void task) | 提交任务,返回 Future 可获取结果 |
shutdown() | 拒绝新任务,等待任务完成后关闭 |
shutdownNow() | 尝试强制关闭,返回未执行任务列表 |
awaitTermination() | 阻塞等待线程池关闭 |
invokeAll(Collection<Callable>) | 同时提交多个任务,全部完成后返回结果列表 |
invokeAny(Collection<Callable>) | 提交多个任务,只取第一个成功返回的结果 |
8.2 ExecutorService 的工作流程
提交任务↓┌─────────────┐│ ExecutorService │└─────────────┘↓线程池中的线程↓执行任务
8.3 线程池创建方式(通过 Executors)
| 创建方式 | 含义 |
|---|---|
Executors.newFixedThreadPool(n) | 固定大小线程池(线程数恒定) |
Executors.newCachedThreadPool() | 可缓存线程池(空闲线程会复用) |
Executors.newSingleThreadExecutor() | 单线程池(串行执行任务) |
Executors.newScheduledThreadPool(n) | 定时任务线程池 |
示例:固定线程池执行任务
ExecutorService executor = Executors.newFixedThreadPool(3);for (int i = 0; i < 5; i++) {final int taskId = i;executor.execute(() -> {System.out.println("任务 " + taskId + " 执行中,线程:" + Thread.currentThread().getName());});
}executor.shutdown();
8.4 配合 Callable + Future 实现任务结果返回
Callable 与 Runnable 区别:
| 特性 | Runnable | Callable |
|---|---|---|
| 是否有返回值 | 无 | 有 |
| 是否抛异常 | 不支持抛异常 | 可以抛异常 |
| 结合使用 | execute() 执行 | submit() 返回 Future |
示例:获取异步执行结果
ExecutorService executor = Executors.newSingleThreadExecutor();Callable<Integer> task = () -> {Thread.sleep(1000);return 42;
};Future<Integer> future = executor.submit(task);System.out.println("主线程继续执行...");
Integer result = future.get(); // 阻塞直到任务完成
System.out.println("结果是:" + result);executor.shutdown();
8.5 关闭线程池的正确方式
推荐做法:
executor.shutdown(); // 拒绝新任务
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {executor.shutdownNow(); // 尝试强制关闭
}
8.6 线程池底层结构(JDK 1.8)
实际上 ExecutorService 的核心实现类是:
ThreadPoolExecutor implements ExecutorService
内部结构包括:
| 组成部分 | 说明 |
|---|---|
| 核心线程数 corePoolSize | 最少保持的线程数 |
| 最大线程数 maximumPoolSize | 最大线程数限制 |
| 工作队列 BlockingQueue | 用于缓存等待执行的任务 |
| 拒绝策略 RejectedExecutionHandler | 当任务满了之后的处理方式 |
| 线程工厂 ThreadFactory | 创建线程时的策略 |
| 线程复用机制 | 空闲线程会回收再利用 |
8.7 线程池常见问题
线程池为什么要使用?
-
避免频繁创建销毁线程;
-
控制并发线程数量;
-
支持任务结果追踪、异常捕获、延迟执行等;
线程池满了会怎样?
取决于 RejectedExecutionHandler 策略,默认是:
AbortPolicy(抛异常);
还可配置为:
-
DiscardPolicy(丢弃任务) -
CallerRunsPolicy(交给主线程执行) -
DiscardOldestPolicy(丢最老的任务)
8.8 逆向分析线程池线索
应用中常见用途:
| 应用场景 | 示例 |
|---|---|
| 异步加密/解密任务 | 多线程加密处理、上传预处理 |
| 日志后台上传处理 | 日志缓存后使用线程池上传 |
| 接口请求并发调用 | 多个网络请求同时发起 |
| UI/主线程解耦 | 任务下发到工作线程处理后通知主线程 |
逆向识别点:
1)反编译关键词:
Executors.newFixedThreadPool(...)
Executors.newSingleThreadExecutor(...)
ExecutorService executor = ...
2)smali 代码:
invoke-static {}, Ljava/util/concurrent/Executors;->newFixedThreadPool(I)Ljava/util/concurrent/ExecutorService;
3)Frida 动态 hook 点:
-
submit→ 获取任务类型与参数(加密、网络任务等) -
Future.get()→ 找出任务完成的时间 -
Thread.currentThread().getName()→ 确认线程身份(后台/加密/上传)
8.9 小结
| 项目 | 说明 |
|---|---|
| 是否线程安全 | 是(内部线程池实现) |
| 提交任务方式 | execute() / submit() |
| 是否支持返回值任务 | 支持 Callable + Future |
| 线程池常见创建方式 | Executors.newXXX() 或自定义构造 |
| 适用场景 | 并发任务执行 / 加密处理 / 网络回调 / 解耦主线程 |
| 在逆向中识别点 | ExecutorService 出现即代表后台线程调度 |
| 推荐关闭方式 | shutdown() + awaitTermination() |
九、Callable
定义:Callable<V> 是 Java 5 引入的接口,用于表示可以在线程中执行并返回结果的任务。它是 java.util.concurrent 包的一部分,弥补了 Runnable 只能执行不能返回结果的缺陷。
和 Runnable 区别:
| 特性 | Runnable | Callable<V> |
|---|---|---|
| 是否有返回值 | 无 | 有 |
| 是否支持抛异常 | 否 | 支持抛出异常(Checked) |
| 提交方式 | execute() | submit() → 返回 Future<V> |
| 接口方法 | run() | V call() throws Exception |
9.1 Callable 的接口定义
@FunctionalInterface
public interface Callable<V> {V call() throws Exception;
}
-
是一个函数式接口,可用 lambda 表达式实现;
-
call()方法执行任务逻辑并返回一个结果; -
可以抛出受检异常(
Exception);
9.2 使用 Callable 的完整流程
搭配 ExecutorService + Future 使用
import java.util.concurrent.*;public class CallableExample {public static void main(String[] args) throws Exception {ExecutorService executor = Executors.newSingleThreadExecutor();Callable<Integer> task = () -> {Thread.sleep(1000);return 42;};Future<Integer> future = executor.submit(task);System.out.println("主线程继续执行...");Integer result = future.get(); // 会阻塞直到结果可用System.out.println("异步任务结果:" + result);executor.shutdown();}
}
9.3 Future 接口:获取任务结果的桥梁
Future<V> future = executor.submit(callable);
V result = future.get(); // 阻塞等待返回结果
Future 常用方法:
| 方法 | 含义 |
|---|---|
get() | 等待任务完成,返回结果 |
get(timeout, unit) | 限时等待结果,超时报错 |
isDone() | 判断任务是否已完成 |
cancel(true) | 取消任务(可中断) |
9.4 多个 Callable 的并发执行(批处理)
List<Callable<String>> tasks = Arrays.asList(() -> "A", () -> "B", () -> "C"
);ExecutorService executor = Executors.newFixedThreadPool(3);
List<Future<String>> results = executor.invokeAll(tasks);for (Future<String> future : results) {System.out.println(future.get());
}
9.5 与 Runnable 的转换技巧
从 Runnable 变成 Callable:
Runnable task = () -> System.out.println("任务执行");
Callable<Void> callable = () -> {task.run();return null;
};
9.6 异常处理方式
-
Callable可以抛出异常; -
Future.get()会把异常包裹为ExecutionException抛出:
Callable<Integer> task = () -> {throw new IOException("失败了");
};Future<Integer> future = executor.submit(task);
try {future.get(); // 会抛 ExecutionException
} catch (ExecutionException e) {Throwable real = e.getCause(); // 拿到原始异常
}
9.7 在逆向工程中的使用线索
典型场景:
| 场景 | 描述 |
|---|---|
| 异步任务提交 | 后台耗时计算、网络请求、加密 |
| 上传/上报任务回调 | 等待执行结果再处理后续逻辑 |
| 动态模块加载 | Callable 中执行耗时初始化逻辑 |
| 解密操作封装 | 解密操作写在 call() 中 |
逆向识别点:
1)Java 层代码:
executor.submit(new Callable<String>() {public String call() {return encrypt(data);}
});
2)Smali 层代码:
invoke-interface {v0}, Ljava/util/concurrent/Callable;->call()Ljava/lang/Object;
3)Frida Hook 建议:
-
Hook
Callable.call()→ 打印任务执行前后的状态与入参 -
配合 Hook
submit()→ 定位哪些任务被封装为异步处理
9.8 常见问题
1)Runnable 能拿到返回值吗?
不能。只能通过共享变量或回调。
2)Callable 如何配合线程池?
必须使用 submit(),不能使用 execute()。
3)Callable 抛出异常会怎么样?
会在 Future.get() 时抛出 ExecutionException。
9.9 小结
| 项目 | Callable |
|---|---|
| 是否有返回值 | 有 |
| 是否能抛异常 | 可以抛出 Exception |
| 提交方式 | executor.submit(callable) |
| 获取结果方式 | Future.get() |
| 常用于 | 异步加密/网络请求/任务调度 |
| 逆向识别点 | call()、submit()、异步执行流程 |
| 与 Runnable 区别 | 可返回结果、可抛异常 |
十、Future
定义:Future<V> 是 Java 提供的一个接口,表示一个异步计算的结果。它是任务提交(通常是 Callable)之后,用来 获取结果 / 取消任务 / 检查是否完成 的控制句柄。
10.1 与 Callable 的配合关系
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<Integer> task = () -> 42;
Future<Integer> future = executor.submit(task); // 提交后立即返回 Future
10.2 Future 的核心方法
| 方法名 | 含义 |
|---|---|
get() | 阻塞等待任务完成并返回结果 |
get(timeout, unit) | 限时等待,超时抛出 TimeoutException |
isDone() | 判断任务是否已完成(成功/异常/取消) |
isCancelled() | 判断任务是否被取消 |
cancel(boolean mayInterruptIfRunning) | 取消任务,参数是否允许中断运行中的任务 |
示例:获取异步执行结果
Callable<Integer> task = () -> {Thread.sleep(1000);return 100;
};Future<Integer> future = executor.submit(task);System.out.println("主线程执行中...");
Integer result = future.get(); // 阻塞直到任务完成
System.out.println("任务完成,结果:" + result);
10.3 get() 方法详解
特点:
-
是一个阻塞方法,任务没完成就会卡住;
-
会抛出两种重要异常:
| 异常类型 | 说明 |
|---|---|
ExecutionException | 任务内部抛异常,被包装后抛出 |
InterruptedException | 当前线程在等待中被中断 |
获取原始异常的方式:
try {future.get();
} catch (ExecutionException e) {Throwable cause = e.getCause(); // 原始异常
}
10.4 cancel() 方法详解
future.cancel(true);
参数说明:
-
true:如果任务正在运行,尝试中断; -
false:不去中断已经在运行的任务,只取消尚未开始的任务。
结果判断:
-
cancel()返回值:-
true:取消成功; -
false:任务已完成或已经取消;
-
-
future.isCancelled():是否已取消; -
future.isDone():是否已完成(包含正常完成、异常、取消三种状态)
10.5 invokeAll() 与批量 Future
List<Callable<String>> tasks = Arrays.asList(() -> "A", () -> "B", () -> "C"
);
List<Future<String>> futures = executor.invokeAll(tasks);for (Future<String> future : futures) {System.out.println(future.get());
}
-
每个任务的执行结果会通过对应的
Future对象返回; -
所有任务都执行完,才会继续往下走。
10.6 应用场景举例
| 场景 | 描述 |
|---|---|
| 加密、压缩等异步计算任务 | 计算结果通过 Future 返回 |
| 并行网络请求 | 多个 Callable 并发发起 HTTP 请求 |
| 控制超时逻辑 | .get(timeout) 控制最长等待时间 |
| 可取消的任务(如上传、导出) | cancel() 取消任务并判断状态 |
10.7 逆向分析中的使用线索
常见行为:
-
某个任务由
Callable执行,返回Future -
主线程或回调中调用
.get()拿结果 -
若出现
cancel()说明有可中断任务(上传、检测等)
逆向识别点:
Java 代码:
Future<String> result = executor.submit(new MyCallable());
String val = result.get();
Smali 代码:
invoke-interface {v0}, Ljava/util/concurrent/Future;->get()Ljava/lang/Object;
Frida hook:
-
Future.get()→ 监控任务完成时刻及返回值; -
cancel()→ 判断任务是否支持中断或被恶意控制。
10.8 常见问题
1)Future.get() 是阻塞的吗?
是,直到任务完成或超时。
2)任务执行异常会抛出吗?
会,但封装为 ExecutionException,通过 getCause() 拿原始异常。
3)任务取消了还能 get 吗?
不行,会抛 CancellationException。
4)cancel(true) 和 cancel(false) 区别?
-
true:正在运行的任务也尝试中断; -
false:仅取消还没开始的任务。
10.9 小结
| 项目 | 说明 |
|---|---|
| 是否有返回值 | 是(用于接收异步任务结果) |
| 是否支持超时等待 | .get(timeout) |
| 是否能取消任务 | .cancel(true/false) |
| get() 是否阻塞 | 是 |
| 配合使用对象 | Callable + ExecutorService |
| 在逆向中的价值 | 找出后台执行逻辑、异步任务的开始与结束、加密/上传行为的完成点 |
十一、死锁分析
死锁定义:当多个线程在互相等待对方持有的锁,且都不主动释放,导致永远阻塞,无法继续执行,就产生了死锁。
死锁成立的四个必要条件(操作系统级定义):
| 条件 | 解释 |
|---|---|
| 互斥 | 资源每次只能被一个线程占用 |
| 不可抢占 | 已获取资源不能被强行剥夺 |
| 占有且等待 | 已占有资源的线程还想再申请其它资源 |
| 循环等待 | 若干线程形成循环等待链(A 等 B,B 等 A) |
只要满足这四个条件,系统就有可能死锁。
11.1 Java 死锁模拟示例
public class DeadlockDemo {private static final Object lock1 = new Object();private static final Object lock2 = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (lock1) {System.out.println("Thread 1 拿到 lock1");try { Thread.sleep(100); } catch (Exception ignored) {}synchronized (lock2) {System.out.println("Thread 1 拿到 lock2");}}});Thread t2 = new Thread(() -> {synchronized (lock2) {System.out.println("Thread 2 拿到 lock2");try { Thread.sleep(100); } catch (Exception ignored) {}synchronized (lock1) {System.out.println("Thread 2 拿到 lock1");}}});t1.start();t2.start();}
}
输出:
Thread 1 拿到 lock1
Thread 2 拿到 lock2
// 然后死锁,两个线程互相等待
11.2 如何分析死锁
1)使用 jstack 命令排查死锁
jstack <PID>
找出 "Found one Java-level deadlock" 的提示。
示例输出:
Found one Java-level deadlock:
=============================
"Thread-1":waiting to lock monitor 0x00007f..., which is held by "Thread-2"
"Thread-2":waiting to lock monitor 0x00007f..., which is held by "Thread-1"
2)使用可视化工具:
-
JConsole
-
VisualVM
-
YourKit
-
Arthas(Alibaba 出品)
可以实时查看线程堆栈、锁持有情况、卡顿线程等。
11.3 常见死锁产生场景
| 场景类型 | 描述 |
|---|---|
| 多线程顺序反转锁 | 两个线程获取资源顺序不一致 |
| synchronized 嵌套过多 | 多层嵌套导致死锁链 |
| 多线程递归调用 | 某线程在调用回调中死锁 |
| 数据库连接池耗尽 | 全部连接被死锁阻塞无法释放 |
| 多线程 + 自定义锁 | 锁组合或多把锁导致交叉死锁 |
11.4 死锁避免策略(核心)
| 策略 | 原理 |
|---|---|
| 锁顺序一致 | 所有线程按相同顺序加锁 |
使用 tryLock + 超时 | 获取不到锁就放弃,避免阻塞 |
| 使用锁合并 | 减少锁的粒度和数量 |
| 死锁检测机制 | 部分框架(如 Redis、数据库)会自动检测死锁 |
用工具类(如 ReentrantLock) | 提供高级功能(可中断、限时)避免死锁 |
示例:使用 tryLock 避免死锁
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();public void doWork() {boolean gotLockA = lockA.tryLock();boolean gotLockB = lockB.tryLock();if (gotLockA && gotLockB) {try {// 业务逻辑} finally {lockB.unlock();lockA.unlock();}} else {// 获取不到锁,避免阻塞,记录日志if (gotLockA) lockA.unlock();if (gotLockB) lockB.unlock();}
}
11.5 逆向工程中如何识别死锁风险点
Java 逆向中识别死锁线索:
常见表现:
-
APP 卡死 / Loading 无限转圈;
-
某线程永远等待另一个线程释放资源;
-
JNI 回调、Java 回调交叉调用卡死;
Java 代码线索:
synchronized(lock1) {synchronized(lock2) { ... }
}
Frida hook 线索:
Java.perform(function () {var LockClass = Java.use("java.lang.Object");LockClass.wait.implementation = function () {console.log("线程进入 wait,可能存在阻塞", Java.use("java.lang.Thread").currentThread().getName());return this.wait();};
});
Native 层(NDK/JNI)死锁识别:
-
多线程使用
pthread_mutex或__android_log_print加锁时; -
Hook JNI 调用栈,检查是否有递归锁或线程等待。
11.6 实战案例分析
场景:上传模块出现卡顿
-
Java 线程负责文件上传,使用同步锁维护上传状态;
-
回调中 JNI 通知底层 C 逻辑等待;
-
C 层使用锁等待 Java 返回结果 → 死锁!
原因链:
Thread A(Java上传) — 获取 Lock1,等待 C 回调完成
Thread B(C层回调) — 获取 Lock2,等待 Java 上传状态完成
==> 死锁
11.7 小结
| 项目 | 说明 |
|---|---|
| 死锁定义 | 多线程循环等待资源,永远阻塞 |
| 必要条件 | 互斥、不可抢占、占有且等待、循环等待 |
| 模拟方式 | 多线程 + 多锁 + 顺序颠倒 |
| 分析工具 | jstack、VisualVM、Arthas、Frida |
| 避免策略 | 锁顺序统一、tryLock、减少锁数量 |
| 逆向分析线索 | wait()、synchronized 嵌套、线程阻塞不动 |
| Native 层死锁识别 | JNI / NDK 层互相等待,或 pthread 死锁 |
十二、线程安全问题识别
当多个线程访问共享资源时,如果没有正确的同步机制,会出现 数据错误、状态混乱、异常崩溃 等问题,我们就称之为“线程不安全”。
常见线程安全问题类型:
| 问题类型 | 描述 |
|---|---|
| 脏读 / 写穿 | 一个线程读到另一个线程未提交的修改 |
| 数据竞争(Race Condition) | 多线程同时修改数据,顺序不确定,导致错误 |
| 原子性缺失 | 多步操作未加锁,不能保证“整体不可中断” |
| 可见性问题 | 一个线程修改变量后,另一个线程看不到 |
| 指令重排序 | 编译器/CPU 重新安排执行顺序,导致异常结果 |
| 死锁 | 多线程互相等待对方释放锁 |
| 活锁 / 饥饿 | 线程持续运行但无法前进 / 长期得不到执行机会 |
12.1 线程不安全场景举例
1)共享变量未加锁(典型)
public class UnsafeCounter {private int count = 0;public void incr() {count++;}public int getCount() {return count;}
}
问题:count++ 实际是三步操作:
1. 读取 count
2. +1
3. 写回 count
多个线程可能交错执行,导致最终结果比预期小(丢失写入)——原子性缺失。
2)多线程修改集合(如 ArrayList)
List<String> list = new ArrayList<>();Runnable task = () -> {for (int i = 0; i < 1000; i++) {list.add("data"); // 非线程安全}
};
结果:
-
ArrayList在扩容时可能发生 越界异常 或 数据覆盖丢失。
解决方案:
-
用
CopyOnWriteArrayList或Collections.synchronizedList(); -
或者自己加锁(但复杂)。
3)双重检查锁(DCL)错误写法
public class Singleton {private static Singleton instance;public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton(); // 非线程安全}}}return instance;}
}
问题:对象初始化不是原子操作,可能出现空指针访问。
解决方案:
private static volatile Singleton instance;
4)静态变量共享风险
public class MyTask implements Runnable {private static int shared = 0;public void run() {shared++;}
}
问题:多个线程执行相同类的 run() 方法,可能导致 并发修改共享变量冲突。
5)非线程安全类使用:SimpleDateFormat
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.parse("2024-01-01");
问题:
-
SimpleDateFormat不是线程安全的,多个线程共享会出现数据解析错误。
替代方案:
-
每次创建新对象;
-
或用线程本地变量
ThreadLocal<SimpleDateFormat>; -
或用
DateTimeFormatter(Java 8)。
12.2 识别线程安全问题的方法
1)静态代码分析(人工+工具)
-
看是否存在:共享变量、多线程读写、集合类、IO 资源等
-
检查是否加锁、是否用 volatile、是否线程安全类
-
工具推荐:
-
IDEA / Eclipse 的警告提示
-
FindBugs / SpotBugs
-
SonarQube
-
2)动态测试 & 压测
for (int i = 0; i < 10000; i++) {new Thread(() -> obj.incr()).start();
}
-
用多线程大并发压测核心方法;
-
观察崩溃、数据错乱、结果异常、死锁卡住;
-
配合工具:
-
JMeter
-
JUnit 多线程测试框架
-
Java Concurrency Stress Test
-
3)jstack / VisualVM / Arthas 调用栈分析
-
定位线程卡住(死锁);
-
检查某个变量是否被多个线程访问;
-
查看锁持有状态、阻塞线程等。
4)使用 volatile、原子类 或 synchronized 等修复后验证
例如:
private volatile boolean flag;
private final AtomicInteger atomicCount = new AtomicInteger();
12.3 如何判断一个方法是否线程安全?
检查点:
| 检查内容 | 如果存在… |
|---|---|
| 是否访问共享变量(static 或成员变量) | ✔ 非线程安全 |
| 是否操作集合类(如 ArrayList、HashMap) | ✔ 非线程安全(默认) |
| 是否有多线程并发入口点 | ✔ 需要加锁或替代 |
| 是否使用线程安全类 | ✘ 用 synchronized 或线程安全类代替 |
| 是否涉及写入操作 | ✔ 需注意原子性、可见性、顺序性问题 |
12.4 在逆向中的作用与识别线索
应用场景:
| 场景 | 风险或用途 |
|---|---|
| 后台线程异步加密任务共享数据 | 多线程对同一缓冲区或密钥池操作冲突 |
| 回调队列多个线程消费 | 如果未加锁,可能造成数据错乱或崩溃 |
| 上传模块文件写入 | 并发写同一文件或日志,产生冲突 |
| JNI 调用 native 层共享变量 | Java 层线程安全 → native 不一定安全 |
逆向识别线索:
Java 层:
-
多线程同时访问
sharedMap,sharedBuffer等变量; -
synchronized、volatile、Atomic*类出现或缺失; -
Collections.synchronized*是否使用。
smali 层:
invoke-virtual {v0}, Ljava/util/concurrent/atomic/AtomicInteger;->incrementAndGet()I
invoke-virtual {v0}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z
Frida 动态追踪建议:
-
Hook 共享方法,加日志打印线程名和变量状态;
-
定位任务是否来自多个线程;
-
检测是否存在修改冲突(如 Hook
put,set,incr等);
12.5 小结
| 项目 | 内容 |
|---|---|
| 线程安全问题表现 | 数据错乱、死锁、崩溃、卡顿 |
| 常见场景 | 多线程共享变量 / 集合 / I/O |
| 判断方法 | 静态分析、并发测试、JVM 工具 |
| 修复方法 | 加锁、用原子类、使用线程安全集合 |
| 逆向中识别点 | 多线程任务执行逻辑 + 无锁访问共享变量 |
| Frida 应用 | 动态观察变量并发读写行为,排查竞态风险 |
十三、逆向识别“后台异步任务”线索
异步任务:由主线程发起、交由后台线程或线程池执行、不阻塞主流程的操作。
逆向目标:识别这些异步任务是谁触发的、在哪里执行的、执行了什么关键逻辑(如加密、校验、上传、反调试)
13.1 逆向中识别异步任务的常见入口
从 Java 层、smali 层、动态 Hook 三方面来梳理。
1)Java 层识别常见异步任务提交点
| 异步方式 | 典型代码 | 用途线索 |
|---|---|---|
Thread | new Thread(() -> {}).start() | 最原始的方式 |
Runnable | threadPool.execute(runnable) | 线程池任务 |
Callable + Future | submit(callable) | 有返回值任务 |
Handler.post() | handler.post(Runnable) | UI 回调、主线程任务 |
AsyncTask | new Task().execute() | 早期 Android 异步 |
TimerTask | timer.schedule(task, delay) | 延迟任务 |
Executors.new* | Executors.newCachedThreadPool() | 动态线程池 |
RxJava、协程等 | Observable.create()、launch{} | 高级异步封装 |
示例:
executorService.submit(() -> {String sign = SignUtil.genSign(data);postRequest(sign);
});
2)Smali 层识别关键调用
常见异步函数调用符号:
invoke-virtual {v0}, Ljava/lang/Thread;->start()V
invoke-interface {v0}, Ljava/lang/Runnable;->run()V
invoke-virtual {v0}, Landroid/os/Handler;->post(Ljava/lang/Runnable;)Z
invoke-interface {v0}, Ljava/util/concurrent/ExecutorService;->submit(Ljava/util/concurrent/Callable;)Ljava/util/concurrent/Future;
示例判断:
-
Runnable 对象是匿名类、lambda 表达式 → 通常藏有敏感逻辑;
-
线程池
submit()之后 → 会有异步加密/网络请求; -
postDelayed()延迟任务 → 常用于规避动态调试、延迟触发反调试等。
3)Frida 动态 Hook 识别异步任务
可以使用以下技巧来追踪这些任务:
> Hook Thread.start()
Java.perform(() => {const ThreadCls = Java.use("java.lang.Thread");ThreadCls.start.implementation = function () {console.log("[Thread.start] 启动异步线程:" + this);return this.start();};
});
> Hook ExecutorService.submit()
Java.perform(() => {const Executor = Java.use("java.util.concurrent.ThreadPoolExecutor");Executor.submit.overload('java.util.concurrent.Callable').implementation = function (callable) {console.log("[submit Callable] 提交异步任务:" + callable);return this.submit(callable);};
});
> Hook Handler.post()
Java.perform(() => {const Handler = Java.use("android.os.Handler");Handler.post.implementation = function (runnable) {console.log("[Handler.post] 提交异步 UI 任务:" + runnable);return this.post(runnable);};
});
13.2 异步任务常出现的功能模块
| 模块 | 异步任务用途 |
|---|---|
| 登录加密 | 加密在后台线程中完成 |
| 数据上报(埋点) | 后台收集并异步发出 |
| 签名 / 防篡改 | 异步生成签名,提高并发 |
| 反调试延迟检测 | 延迟几秒后检查 Frida、Magisk |
| 广告或推送拉取 | 异步请求接口或更新信息 |
| 配置更新 / 动态模块加载 | 异步拉配置,隐藏敏感逻辑 |
| 图片 / 文件上传 | 异步 IO 操作 |
13.3 如何定位异步任务中的关键逻辑
步骤一:定位提交任务的位置(Thread、submit、post)
可静态查找,也可动态 hook 抓堆栈。
步骤二:分析 Runnable.run() 或 Callable.call() 方法体
这些匿名类或内部类中,往往包含了:
-
加密调用(AES、RSA、HMAC)
-
网络请求(OkHttp、HttpURLConnection)
-
动态参数生成(sign、nonce、uuid)
步骤三:跟踪线程内调用链
举例:Frida trace 调用链:
frida-trace -n target.app -m "java.lang.Runnable.run"
或自定义 Trace:
Interceptor.attach(Method.run, {onEnter(args) {console.log("Async run called by: ", Thread.backtrace(this.context, Backtracer.ACCURATE));}
});
13.4 反调试中的异步线索
一些 APP 会通过延迟触发异步任务,规避动态调试:
new Handler().postDelayed(() -> {if (isDebuggerConnected()) {// kill or obfuscate}
}, 5000);
可以 Hook postDelayed() 找到这种延迟的反调试检测。
13.5 小结
| 识别方法 | 描述 |
|---|---|
| Java 静态分析 | 查找 Thread, submit, post, execute 等代码 |
| Smali 分析 | 识别 invoke-* 中对异步类的调用 |
| Frida Hook | 动态打印 start(), submit(), run() |
| 异步任务常用模块 | 加密、上报、配置、反调试、网络请求等 |
| 如何确认敏感逻辑 | 看是否有加密、HTTP、JNI、反调试等关键行为 |
| 延迟执行线索 | postDelayed, TimerTask, sleep 等控制行为 |