关于 java:7. 多线程与并发编程

一、Thread 类

作用:Thread 类代表一个线程,用于创建和控制一个新的执行流(即“子线程”)。

定义:java.lang.Thread 类实现了 Runnable 接口。

1.1 使用方式

方法一:继承 Thread

步骤:

  1. 自定义类继承 Thread

  2. 重写 run() 方法。

  3. 创建线程对象并调用 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();
    }
}

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.StateisAlive()
逆向重点关注点 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;->(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 接口配合 Future 使用。

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 或全局变量,防止外部代码误用锁。

  • 多线程下需要 共享的资源才需要加锁,不共享就不必同步。

  • 能用 ConcurrentHashMapAtomicXXX 替代锁的场景,尽量不用锁。

3.11 小结

项目 说明
本质 基于 JVM Monitor 的内置锁
用法 代码块、实例方法、静态方法
锁对象 任意非 null 对象
是否可重入  是
是否可见性/原子性  有可见性  有原子性
是否阻塞其他线程  是
逆向识别点 monitor-enter/exit,配合分析共享资源逻辑

四、volatile

定义:volatile 是 Java 提供的轻量级同步机制,用于修饰变量,保证线程间的 可见性 和一定程度的 有序性

4.1 volatile 解决了什么问题?

背景:在 Java 中,每个线程有自己的工作内存(CPU 缓存),线程可能不会直接从主内存读取变量的值。

这就会导致:

// 主线程设置 running = false
// 工作线程却还在执行 while(running)

volatile 的作用:

  1. 可见性:一个线程修改变量,其他线程能立即看到。

  2. 禁止指令重排(有序性):保证写操作发生在读操作之前。

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 flagvolatile int status
线程通信变量 某变量决定线程是否继续、是否初始化完成
双重检查单例 if (instance == null) 的场景中用 volatile 修饰 instance

逆向定位技巧

1)使用 jadxFernflower 查看 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 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 是 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:
 └── table:Node[] 数组(核心结构)
       └── 每个桶是一个 Node 链表(冲突时链式存储,链表长了会转红黑树)

插入流程核心步骤:

  • 根据 key 计算 hash 值;

  • 定位到 table 数组的桶位置;

  • 如果为空,使用 CAS(无锁)写入新节点

  • 如果已有节点,进入链表/树结构:

    • 同步加锁(使用 synchronized);

    • 插入节点;

    • 若链表太长,转为红黑树。

高并发性能的来源:

  • 空桶插入使用 CAS(无需加锁);

  • 桶内冲突才使用 synchronized,锁粒度更细;

  • 读操作不加锁,使用 volatile + 内存语义 保证可见性;

  • 分段并发,支持多个线程同时写不同桶。

6.4 常用方法

ConcurrentHashMap 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 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 sessionMap = new ConcurrentHashMap<>();

2)smali 反编译:

invoke-direct {v0}, Ljava/util/concurrent/ConcurrentHashMap;->()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 extends Queue { ... }

7.1 核心特性

特性 说明
线程安全  内部自动加锁(ReentrantLock)
支持阻塞操作  插入或获取元素时,可自动等待资源就绪
有界 / 无界  支持设置队列最大容量
多线程通信  常用于生产者-消费者模型
常见实现类 ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueueSynchronousQueue

7.2 核心方法分类

操作 抛出异常 返回特殊值 阻塞等待 限时等待
插入元素 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(); // 若空则阻塞

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 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 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、任务缓存、解密队列、线程间通信
常见实现类 LinkedBlockingQueueArrayBlockingQueueSynchronousQueue

八、ExecutorService

定义:ExecutorServicejava.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) 同时提交多个任务,全部完成后返回结果列表
invokeAny(Collection) 提交多个任务,只取第一个成功返回的结果

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 task = () -> {
    Thread.sleep(1000);
    return 42;
};

Future 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 是 Java 5 引入的接口,用于表示可以在线程中执行并返回结果的任务。它是 java.util.concurrent 包的一部分,弥补了 Runnable 只能执行不能返回结果的缺陷。

和 Runnable 区别:

特性 Runnable Callable
是否有返回值  无  有
是否支持抛异常  否  支持抛出异常(Checked)
提交方式 execute() submit() → 返回 Future
接口方法 run() V call() throws Exception

9.1 Callable 的接口定义

@FunctionalInterface
public interface Callable {
    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 task = () -> {
            Thread.sleep(1000);
            return 42;
        };

        Future future = executor.submit(task);

        System.out.println("主线程继续执行...");
        Integer result = future.get(); // 会阻塞直到结果可用
        System.out.println("异步任务结果:" + result);

        executor.shutdown();
    }
}

9.3 Future 接口:获取任务结果的桥梁

Future future = executor.submit(callable);
V result = future.get(); // 阻塞等待返回结果

Future 常用方法:

方法 含义
get() 等待任务完成,返回结果
get(timeout, unit) 限时等待结果,超时报错
isDone() 判断任务是否已完成
cancel(true) 取消任务(可中断)

9.4 多个 Callable 的并发执行(批处理)

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());
}

9.5 与 Runnable 的转换技巧

从 Runnable 变成 Callable:

Runnable task = () -> System.out.println("任务执行");
Callable callable = () -> {
    task.run();
    return null;
};

9.6 异常处理方式

  • 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(); // 拿到原始异常
}

9.7 在逆向工程中的使用线索

典型场景:

场景 描述
异步任务提交 后台耗时计算、网络请求、加密
上传/上报任务回调 等待执行结果再处理后续逻辑
动态模块加载 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() → 定位哪些任务被封装为异步处理

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 是 Java 提供的一个接口,表示一个异步计算的结果。它是任务提交(通常是 Callable)之后,用来 获取结果 / 取消任务 / 检查是否完成 的控制句柄。

10.1 与 Callable 的配合关系

ExecutorService executor = Executors.newSingleThreadExecutor();
Callable task = () -> 42;
Future future = executor.submit(task); // 提交后立即返回 Future

10.2 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);

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> tasks = Arrays.asList(
    () -> "A", () -> "B", () -> "C"
);
List> futures = executor.invokeAll(tasks);

for (Future future : futures) {
    System.out.println(future.get());
}
  • 每个任务的执行结果会通过对应的 Future 对象返回;

  • 所有任务都执行完,才会继续往下走。

10.6 应用场景举例

场景 描述
加密、压缩等异步计算任务 计算结果通过 Future 返回
并行网络请求 多个 Callable 并发发起 HTTP 请求
控制超时逻辑 .get(timeout) 控制最长等待时间
可取消的任务(如上传、导出) cancel() 取消任务并判断状态

10.7 逆向分析中的使用线索

常见行为:

  • 某个任务由 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() → 判断任务是否支持中断或被恶意控制。

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 

找出 "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 list = new ArrayList<>();

Runnable task = () -> {
    for (int i = 0; i < 1000; i++) {
        list.add("data");  // 非线程安全
    }
};

结果:

  • ArrayList 在扩容时可能发生 越界异常数据覆盖丢失

解决方案:

  • CopyOnWriteArrayListCollections.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)。

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 等变量;

  • synchronizedvolatileAtomic* 类出现或缺失;

  • 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 等控制行为

你可能感兴趣的:(java,开发语言)