作用:Thread
类代表一个线程,用于创建和控制一个新的执行流(即“子线程”)。
定义:java.lang.Thread
类实现了 Runnable
接口。
方法一:继承 Thread
类
步骤:
自定义类继承 Thread
。
重写 run()
方法。
创建线程对象并调用 start()
方法。
示例代码:
class MyThread extends Thread {
@Override
public 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();
}
}
方法名 | 作用 |
---|---|
start() |
启动线程,执行 run() ,异步执行 |
run() |
线程实际要做的事情,用户重写 |
sleep(long millis) |
让当前线程休眠指定时间,不释放锁 |
join() |
等待其他线程执行完再继续当前线程 |
interrupt() |
中断线程(设置中断标志) |
isInterrupted() |
判断是否被中断 |
setName(String name) |
设置线程名 |
getName() |
获取线程名 |
setPriority(int) |
设置优先级(1~10) |
setDaemon(true) |
设置为守护线程(随主线程退出) |
Java 中线程有 6 种状态,可通过 getState()
查看:
状态 | 说明 |
---|---|
NEW | 线程已创建但未启动 |
RUNNABLE | 正在运行或准备运行 |
BLOCKED | 等待获取锁 |
WAITING | 等待 wait() 、join() |
TIMED_WAITING | sleep() 、wait(timeout) |
TERMINATED | 执行完毕或异常退出 |
start()
和 run()
区别:
Thread t = new MyThread();
t.start(); // 开启新线程,异步执行 run()
t.run(); // 同步调用 run(),没有新线程
在逆向时,尤其是 Android APP 里,经常用到线程来执行异步任务,比如后台加密、网络请求、日志上传等。
在逆向时识别 Thread 的方法:
new Thread(...).start()
run()
方法:new Thread(() -> {
// 加密、上传、收集信息等
}).start();
Thread.start()
或 Thread.run()
,可以获得关键的“异步行为入口”。对比项 | 线程(Thread) | 进程(Process) |
---|---|---|
定义 | 程序执行中的一个执行流 | 程序运行中的一个实例 |
开销 | 小 | 大 |
通信 | 共享内存(易出错) | IPC 机制 |
创建速度 | 快 | 慢 |
应用场景 | 高并发任务、异步操作 | 系统级隔离、多任务 |
Thread t = new Thread(() -> {
System.out.println("守护线程");
});
t.setDaemon(true); // 设置为守护线程(主线程退出,它也退出)
t.setName("EncryptThread"); // 设置线程名称,方便调试和逆向时识别
t.start();
核心点 | 内容 |
---|---|
创建线程方式 | 继承 Thread、实现 Runnable |
启动线程 | 用 start() ,不能直接 run() |
核心方法 | sleep , join , interrupt 等 |
状态管理 | Thread.State 、isAlive() |
逆向重点关注点 | start() 、匿名线程、线程任务中的加密或网络请求 |
动态分析策略 | Hook start() 和 run() ,查看异步行为 |
作用:Runnable
是 Java 中的一个函数式接口,用于定义线程执行的任务。
它只包含一个方法:
public interface Runnable {
void run();
}
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()
。
方式一:创建实现类
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();
特点 | 实现 Runnable | 继承 Thread |
---|---|---|
是否支持多继承 | 支持(更灵活) | 不支持(Java 单继承限制) |
任务与线程是否解耦 | 解耦(任务是独立的) | 耦合(任务写在线程类中) |
推荐程度 | 推荐(现代通用方式) | 只在需要自定义线程行为时使用 |
Runnable task = new MyRunnable();
Thread thread = new Thread(task);
thread.start();
执行顺序如下:
thread.start()
—— 创建新线程。
新线程调用 Thread.run()
。
Thread.run()
中调用了 target.run()
(就是你传的 Runnable
)。
执行你在 Runnable.run()
中写的业务逻辑。
分离任务逻辑与线程控制
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
传递任务。
在逆向工程中,Runnable
是“异步逻辑”的典型线索。
能在哪里看到 Runnable?
smali 中:
new-instance v0, Ljava/lang/Thread;
invoke-direct {v0, new Runnable$1()}, Ljava/lang/Thread;->(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 |
后台异步任务起点(如埋点统计) |
直接调用 run()
行不行?
可以运行,但不会创建新线程,代码仍在主线程中执行。
Runnable r = () -> System.out.println(Thread.currentThread().getName());
r.run(); // 主线程执行
new Thread(r).start(); // 新线程执行
Runnable 有没有返回值?
没有!如果需要返回值,可用 Callable
接口配合 Future
使用。
项目 | 内容 |
---|---|
定义 | 表示可执行的任务,定义在 Runnable.run() 中 |
与 Thread 关系 | Thread 实现了 Runnable,可以组合使用 |
使用方式 | 1)实现类 2)匿名内部类 3)Lambda 表达式 |
推荐原因 | 避免继承限制、解耦任务与线程、可重用性强 |
逆向分析线索 | 在 Runnable.run() 中找异步逻辑,如加密、网络、日志 |
动态分析手段 | Hook Thread.start() + 拿到 Runnable 对象和堆栈 |
synchronized
是 Java 提供的 内置锁(monitor lock) 机制。
目的:当多个线程访问共享资源(变量、方法、对象)时,防止出现 并发冲突 或 数据不一致 的问题。
1)同步代码块(推荐)
synchronized (lockObject) {
// 临界区代码:同一时刻只有一个线程能进入
}
lockObject
是锁对象,必须是引用类型(不能是 null)。
2)同步实例方法
public synchronized void doWork() {
// 相当于 synchronized(this)
}
每个实例对象有一个锁,不同实例互不影响。
3)同步静态方法
public static synchronized void staticWork() {
// 相当于 synchronized(ClassName.class)
}
类锁,对类级别的资源加锁(所有对象共用一把锁)。
类型 | 锁对象 | 应用场景 |
---|---|---|
同步代码块 | 显式指定对象 | 推荐:灵活控制锁粒度、避免死锁 |
实例方法 | this |
同步访问对象级别的成员变量 |
静态方法 | ClassName.class |
同步访问类级别的静态变量 |
注意:如果两个线程用的不是同一个锁对象,那么它们是不会互斥的。
synchronized 的实现机制:
JVM 会为每个对象维护一个 Monitor(监视器)。
每次进入同步块或方法时,线程尝试获取对象的 Monitor 锁。
进入后会将该锁标记为“占用”,其他线程进入时会被阻塞。
退出同步块或方法后,释放锁。
synchronized
是 可重入的,即同一线程可以重复获得同一个锁不会阻塞自己。
public synchronized void a() {
b(); // 同一线程再次进入锁
}
public synchronized void b() {
// 可执行
}
原子性(Atomicity): 进入 synchronized
的临界区代码,只能被一个线程执行,确保操作不可分割。
可见性(Visibility): 一个线程对变量的修改,对其他线程可见(解锁时会强制刷回主内存)。
如果两个线程持有彼此需要的锁,且不释放,就可能产生死锁。
示例:
synchronized (A) {
synchronized (B) {
// do something
}
}
如果另一个线程是:
synchronized (B) {
synchronized (A) {
// do something
}
}
两个线程互相等待就会死锁。
实战建议:
尽量按固定顺序加锁。
推荐使用细粒度锁或显式锁(如 ReentrantLock
)处理复杂场景。
特性 | synchronized | ReentrantLock |
---|---|---|
写法简单 | ✔ | ✘(需要 try-finally) |
可重入性 | ✔ | ✔ |
中断响应锁获取 | ✘ | ✔(lockInterruptibly) |
公平锁 | ✘(非公平) | ✔(可设为公平锁) |
超时获取锁 | ✘ | ✔(tryLock) |
条件变量 | ✘ | ✔(newCondition) |
在 Java / Smali 层的表现:
Java:
synchronized (this) {
// 异步加密、缓存写入、日志处理、文件操作等常见目标
}
smali 反编译中:
monitor-enter v0 # 加锁
... # 临界区代码
monitor-exit v0 # 解锁
线索:
多线程竞争资源时使用了 monitor-enter
。
在 Hook 分析或 Frida 动态调试时关注这个锁作用范围。
应用场景 | 原因 |
---|---|
单例模式(懒汉式) | 保证多线程只创建一个实例 |
缓存访问 | 读写共享缓存必须加锁 |
日志系统 | 多线程同时写文件需同步 |
并发计数器 | 保证 ++count 操作的原子性 |
Android 加密模块 | 共享密钥加解密需同步 |
只锁必要代码块,减小锁的范围,提高性能。
避免锁对象为 this
或全局变量,防止外部代码误用锁。
多线程下需要 共享的资源才需要加锁,不共享就不必同步。
能用 ConcurrentHashMap
、AtomicXXX
替代锁的场景,尽量不用锁。
项目 | 说明 |
---|---|
本质 | 基于 JVM Monitor 的内置锁 |
用法 | 代码块、实例方法、静态方法 |
锁对象 | 任意非 null 对象 |
是否可重入 | 是 |
是否可见性/原子性 | 有可见性 有原子性 |
是否阻塞其他线程 | 是 |
逆向识别点 | monitor-enter/exit ,配合分析共享资源逻辑 |
定义:volatile
是 Java 提供的轻量级同步机制,用于修饰变量,保证线程间的 可见性 和一定程度的 有序性。
背景:在 Java 中,每个线程有自己的工作内存(CPU 缓存),线程可能不会直接从主内存读取变量的值。
这就会导致:
// 主线程设置 running = false
// 工作线程却还在执行 while(running)
volatile 的作用:
可见性:一个线程修改变量,其他线程能立即看到。
禁止指令重排(有序性):保证写操作发生在读操作之前。
特性 | 是否支持 | 说明 |
---|---|---|
可见性 | ✔ | 主内存值修改后立即通知其他线程 |
有序性 | ✔ | 禁止与 volatile 相关操作的重排序 |
原子性 | ✘ | 不支持复合操作(如 i++ ) |
示例一:线程终止标志
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 缓存中读旧值,导致死循环。
特性 | volatile |
synchronized |
---|---|---|
原子性 | 不保证 | 保证 |
可见性 | 保证 | 保证 |
是否加锁 | 无锁 | 加锁(性能低) |
性能 | 高(无阻塞) | 低(涉及上下文切换) |
使用难度 | 中等(易误用) | 简单 |
示例:i++ 不是原子操作
volatile int count = 0;
public void add() {
count++; // 实际上是:读取 -> 加一 -> 写入(非原子)
}
多线程下可能造成丢数据!
正确方式:使用 AtomicInteger
或 synchronized
:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
场景类型 | 示例说明 |
---|---|
状态标志 | 控制线程退出、任务是否开始等 |
配置信息 | 热更新配置字段 |
单例双重检查锁 DCL | 保证初始化顺序和可见性 |
在 JMM(Java 内存模型)中,volatile 有如下语义:
写操作: 把当前线程的工作内存变量刷新到主内存;
读操作: 清空当前线程的工作内存,并从主内存读取最新值。
happens-before 原则:
原则 | 含义 |
---|---|
对一个 volatile 变量的写操作先行发生于后续对该变量的读操作 |
保证有序 & 可见 |
字节码层面
使用 volatile
修饰字段后,JVM 字节码会加入:
字节码指令 | 说明 |
---|---|
volatile-read |
加入 loadload 屏障 |
volatile-write |
加入 storestore + storeload 屏障 |
CPU 层面
x86 平台上,volatile
使用 lock
前缀的汇编指令,如 lock xadd
。
保证:
内存写入顺序
刷新缓存行
让其他 CPU 核读取主存数据
常见用法线索:
类型 | 表现形式 |
---|---|
控制标志变量 | 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,可识别线程状态切换时机。
错误理解 | 正确认知 |
---|---|
volatile 保证线程安全 |
仅保证可见性,不保证原子性 |
用 volatile 修饰的变量可以替代锁 |
仅在极少数“写少读多”的场景可以 |
多线程访问共享变量,加上 volatile 就万无一失了 |
对于复合操作,仍需 synchronized 或 CAS |
特性 | volatile |
---|---|
保证可见性 | ✔ |
保证有序性 | ✔(部分) |
保证原子性 | ✘ |
是否加锁 | ✘ |
使用场景 | 状态标志、配置信息、单例懒加载 |
不适用场景 | i++ 、count++ 、多个字段一起更新 |
背景:synchronized
实现的是 互斥 —— 保证同一时刻只有一个线程进入临界区。但多个线程之间常常还需要 通信 —— 一个线程执行完某个条件,通知另一个线程继续执行。
wait/notify 就是线程间通信机制:
方法 | 含义 |
---|---|
wait() |
当前线程等待并释放锁,进入“等待队列” |
notify() |
唤醒一个等待该对象锁的线程 |
notifyAll() |
唤醒所有等待该对象锁的线程 |
只能在 synchronized 代码块或方法中使用
否则抛出异常:
IllegalMonitorStateException
因为这些方法依赖于“监视器锁(monitor)”,必须在持有锁对象的线程中调用。
synchronized(obj) {
obj.wait(); // 当前线程阻塞,释放 obj 锁
// 或
obj.notify(); // 通知等待 obj 的线程
}
wait()
:线程进入等待队列,释放锁。
notify()
:将等待队列中的一个线程移动到 锁池中。
被唤醒的线程必须重新获得锁,才会从 wait()
处继续执行。
class SharedQueue {
private final Queue 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;
}
}
while (!条件成立) {
wait();
}
不用 if
的原因是:线程被唤醒后不一定立刻获得锁,即便获得锁,也要重新检查条件是否成立(可能被其他线程抢先修改)。
方法名 | 作用与注意事项 |
---|---|
wait() |
当前线程等待,释放锁,进入“等待队列”,直到被唤醒(notify ) |
notify() |
唤醒一个等待线程,谁被唤醒不确定(不公平) |
notifyAll() |
唤醒所有等待线程(推荐使用) |
wait/notify 必须配合 | synchronized ,否则抛出异常 |
wait 会释放锁 | 唤醒后重新竞争锁才能继续 |
+--------------+
| 运行状态 |
+--------------+
|
wait()调用
|
V
+--------------+
| 等待(waiting) |
+--------------+
|
notify()调用
|
V
+--------------+
| 阻塞(锁池) |
+--------------+
|
获得锁
|
V
+--------------+
| 运行状态 |
+--------------+
操作 | 说明 |
---|---|
wait() |
释放当前锁,线程挂起(暂停) |
notify() |
唤醒一个等待锁的线程 |
notifyAll() |
唤醒所有等待锁的线程 |
唤醒后必须重新获得锁 | 否则不能继续执行 |
synchronized 是必要条件 |
wait/notify 只能在锁保护的代码块中用 |
Object
的关系wait()
/ notify()
/ notifyAll()
是 Object
类的方法。
因为每个对象都可以作为锁对象(monitor),所以 Java 把这几个方法放到了 Object
里,而不是放在 Thread
类里。
wait 和 sleep 区别?
比较点 | wait() |
sleep() |
---|---|---|
所在类 | Object |
Thread |
是否释放锁 | 是 | 否 |
是否需要同步块 | 是 | 否 |
唤醒方式 | notify()/notifyAll() |
自动唤醒或中断 |
使用目的 | 多线程通信 | 暂停线程 |
在 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()
+ 栈回溯:找出阻塞点
方法 | 是否释放锁 | 是否阻塞线程 | 唤醒方式 | 使用场景 |
---|---|---|---|---|
wait() |
是 | 是 | notify() 唤醒 |
阻塞等待 |
notify() |
否 | 否 | 唤醒一个线程 | 通信 |
notifyAll() |
否 | 否 | 唤醒所有线程 | 通信 |
定义:ConcurrentHashMap
是 Java 提供的线程安全的 哈希表容器,适合高并发环境下的读写操作。它是 java.util.concurrent
包的一部分,在 Java 多线程中用于 代替 HashMap + synchronized
的组合。
容器类型 | 线程安全? | 问题 |
---|---|---|
HashMap |
否 | 多线程读写可能数据错乱、死循环 |
Hashtable |
是 | 整体加锁,效率低 |
ConcurrentHashMap |
是 | 细粒度锁,性能高,推荐使用 |
JDK版本 | 实现结构 |
---|---|
JDK 1.7 | Segment + HashEntry + 锁分段(类似分桶加锁) |
JDK 1.8+ | 数组 + 链表/红黑树 + CAS + synchronized(无 Segment) |
目前主流使用的是 JDK 1.8+ 的实现方式。
整体结构:
ConcurrentHashMap:
└── table:Node[] 数组(核心结构)
└── 每个桶是一个 Node 链表(冲突时链式存储,链表长了会转红黑树)
插入流程核心步骤:
根据 key 计算 hash 值;
定位到 table 数组的桶位置;
如果为空,使用 CAS(无锁)写入新节点;
如果已有节点,进入链表/树结构:
同步加锁(使用 synchronized
);
插入节点;
若链表太长,转为红黑树。
高并发性能的来源:
空桶插入使用 CAS(无需加锁);
桶内冲突才使用 synchronized
,锁粒度更细;
读操作不加锁,使用 volatile + 内存语义 保证可见性;
分段并发,支持多个线程同时写不同桶。
ConcurrentHashMap map = new ConcurrentHashMap<>();
map.put("a", "1"); // 插入
map.get("a"); // 获取
map.remove("a"); // 删除
map.containsKey("a"); // 判断 key 是否存在
map.size(); // 获取大小
特性 | HashMap | ConcurrentHashMap |
---|---|---|
是否线程安全 | 否 | 是 |
并发性能 | 差 | 高(细粒度锁 + CAS) |
null key/value 支持 | key 和 value 都能为 null | key/value 都不能为 null |
适用场景 | 单线程 / 无并发需求 | 多线程 / 并发环境(爬虫、服务端缓存等) |
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 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,减少内存占用 & 提升性能
ConcurrentHashMap 的 key 或 value 能为 null 吗?
不能!否则会抛 NullPointerException
。
防止因为 null 值而产生非预期的 Null == Null
等逻辑错误;
保证内部算法的正确性。
ConcurrentHashMap 如何保证线程安全?
写操作: 通过 CAS + synchronized
实现原子性;
读操作: 无锁,使用 volatile 保证可见性;
并发性: 细粒度锁(桶级别),避免整表加锁。
常见用途:
多线程任务缓存(如 WebSocket 多连接 Map)
异步回调 ID-Callback 映射表
加密模块中维护 session/token 映射表
上传文件任务的状态表等
逆向时识别方式:
1)jadx 反编译:
private static final ConcurrentHashMap sessionMap = new ConcurrentHashMap<>();
2)smali 反编译:
invoke-direct {v0}, Ljava/util/concurrent/ConcurrentHashMap;->()V
3)hook 时判断:
类中存在名为 ConcurrentHashMap
的成员;
多线程访问共享资源时没有加锁,仍能运行正确,说明用了并发容器。
适合多线程读多写少的缓存场景;
不要手动加锁再访问 ConcurrentHashMap;
不要滥用 .compute()
之类复杂操作(可能加锁较多);
不要用它来实现精确计数器(建议使用 LongAdder
等更适合高并发写的结构)。
项目 | 说明 |
---|---|
线程安全性 | 是 |
底层结构(JDK1.8) | 数组 + 链表 + 红黑树 + CAS + synchronized |
插入是否加锁 | 空桶用 CAS,不空则 synchronized |
读取是否加锁 | 无锁 |
是否支持 null | key 和 value 都不支持 |
用于逆向中分析什么 | 多线程共享任务、回调注册、状态同步 Map 等 |
BlockingQueue
是 Java 提供的 支持阻塞式读写 的 线程安全队列,它广泛用于生产者-消费者模式、任务队列、异步日志、线程池任务缓存等场景。
属于: java.util.concurrent
包
接口定义:
public interface BlockingQueue extends Queue { ... }
特性 | 说明 |
---|---|
线程安全 | 内部自动加锁(ReentrantLock) |
支持阻塞操作 | 插入或获取元素时,可自动等待资源就绪 |
有界 / 无界 | 支持设置队列最大容量 |
多线程通信 | 常用于生产者-消费者模型 |
常见实现类 | ArrayBlockingQueue 、LinkedBlockingQueue 、PriorityBlockingQueue 、SynchronousQueue 等 |
操作 | 抛出异常 | 返回特殊值 | 阻塞等待 | 限时等待 |
---|---|---|---|---|
插入元素 | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
获取元素 | remove() |
poll() |
take() |
poll(time, unit) |
查看队首元素 | element() |
peek() |
✘ | ✘ |
示例:put()
和 take()
的使用
BlockingQueue queue = new LinkedBlockingQueue<>(5);
queue.put("A"); // 若满则阻塞
String item = queue.take(); // 若空则阻塞
类名 | 结构 | 有界? | 特点说明 |
---|---|---|---|
ArrayBlockingQueue |
数组结构 | 是 | 有界队列,性能高,读写分离锁 |
LinkedBlockingQueue |
链表结构 | 可设置 | 默认容量为 Integer.MAX_VALUE,适合任务量不确定 |
PriorityBlockingQueue |
优先队列(堆) | 否 | 按优先级排序出队,任务调度常用 |
SynchronousQueue |
无缓冲队列 | 是 | 每个 put() 必须等待 take() ,用于线程交替协作 |
DelayQueue |
延迟队列(按时间排序) | 否 | 支持定时任务延迟执行 |
import java.util.concurrent.*;
public class ProducerConsumer {
public static void main(String[] args) {
BlockingQueue 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();
}
}
以 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()
唤醒对方线程。
线程A执行 queue.put(x)
↓(队列满了)
挂起等待 → notFull.await()
↓
线程B执行 queue.take()
↓
队列不满 → notFull.signal() → 唤醒线程A
典型使用场景:
场景 | 说明 |
---|---|
线程间任务投递 | 多线程模块解耦:异步任务、解密队列、上报、日志队列等 |
线程池工作队列 | ThreadPoolExecutor 内部使用 BlockingQueue |
网络请求 + UI 更新 | 网络线程生产数据,UI 线程消费(主线程 Handler 机制) |
回调处理、事件流转 | 防止回调线程阻塞主流程 |
逆向识别线索:
Java 层:
private BlockingQueue 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()
输出线程名称
场景 | 建议 |
---|---|
任务队列(写多读少) | 用 LinkedBlockingQueue |
固定任务容量控制(如 100 个) | 用 ArrayBlockingQueue(100) |
高频实时通信(线程对线程) | 用 SynchronousQueue |
定时任务 / 延迟任务调度 | 用 DelayQueue |
优先级任务调度 | 用 PriorityBlockingQueue |
特性 | BlockingQueue |
---|---|
是否线程安全 | 是 |
是否支持阻塞 | put/take 阻塞 |
是否可设置容量 | 有界队列(推荐) |
内部实现机制 | ReentrantLock + Condition |
用于逆向中的识别点 | put/take 、任务缓存、解密队列、线程间通信 |
常见实现类 | LinkedBlockingQueue 、ArrayBlockingQueue 、SynchronousQueue 等 |
定义:ExecutorService
是 java.util.concurrent
包下的接口,表示一个可以执行提交的任务并管理其生命周期的线程池服务。它是 Java 并发框架中“任务提交者”和“线程池执行器”之间的桥梁。
和以前的区别:
传统方式:
new Thread(() -> {
// 任务逻辑
}).start();
不推荐!因为:
无法复用线程,频繁创建线程开销大;
无法控制线程数;
无法追踪任务执行状态;
推荐方式 :
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.execute(() -> {
// 任务逻辑
});
方法名 | 说明 |
---|---|
execute(Runnable task) |
提交无返回值任务(类似 submit ,但不能拿结果) |
submit(Callable/Void task) |
提交任务,返回 Future 可获取结果 |
shutdown() |
拒绝新任务,等待任务完成后关闭 |
shutdownNow() |
尝试强制关闭,返回未执行任务列表 |
awaitTermination() |
阻塞等待线程池关闭 |
invokeAll(Collection |
同时提交多个任务,全部完成后返回结果列表 |
invokeAny(Collection |
提交多个任务,只取第一个成功返回的结果 |
提交任务
↓
┌─────────────┐
│ ExecutorService │
└─────────────┘
↓
线程池中的线程
↓
执行任务
创建方式 | 含义 |
---|---|
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();
Callable 与 Runnable 区别:
特性 | Runnable |
Callable |
---|---|---|
是否有返回值 | 无 | 有 |
是否抛异常 | 不支持抛异常 | 可以抛异常 |
结合使用 | execute() 执行 |
submit() 返回 Future |
示例:获取异步执行结果
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable task = () -> {
Thread.sleep(1000);
return 42;
};
Future future = executor.submit(task);
System.out.println("主线程继续执行...");
Integer result = future.get(); // 阻塞直到任务完成
System.out.println("结果是:" + result);
executor.shutdown();
推荐做法:
executor.shutdown(); // 拒绝新任务
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 尝试强制关闭
}
实际上 ExecutorService
的核心实现类是:
ThreadPoolExecutor implements ExecutorService
内部结构包括:
组成部分 | 说明 |
---|---|
核心线程数 corePoolSize | 最少保持的线程数 |
最大线程数 maximumPoolSize | 最大线程数限制 |
工作队列 BlockingQueue | 用于缓存等待执行的任务 |
拒绝策略 RejectedExecutionHandler | 当任务满了之后的处理方式 |
线程工厂 ThreadFactory | 创建线程时的策略 |
线程复用机制 | 空闲线程会回收再利用 |
线程池为什么要使用?
避免频繁创建销毁线程;
控制并发线程数量;
支持任务结果追踪、异常捕获、延迟执行等;
线程池满了会怎样?
取决于 RejectedExecutionHandler
策略,默认是:
AbortPolicy
(抛异常);还可配置为:
DiscardPolicy
(丢弃任务)
CallerRunsPolicy
(交给主线程执行)
DiscardOldestPolicy
(丢最老的任务)
应用中常见用途:
应用场景 | 示例 |
---|---|
异步加密/解密任务 | 多线程加密处理、上传预处理 |
日志后台上传处理 | 日志缓存后使用线程池上传 |
接口请求并发调用 | 多个网络请求同时发起 |
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()
→ 确认线程身份(后台/加密/上传)
项目 | 说明 |
---|---|
是否线程安全 | 是(内部线程池实现) |
提交任务方式 | execute() / submit() |
是否支持返回值任务 | 支持 Callable + Future |
线程池常见创建方式 | Executors.newXXX() 或自定义构造 |
适用场景 | 并发任务执行 / 加密处理 / 网络回调 / 解耦主线程 |
在逆向中识别点 | ExecutorService 出现即代表后台线程调度 |
推荐关闭方式 | shutdown() + awaitTermination() |
定义:Callable
是 Java 5 引入的接口,用于表示可以在线程中执行并返回结果的任务。它是 java.util.concurrent
包的一部分,弥补了 Runnable
只能执行不能返回结果的缺陷。
和 Runnable 区别:
特性 | Runnable |
Callable |
---|---|---|
是否有返回值 | 无 | 有 |
是否支持抛异常 | 否 | 支持抛出异常(Checked) |
提交方式 | execute() |
submit() → 返回 Future |
接口方法 | run() |
V call() throws Exception |
@FunctionalInterface
public interface Callable {
V call() throws Exception;
}
是一个函数式接口,可用 lambda 表达式实现;
call()
方法执行任务逻辑并返回一个结果;
可以抛出受检异常(Exception
);
搭配 ExecutorService + Future
使用
import java.util.concurrent.*;
public class CallableExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable task = () -> {
Thread.sleep(1000);
return 42;
};
Future future = executor.submit(task);
System.out.println("主线程继续执行...");
Integer result = future.get(); // 会阻塞直到结果可用
System.out.println("异步任务结果:" + result);
executor.shutdown();
}
}
Future future = executor.submit(callable);
V result = future.get(); // 阻塞等待返回结果
Future 常用方法:
方法 | 含义 |
---|---|
get() |
等待任务完成,返回结果 |
get(timeout, unit) |
限时等待结果,超时报错 |
isDone() |
判断任务是否已完成 |
cancel(true) |
取消任务(可中断) |
List> tasks = Arrays.asList(
() -> "A", () -> "B", () -> "C"
);
ExecutorService executor = Executors.newFixedThreadPool(3);
List> results = executor.invokeAll(tasks);
for (Future future : results) {
System.out.println(future.get());
}
从 Runnable 变成 Callable:
Runnable task = () -> System.out.println("任务执行");
Callable callable = () -> {
task.run();
return null;
};
Callable
可以抛出异常;
Future.get()
会把异常包裹为 ExecutionException
抛出:
Callable task = () -> {
throw new IOException("失败了");
};
Future future = executor.submit(task);
try {
future.get(); // 会抛 ExecutionException
} catch (ExecutionException e) {
Throwable real = e.getCause(); // 拿到原始异常
}
典型场景:
场景 | 描述 |
---|---|
异步任务提交 | 后台耗时计算、网络请求、加密 |
上传/上报任务回调 | 等待执行结果再处理后续逻辑 |
动态模块加载 | Callable 中执行耗时初始化逻辑 |
解密操作封装 | 解密操作写在 call() 中 |
逆向识别点:
1)Java 层代码:
executor.submit(new Callable() {
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()
→ 定位哪些任务被封装为异步处理
1)Runnable 能拿到返回值吗?
不能。只能通过共享变量或回调。
2)Callable 如何配合线程池?
必须使用 submit()
,不能使用 execute()
。
3)Callable 抛出异常会怎么样?
会在 Future.get()
时抛出 ExecutionException
。
项目 | Callable |
---|---|
是否有返回值 | 有 |
是否能抛异常 | 可以抛出 Exception |
提交方式 | executor.submit(callable) |
获取结果方式 | Future.get() |
常用于 | 异步加密/网络请求/任务调度 |
逆向识别点 | call() 、submit() 、异步执行流程 |
与 Runnable 区别 | 可返回结果、可抛异常 |
定义:Future
是 Java 提供的一个接口,表示一个异步计算的结果。它是任务提交(通常是 Callable
)之后,用来 获取结果 / 取消任务 / 检查是否完成 的控制句柄。
Callable
的配合关系ExecutorService executor = Executors.newSingleThreadExecutor();
Callable task = () -> 42;
Future future = executor.submit(task); // 提交后立即返回 Future
方法名 | 含义 |
---|---|
get() |
阻塞等待任务完成并返回结果 |
get(timeout, unit) |
限时等待,超时抛出 TimeoutException |
isDone() |
判断任务是否已完成(成功/异常/取消) |
isCancelled() |
判断任务是否被取消 |
cancel(boolean mayInterruptIfRunning) |
取消任务,参数是否允许中断运行中的任务 |
示例:获取异步执行结果
Callable task = () -> {
Thread.sleep(1000);
return 100;
};
Future future = executor.submit(task);
System.out.println("主线程执行中...");
Integer result = future.get(); // 阻塞直到任务完成
System.out.println("任务完成,结果:" + result);
get()
方法详解特点:
是一个阻塞方法,任务没完成就会卡住;
会抛出两种重要异常:
异常类型 | 说明 |
---|---|
ExecutionException |
任务内部抛异常,被包装后抛出 |
InterruptedException |
当前线程在等待中被中断 |
获取原始异常的方式:
try {
future.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // 原始异常
}
future.cancel(true);
参数说明:
true
:如果任务正在运行,尝试中断;
false
:不去中断已经在运行的任务,只取消尚未开始的任务。
结果判断:
cancel()
返回值:
true
:取消成功;
false
:任务已完成或已经取消;
future.isCancelled()
:是否已取消;
future.isDone()
:是否已完成(包含正常完成、异常、取消三种状态)
invokeAll()
与批量 Future
List> tasks = Arrays.asList(
() -> "A", () -> "B", () -> "C"
);
List> futures = executor.invokeAll(tasks);
for (Future future : futures) {
System.out.println(future.get());
}
每个任务的执行结果会通过对应的 Future
对象返回;
所有任务都执行完,才会继续往下走。
场景 | 描述 |
---|---|
加密、压缩等异步计算任务 | 计算结果通过 Future 返回 |
并行网络请求 | 多个 Callable 并发发起 HTTP 请求 |
控制超时逻辑 | .get(timeout) 控制最长等待时间 |
可取消的任务(如上传、导出) | cancel() 取消任务并判断状态 |
常见行为:
某个任务由 Callable
执行,返回 Future
主线程或回调中调用 .get()
拿结果
若出现 cancel()
说明有可中断任务(上传、检测等)
逆向识别点:
Java 代码:
Future 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()
→ 判断任务是否支持中断或被恶意控制。
1)Future.get()
是阻塞的吗?
是,直到任务完成或超时。
2)任务执行异常会抛出吗?
会,但封装为 ExecutionException
,通过 getCause()
拿原始异常。
3)任务取消了还能 get 吗?
不行,会抛 CancellationException
。
4)cancel(true) 和 cancel(false) 区别?
true
:正在运行的任务也尝试中断;
false
:仅取消还没开始的任务。
项目 | 说明 |
---|---|
是否有返回值 | 是(用于接收异步任务结果) |
是否支持超时等待 | .get(timeout) |
是否能取消任务 | .cancel(true/false) |
get() 是否阻塞 | 是 |
配合使用对象 | Callable + ExecutorService |
在逆向中的价值 | 找出后台执行逻辑、异步任务的开始与结束、加密/上传行为的完成点 |
死锁定义:当多个线程在互相等待对方持有的锁,且都不主动释放,导致永远阻塞,无法继续执行,就产生了死锁。
死锁成立的四个必要条件(操作系统级定义):
条件 | 解释 |
---|---|
互斥 | 资源每次只能被一个线程占用 |
不可抢占 | 已获取资源不能被强行剥夺 |
占有且等待 | 已占有资源的线程还想再申请其它资源 |
循环等待 | 若干线程形成循环等待链(A 等 B,B 等 A) |
只要满足这四个条件,系统就有可能死锁。
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
// 然后死锁,两个线程互相等待
1)使用 jstack 命令排查死锁
jstack
找出 "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 出品)
可以实时查看线程堆栈、锁持有情况、卡顿线程等。
场景类型 | 描述 |
---|---|
多线程顺序反转锁 | 两个线程获取资源顺序不一致 |
synchronized 嵌套过多 | 多层嵌套导致死锁链 |
多线程递归调用 | 某线程在调用回调中死锁 |
数据库连接池耗尽 | 全部连接被死锁阻塞无法释放 |
多线程 + 自定义锁 | 锁组合或多把锁导致交叉死锁 |
策略 | 原理 |
---|---|
锁顺序一致 | 所有线程按相同顺序加锁 |
使用 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();
}
}
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 调用栈,检查是否有递归锁或线程等待。
场景:上传模块出现卡顿
Java 线程负责文件上传,使用同步锁维护上传状态;
回调中 JNI 通知底层 C 逻辑等待;
C 层使用锁等待 Java 返回结果 → 死锁!
原因链:
Thread A(Java上传) — 获取 Lock1,等待 C 回调完成
Thread B(C层回调) — 获取 Lock2,等待 Java 上传状态完成
==> 死锁
项目 | 说明 |
---|---|
死锁定义 | 多线程循环等待资源,永远阻塞 |
必要条件 | 互斥、不可抢占、占有且等待、循环等待 |
模拟方式 | 多线程 + 多锁 + 顺序颠倒 |
分析工具 | jstack、VisualVM、Arthas、Frida |
避免策略 | 锁顺序统一、tryLock、减少锁数量 |
逆向分析线索 | wait() 、synchronized 嵌套、线程阻塞不动 |
Native 层死锁识别 | JNI / NDK 层互相等待,或 pthread 死锁 |
当多个线程访问共享资源时,如果没有正确的同步机制,会出现 数据错误、状态混乱、异常崩溃 等问题,我们就称之为“线程不安全”。
常见线程安全问题类型:
问题类型 | 描述 |
---|---|
脏读 / 写穿 | 一个线程读到另一个线程未提交的修改 |
数据竞争(Race Condition) | 多线程同时修改数据,顺序不确定,导致错误 |
原子性缺失 | 多步操作未加锁,不能保证“整体不可中断” |
可见性问题 | 一个线程修改变量后,另一个线程看不到 |
指令重排序 | 编译器/CPU 重新安排执行顺序,导致异常结果 |
死锁 | 多线程互相等待对方释放锁 |
活锁 / 饥饿 | 线程持续运行但无法前进 / 长期得不到执行机会 |
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 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
;
或用 DateTimeFormatter
(Java 8)。
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();
检查点:
检查内容 | 如果存在… |
---|---|
是否访问共享变量(static 或成员变量) | ✔ 非线程安全 |
是否操作集合类(如 ArrayList、HashMap) | ✔ 非线程安全(默认) |
是否有多线程并发入口点 | ✔ 需要加锁或替代 |
是否使用线程安全类 | ✘ 用 synchronized 或线程安全类代替 |
是否涉及写入操作 | ✔ 需注意原子性、可见性、顺序性问题 |
应用场景:
场景 | 风险或用途 |
---|---|
后台线程异步加密任务共享数据 | 多线程对同一缓冲区或密钥池操作冲突 |
回调队列多个线程消费 | 如果未加锁,可能造成数据错乱或崩溃 |
上传模块文件写入 | 并发写同一文件或日志,产生冲突 |
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
等);
项目 | 内容 |
---|---|
线程安全问题表现 | 数据错乱、死锁、崩溃、卡顿 |
常见场景 | 多线程共享变量 / 集合 / I/O |
判断方法 | 静态分析、并发测试、JVM 工具 |
修复方法 | 加锁、用原子类、使用线程安全集合 |
逆向中识别点 | 多线程任务执行逻辑 + 无锁访问共享变量 |
Frida 应用 | 动态观察变量并发读写行为,排查竞态风险 |
异步任务:由主线程发起、交由后台线程或线程池执行、不阻塞主流程的操作。
逆向目标:识别这些异步任务是谁触发的、在哪里执行的、执行了什么关键逻辑(如加密、校验、上传、反调试)
从 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);
};
});
模块 | 异步任务用途 |
---|---|
登录加密 | 加密在后台线程中完成 |
数据上报(埋点) | 后台收集并异步发出 |
签名 / 防篡改 | 异步生成签名,提高并发 |
反调试延迟检测 | 延迟几秒后检查 Frida、Magisk |
广告或推送拉取 | 异步请求接口或更新信息 |
配置更新 / 动态模块加载 | 异步拉配置,隐藏敏感逻辑 |
图片 / 文件上传 | 异步 IO 操作 |
步骤一:定位提交任务的位置(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));
}
});
一些 APP 会通过延迟触发异步任务,规避动态调试:
new Handler().postDelayed(() -> {
if (isDebuggerConnected()) {
// kill or obfuscate
}
}, 5000);
可以 Hook postDelayed()
找到这种延迟的反调试检测。
识别方法 | 描述 |
---|---|
Java 静态分析 | 查找 Thread , submit , post , execute 等代码 |
Smali 分析 | 识别 invoke-* 中对异步类的调用 |
Frida Hook | 动态打印 start() , submit() , run() |
异步任务常用模块 | 加密、上报、配置、反调试、网络请求等 |
如何确认敏感逻辑 | 看是否有加密、HTTP、JNI、反调试等关键行为 |
延迟执行线索 | postDelayed , TimerTask , sleep 等控制行为 |