12.9八股面经案例学习

12.9八股面经案例学习


如何保证线程安全?

解答

Java 保证线程安全主要依靠三类机制:互斥 (如 synchronized、ReentrantLock 保证同一时刻只有一个线程进入临界区)、可见性与有序性 (如 volatile 禁止指令重排、确保修改对其他线程立即可见)、以及 原子性(如 Atomic 原子类基于 CAS 无锁更新避免竞争)。这些构成 Java 并发的底层基础。

实际开发中一般通过 并发安全的容器和工具类 (如 ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue)、线程封闭与不可变对象 、以及 线程池(Executor 框架) 来构建整体线程安全的体系。在读多写少场景常用 ReentrantReadWriteLock,在任务协作使用 CountDownLatch、Semaphore 等,从语言、容器到框架层层保证程序在多线程环境下运行正确且高效。


详解

互斥

句子互斥(如 synchronized、ReentrantLock 保证同一时刻只有一个线程进入临界区)

  • 意义:互斥是为了解决竞态条件(两个或更多线程同时修改共享状态导致结果不确定)。互斥保证在临界区的执行有序、互相排斥。

  • synchronized

    • 作用:取得对象或类的监视器(monitor),进入临界区;退出时释放锁。

    • 特性:可重入(同一线程可重复获得),由 JVM 管理,自动释放(异常也会释放)。

    • 例子:

      java 复制代码
      public synchronized void add(int x) {
          this.count += x;
      }

  • ReentrantLock

    • 作用:java.util.concurrent.locks 下的显式锁。

    • 优点:可中断锁获取(lockInterruptibly)、可尝试获取(tryLock)、可选择公平策略、能结合条件变量(Condition)。

    • 要点:要手动 unlock,通常放在 finally 块。

    • 例子:

      java 复制代码
      lock.lock();
      try {
          // 临界区
      } finally {
          lock.unlock();
      }

可见性与有序性

句子可见性与有序性(如 volatile 禁止指令重排、确保修改对其他线程立即可见)

  • 问题来源:处理器/编译器/JVM 都会做优化(寄存器缓存、指令重排等),导致一个线程对共享变量的写对其他线程不可见或执行顺序被打乱。

  • volatile​ 的语义:

    • 可见性:写入 volatile 的值,会被立即刷新到主内存,随后读 volatile 的线程能看到最新值。
    • 禁止特定重排序:volatile 写之前的操作不会被重排到 volatile 写之后,volatile 读之后的操作不会被重排到 volatile 读之前。(形成 happens-before 关系)
    • 不保证原子性:volatile count; count++ 仍然是读-改-写三步,非原子。
  • 常见用法:标志位(stop flag)、双重检验锁(DCL)里保证 instance​ 的可见性(配合 volatile​)。

原子性

句子原子性(如 Atomic 原子类基于 CAS 无锁更新避免竞争)

  • 原子操作:一个不可被线程调度中断的操作(要么全部执行,要么完全不执行)。

  • AtomicInteger​、AtomicReference​ 等:

    • 基于 CPU 的 CAS(Compare-And-Swap)实现:比较内存中当前值和预期值,相等则写入新值;否则失败并可重试。

    • 优点:非阻塞,性能好(在低到中等争用下优于锁)。

    • 例子:

      java 复制代码
      AtomicInteger ai = new AtomicInteger(0);
      ai.incrementAndGet(); // 原子递增
  • CAS 的问题:

    • ABA 问题:值从 A -> B -> A,会让 CAS 误判;解决方案有 AtomicStampedReference(带版本号)或使用更高层次的同步。
  • 重要:某些复杂操作仍需锁或原子组合(例如同时对两个变量进行一致性更新)。


并发安全的容器和工具类

句子并发安全的容器和工具类(如 ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue)

  • ConcurrentHashMap

    • 高层次语义:并发读写优化,不像 Hashtable 那样 global 锁。
    • JDK8 实现要点:使用 CAS + synchronized(在桶上)+ 链表/树化;读操作通常无锁。
    • 场景:高并发读写、计数(配合 compute/computeIfAbsent 等原子方法)。
  • CopyOnWriteArrayList

    • 写时复制:写(add/remove)会复制底层数组,读操作无锁、非常快。
    • 优点:读多写少场景(如监听器列表)。
    • 缺点:写成本高、内存消耗大,不适合频繁写。
  • BlockingQueue​(如 ArrayBlockingQueue​, LinkedBlockingQueue​)

    • 用于生产者-消费者:put/take 会在队列空/满时阻塞,天然线程安全且易用。

    • 示例(简单生产者-消费者):

      java 复制代码
      BlockingQueue<String> q = new ArrayBlockingQueue<>(100);
      // 生产线程: q.put(item);
      // 消费线程: String item = q.take();

