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秒瞬间访问下一页


③ 访问路径

正常用户:

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

爬虫:

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

路径非常"机械化"。


相关推荐
●VON1 天前
从零构建可扩展 Flutter 应用:v1.0 → v2.0 全代码详解 -《已适配开源鸿蒙》
学习·flutter·开源·openharmony·开源鸿蒙
(●—●)橘子……1 天前
3643.垂直翻转子矩阵 练习理解
笔记·python·学习·算法·leetcode·矩阵
Ada大侦探1 天前
新手小白学习PowerBI第四弹--------RFM模型建模以及饼图、分解树、树状图、增长趋势图的可视化
人工智能·学习·数据分析·powerbi
我命由我123451 天前
Java 开发使用 MyBatis PostgreSQL 问题:传入的参数为 null,CONCAT 函数无法推断参数的数据类型
java·开发语言·数据库·学习·postgresql·mybatis·学习方法
黑岚樱梦1 天前
Git学习和Linux基础
git·学习
呱呱巨基1 天前
Linux 进程概念
linux·c++·笔记·学习
yong15858553431 天前
2. Linux C++ muduo 库学习——原子变量操作头文件
linux·c++·学习
IDIOT___IDIOT1 天前
KNN and K-means 监督与非监督学习
学习·算法·kmeans
Rousson1 天前
硬件学习笔记--91 TMR型互感器介绍
笔记·学习