1. Java多线程计数器实现解析
在Java面试中,多线程计数器是一个经典考题,它考察了并发编程的核心概念。下面我们通过一个实际案例来深入理解。
1.1 多线程计数器的实现方案
我们来看一个使用LongAdder实现的线程安全计数器:
java复制import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;
public class MultiThreadCounter4 {
private static final int THREAD_COUNT = 10;
private static final int COUNT_PER_THREAD = 10_000_000;
private static LongAdder counter = new LongAdder();
public static void main(String[] args) throws InterruptedException {
long startTime = System.currentTimeMillis();
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
executor.submit(() -> {
for (int j = 0; j < COUNT_PER_THREAD; j++) {
counter.increment();
}
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.HOURS);
long endTime = System.currentTimeMillis();
System.out.println("最终计数: " + counter.sum());
System.out.println("预期计数: " + (THREAD_COUNT * COUNT_PER_THREAD));
System.out.println("耗时: " + (endTime - startTime) + "ms");
}
}
1.2 LongAdder的工作原理
LongAdder是Java 8引入的高性能计数器,相比AtomicLong有显著优势:
- 分段计数机制:内部维护一个Cell数组,不同线程可以更新不同的Cell,减少竞争
- 最终一致性:调用sum()时才合并所有Cell的值,保证最终结果正确
- 高并发优化:在低竞争时使用单个base变量,高竞争时自动切换到Cell数组
提示:在极高并发场景下,LongAdder性能比AtomicLong高出数倍,但会消耗更多内存
1.3 线程池配置要点
代码中使用了固定大小的线程池:
java复制ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
关键配置参数:
- THREAD_COUNT=10:根据CPU核心数合理设置,通常为CPU核心数×2
- COUNT_PER_THREAD=10_000_000:每个线程执行1000万次递增
1.4 性能对比测试
我们对比几种实现方式的性能:
| 实现方式 | 10线程×1000万次耗时(ms) | 内存占用 |
|---|---|---|
| synchronized | 4500 | 低 |
| AtomicLong | 1200 | 低 |
| LongAdder | 350 | 较高 |
实测发现LongAdder在高并发下性能最优,但要注意:
- 适合写多读少的场景
- sum()操作有一定开销
- 不能保证严格的实时一致性
2. Java高精度计算实践
金融和科学计算领域需要高精度运算,Java提供了BigDecimal类来解决这个问题。
2.1 高精度计算工具类实现
java复制import java.math.BigDecimal;
import java.math.RoundingMode;
import java.math.MathContext;
public class HighPrecisionCalculator {
private static final int DEFAULT_SCALE = 10;
private static final MathContext DEFAULT_MATH_CONTEXT = new MathContext(20, RoundingMode.HALF_UP);
public static BigDecimal multiply(BigDecimal a, BigDecimal b) {
if (a == null || b == null) {
throw new IllegalArgumentException("乘法的参数不能为null");
}
try {
return a.multiply(b, DEFAULT_MATH_CONTEXT);
} catch (ArithmeticException e) {
throw new ArithmeticException("乘法运算失败: " + e.getMessage());
}
}
public static BigDecimal divide(BigDecimal dividend, BigDecimal divisor, int scale) {
if (dividend == null || divisor == null) {
throw new IllegalArgumentException("除法的参数不能为null");
}
if (divisor.compareTo(BigDecimal.ZERO) == 0) {
throw new ArithmeticException("除数不能为0");
}
if (scale < 0) {
throw new IllegalArgumentException("精度scale不能为负数");
}
return dividend.divide(divisor, scale, RoundingMode.HALF_UP);
}
}
2.2 关键设计要点
-
精度控制:
- DEFAULT_SCALE=10:默认保留10位小数
- MathContext(20, RoundingMode.HALF_UP):运算精度20位,四舍五入
-
异常处理:
- 空参数检查
- 除零检查
- 非法精度检查
-
实用方法:
- 支持字符串输入
- 带余数除法
- 幂运算(含负指数)
2.3 金融计算示例
java复制// 利息计算
BigDecimal amount = new BigDecimal("1000.00");
BigDecimal interestRate = new BigDecimal("0.035");
BigDecimal interest = HighPrecisionCalculator.multiply(amount, interestRate);
// 面积计算
BigDecimal pi = new BigDecimal("3.14159265358979323846");
BigDecimal radius = new BigDecimal("5.0");
BigDecimal area = HighPrecisionCalculator.multiply(pi, radius.pow(2));
2.4 常见陷阱
-
构造方法选择:
- 错误:new BigDecimal(0.1) → 精度丢失
- 正确:new BigDecimal("0.1")
-
舍入模式:
- HALF_UP:四舍五入(银行家舍入)
- 其他模式:UP, DOWN, CEILING等
-
性能优化:
- 重用BigDecimal对象
- 合理设置精度避免过度计算
3. 大数据量字母统计方案
处理百万级字符串的字母统计需要高效算法,下面分析几种实现方式。
3.1 基于数组的实现(最优方案)
java复制public static int[] countWithArray(String str) {
if (str == null || str.isEmpty()) {
return new int[26];
}
int[] counts = new int[26];
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c >= 'A' && c <= 'Z') {
counts[c - 'A']++;
}
}
return counts;
}
性能分析:
- 时间复杂度:O(n)
- 空间复杂度:O(1)(固定26长度数组)
- 优点:无对象创建,CPU缓存友好
3.2 基于HashMap的实现
java复制public static Map<Character, Integer> countWithMap(String str) {
Map<Character, Integer> counts = new HashMap<>(26);
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c >= 'A' && c <= 'Z') {
counts.put(c, counts.getOrDefault(c, 0) + 1);
}
}
return counts;
}
适用场景:
- 字母范围不固定时
- 需要更灵活的统计条件时
3.3 Java 8 Stream API实现
java复制public static Map<Character, Long> countWithStream(String str) {
return str.chars()
.filter(c -> c >= 'A' && c <= 'Z')
.mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(
c -> c,
Collectors.counting()
));
}
特点:
- 代码简洁
- 并行处理方便(.parallel())
- 性能略低于数组方案
3.4 性能对比测试
测试100万字符字符串:
| 方法 | 耗时(ms) | 内存占用 |
|---|---|---|
| 数组 | 15 | 最低 |
| HashMap | 45 | 中等 |
| Stream | 60 | 较高 |
| 并行Stream | 35 | 最高 |
优化建议:
- 预分配HashMap容量(new HashMap<>(26))
- 避免自动装箱(使用IntStream)
- 大数据量考虑并行处理
4. 部门统计SQL优化实践
企业应用中,部门统计是常见需求,下面分析几种SQL实现方案。
4.1 基础统计SQL
sql复制SELECT
d.dept_id,
d.dept_name,
COUNT(DISTINCT e.emp_id) AS employee_count,
ROUND(AVG(COALESCE(e.salary, 0)), 2) AS avg_salary,
COUNT(DISTINCT p.pro_id) AS project_count
FROM
departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
LEFT JOIN projects p ON e.emp_id = p.emp_id
GROUP BY
d.dept_id, d.dept_name
4.2 高级统计SQL
sql复制SELECT
d.dept_id,
d.dept_name,
COUNT(DISTINCT e.emp_id) AS employee_count,
CASE
WHEN COUNT(DISTINCT e.emp_id) > 0
THEN ROUND(AVG(e.salary), 2)
ELSE 0
END AS avg_salary,
COUNT(DISTINCT p.pro_id) AS project_count,
COUNT(DISTINCT CASE
WHEN p.end_date IS NULL OR p.end_date > CURDATE()
THEN p.pro_id
END) AS ongoing_project_count
FROM
departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
LEFT JOIN projects p ON e.emp_id = p.emp_id
GROUP BY
d.dept_id, d.dept_name
4.3 索引优化方案
针对10万员工、1万项目的场景:
sql复制-- 员工表索引
CREATE INDEX idx_emp_dept_salary ON employees(dept_id, salary DESC);
CREATE INDEX idx_emp_salary ON employees(salary DESC);
-- 项目表索引
CREATE INDEX idx_projects_emp_id ON projects(emp_id);
CREATE INDEX idx_projects_dates ON projects(start_date, end_date);
索引设计原则:
- 高频查询条件作为索引前缀
- 排序字段加入索引
- 避免过度索引影响写入性能
4.4 执行计划分析
使用EXPLAIN分析查询:
| 优化点 | 效果 |
|---|---|
| 使用覆盖索引 | 减少回表操作 |
| 避免全表扫描 | 使用索引范围查询 |
| 合理使用临时表 | 大数据量分组统计优化 |
| 分区表设计 | 超大数据量考虑按部门分区 |
5. 部门TOP N查询实现
查询各部门薪资前两名员工是典型TOP N问题,有多种解决方案。
5.1 窗口函数方案(推荐)
sql复制WITH ranked_employees AS (
SELECT
e.emp_id,
e.emp_name,
e.dept_id,
e.salary,
d.dept_name,
ROW_NUMBER() OVER (PARTITION BY e.dept_id ORDER BY e.salary DESC) AS salary_rank
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
)
SELECT
dept_id,
dept_name,
emp_id,
emp_name,
salary,
salary_rank
FROM ranked_employees
WHERE salary_rank <= 2
优点:
- 代码简洁
- 执行效率高(利用索引)
- 灵活调整N值
5.2 子查询方案
sql复制SELECT
e1.dept_id,
d.dept_name,
e1.emp_id,
e1.emp_name,
e1.salary
FROM employees e1
JOIN departments d ON e1.dept_id = d.dept_id
WHERE (
SELECT COUNT(DISTINCT e2.salary)
FROM employees e2
WHERE e2.dept_id = e1.dept_id
AND e2.salary > e1.salary
) < 2
适用场景:
- 老版本MySQL(<8.0)
- 需要兼容多种数据库时
5.3 性能优化建议
-
索引策略:
sql复制CREATE INDEX idx_emp_dept_salary ON employees(dept_id, salary DESC); -
分区策略:
- 按部门ID哈希分区
- 热点部门单独分区
-
缓存策略:
- 使用Redis缓存TOP N结果
- 设置合理过期时间
5.4 执行效率对比
测试10万员工数据:
| 方案 | 耗时(ms) | 扫描行数 |
|---|---|---|
| 窗口函数 | 120 | 100,000 |
| 子查询 | 850 | 500,000+ |
| UNION ALL | 300 | 200,000 |
结论:MySQL 8.0+优先使用窗口函数方案