线程封闭与不可变对象

  • 不可变对象(immutable)

    • 一旦构造完成,其状态不可改变,天然线程安全(如 String, Integer)。
    • 制作不可变类要点:字段 final、不提供修改方法、构造中深拷贝可变输入。
  • 线程封闭(thread confinement)

    • 把数据限定在某个线程内部(局部变量、线程私有对象),无需同步。
  • ThreadLocal

    • 为每个线程保存独立副本,适合保存线程相关状态(如数据库连接上下文、格式化器等)。

线程池(Executor 框架)

  • 为什么用线程池:减少线程频繁创建销毁开销、控制并发数、统一管理异常与任务调度、提供可配置队列/拒绝策略。

  • 常用类型:newFixedThreadPool​(固定线程数)、newCachedThreadPool​(弹性线程池)、ScheduledThreadPoolExecutor​(定时任务)。

读多写少:ReentrantReadWriteLock

  • 语义:读锁允许多线程并发读取,写锁排他。适合读远多于写的场景。
  • 要点:写锁时会阻塞所有读;有"公平/非公平"策略;要注意避免写操作长时间占用导致读阻塞。

线程协作工具

  • CountDownLatch​:一次性等待 N 个事件完成(不可重用)。

    java 复制代码
    CountDownLatch latch = new CountDownLatch(N);
    // 工作线程: latch.countDown();
    // 主线程: latch.await();
  • CyclicBarrier​:等待一组线程都到达某点后再一起继续(可重复使用)。

  • Semaphore​:控制并发许可数(比如控制并发访问资源的数量)。

  • Exchanger​、Phaser​ 等:更复杂的协作场景。


常见错误/陷阱

  1. 忘记 unlock / 忘记在 finally 中释放锁 → 容易死锁或线程挂死。
  2. 误用 volatile 以为能保证复合操作的原子性 (例如 volatile int cnt; cnt++ 仍不安全)。
  3. 错误地把锁对象用为可变对象或包装类型 (例如 synchronized(Integer.valueOf(x))),会导致锁共享或不可预期。
  4. 发布逸出(this 在构造中被泄露) :构造期间把对象引用交给其他线程,导致不完全初始化就被访问。
  5. 死锁:多把锁按不同顺序请求会死锁。防范:按照固定顺序获取锁、使用 tryLock + 超时回退、减少锁粒度。
  6. 滥用 CopyOnWriteArrayList 在写多场景会性能崩塌。

面试常问追问(以及简短回答要点)

  • Q: synchronizedReentrantLock 的区别?
    A: synchronized 简洁、由 JVM 管理、自动释放;ReentrantLock 更灵活(可中断、公平、tryLock、Condition),需手动 unlock。
  • Q: volatile 的 happens-before 是什么?
    A: 写volatile happens-before 后续读volatile,保证写对读可见并禁止特定重排序。
  • Q: CAS 的 ABA 问题怎么解决?
    A: 用带版本号的引用(AtomicStampedReference/AtomicMarkableReference)或在更高层用锁。
  • Q: ConcurrentHashMap 如何做到并发?
    A: JDK8 用 CAS + synchronized(桶/链表锁定) + 链表树化设计,读操作无锁。

新生代和老生代的区别

解答

Java 堆分为新生代和老生代。新生代主要存放新建对象 ,采用 复制算法 ,回收频繁且速度快;老生代存放生命周期长、晋升过来的对象 ,采用 标记-清除/标记-压缩算法,回收次数少但耗时长。两者配合让 GC 既快又高效。


详解

新生代

特点:

  1. 绝大部分对象刚创建就会很快死亡

    例如短期字符串、临时变量等。

  2. 新生代又分为:

    • Eden 区(大部分对象在这里创建)
    • Survivor 区(S0、S1 两个,为对象存活交换使用)
  3. 回收策略:复制算法(Copying)

    含义:把存活对象从一个区域复制到另一个区域,然后清空旧区域。

    优点:

    • 效率很高
    • 简单
    • 不会产生内存碎片
  4. 触发频率:Minor GC(频繁)

    因为新生代小、对象变化快。


老生代

特点:

  1. 在新生代经历多次 GC 仍然存活的对象,会被晋升到老生代

    常见场景:

    • 单例对象
    • 线程池、连接池对象
    • 缓存对象
    • 业务中长期存在的容器对象
  2. 核心特点:对象存活率高。

  3. 回收策略:

    • 标记-清除(Mark-Sweep)
    • 标记-整理/压缩(Mark-Compact)

    这些算法比复制算法更复杂,也更慢。

  4. 触发频率:Major GC / Full GC(次数少,但耗时长)


项目 新生代(Young) 老生代(Old)
存储对象 新建对象、短生命周期 长生命周期对象
GC 名称 Minor GC Major/Full GC
回收频率 非常频繁 很少
回收速度
回收算法 复制算法 标记清除 / 标记整理
对象死亡率
注意点 内存小、快速回收 一旦满了会触发 Full GC(可能卡顿)

为什么要分新生代和老生代?

简单回答:

因为大多数对象朝生夕死,不必使用复杂GC算法;而少数长期存活对象才需要老生代的慢 GC。分代管理可以让 GC 的整体性能最大化。


Java中的String和C++有什么不同

解答

Java 的 String 是不可变对象(immutable),存放在堆里、带字符串常量池机制;C++ 的 std::string 是可变的普通类,直接管理一块连续内存,没有常量池。

详解

Java String:不可变、常量池、对象语义

不可变

Java 的 String 一旦创建,内容不能改变

java 复制代码
String s = "abc";
s = s + "d"; // 其实创建了新对象 "abcd"

特点:

  • 线程安全(因为不能改)
  • 可缓存(可以放进常量池复用)
  • 适合作为 HashMap 的 key
  • 字符串拼接需要注意性能(推荐 StringBuilder)

字符串常量池(String Pool)

Java 为了节省内存,引入了 String Pool。

java 复制代码
String a = "hello";
String b = "hello"; // a 和 b 指向同一个对象

这种"同值字符串复用"只有 Java 有,C++ 没有。


Java String 是一个特殊的对象

底层本质是 final char[](或 byte[])

它的不可变是语言级别 + JDK 实现层面的设计,保证每个String安全可靠。


为什么 Java String 要不可变?

答:

  • 保证线程安全
  • 提升性能(常量池复用)
  • 能作为 HashMap key(hash 不改变)
  • 安全(ClassLoader、URL 等一些敏感参数不能被改)

说一说网站中如何进行反爬虫

解答

网站反爬虫的核心就是:识别"机器行为",限制"异常流量",并保护"关键数据"。手段分为基础限制(UA、IP、频率)、行为识别(速度、路径、操作习惯)、技术校验(Cookie、Token、验证码、加密)、以及服务端的大数据风控策略。

详解

基础反爬(简单但有效)

这种策略成本低,大部分网站都会直接上。

① User-Agent 判断

拦截明显的爬虫 UA,例如:

复制代码
python-requests
scrapy
curl/7.29

缺点:很容易伪造。


② IP 限流(Rate Limit)

同一 IP 在短时间内访问超过阈值 → 封禁或限速。

例子:

  • 1 分钟超过 100 次访问 → 拉黑
  • 同一接口 QPS > 10 → 限流

缺点:IP 可以更换(代理池)。


③ Referer 校验

只允许从本站页面跳转,不允许外部直接访问接口。

缺点:Referer 也可伪造。


④ Robots.txt

告诉正常爬虫不要抓哪些目录(如搜索引擎友好)。

缺点:恶意爬虫完全不遵守。


2. 技术反爬(高级反爬虫)

对机器来说非常难突破。

  • 首次访问生成 Cookie
  • 要求后续请求携带
  • 判断 Cookie 生命周期、序列化、加密方式防作弊

② 请求参数加密/签名(Sign)

常见方式:

  • MD5 签名
  • AES/RC4 加密参数
  • timestamp + 随机数
  • 多个混淆字段让爬虫难以伪造

例如:

复制代码
sign = md5(参数 + 时间戳 + 密钥)

爬虫无法知道密钥 → 请求被拒绝。


③ 验证码(强力阻断)

尤其是:

  • 普通文本验证码
  • 滑块验证码
  • 点选验证码
  • 行为验证码(声称"智能验证")

爬虫难以绕过,除非用 OCR + 人机协作成本极高。


④ JS 动态生成关键参数(JS 混淆)

浏览器执行一段复杂 JS 代码,生成:

  • token
  • cookie
  • signature
  • dynamic-timestamp
  • 验证字段

爬虫想模拟 浏览器 + JS 执行 成本非常高

尤其是 混淆 JS(比如 360、Baidu、某些大厂常用)。


⑤ 前端混淆与加密

  • JS 混淆(Obfuscator)
  • 属性名随机
  • API 路由动态
  • 参数 AES 混淆

让爬虫很难逆向出数据接口。


行为/流量风控(高级)

这是大网站(抖音、淘宝、小红书)常用的核心手段。

系统会从多个维度判断用户是不是机器人:

① 访问速度

机器请求一般非常快:

  • 10ms、20ms 连续发请求
  • 人类行为不可能如此稳定

② 页面停留时间

用户打开页面------几秒/几十秒

爬虫打开页面------0秒瞬间访问下一页


③ 访问路径

正常用户:

复制代码
列表页 -> 商品页 -> 详情页 -> 返回列表 -> 切换分类 -> ...

爬虫:

复制代码
列表页 -> 列表页 -> 列表页 -> ...

路径非常"机械化"。


相关推荐
西岸行者2 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意2 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码2 天前
嵌入式学习路线
学习
毛小茛2 天前
计算机系统概论——校验码
学习
babe小鑫2 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms2 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下2 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。2 天前
2026.2.25监控学习
学习
im_AMBER2 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J2 天前
从“Hello World“ 开始 C++
c语言·c++·学习