二本26届大三Java实习准备 Day7

2025 年 3 月 3 日 Day 7

今天终于把JUC的内容基本上过完了一遍;

  • 线程的基本概念
  • Synchronized
  • AQS
  • ReentrantLock
  • CAS
  • volatile
  • 线程池

ReentrantLock 和 synchronized 有什么区别?

ReentrantLock 是通过 java 实现的一个同步器框架; 而 Synchronized 则是 java 底层是特性; ReentrantLock 手动显式地获取和释放锁; Synchronized 通过声明需要进行同步的范围隐式地自动管理锁; ReentrantLock 支持公平和非公平两种锁; Synchronized 不支持公平锁; 相比于 Synchronized,ReentrantLock 有着更温和的中断和超时取消机制;

ReentrantLock 如何实现可重入性

通过 AQS 提供的状态只 state 实现的; 在每次获取锁时都会判断当前持有锁的线程是否为当前线程如果是则将 state 进行+1 操作; 每次释放锁时也要判断 state-1 后是否为 0 如果为 0 再释放锁唤醒其他队列节点

synchronized 和 reentrantLock 区别?

两者都是实现同步控制的实现方法, synchronized 是 java 内置提供的特性, ReentrantLock 则是用 java 代码实现的; synchronized 只能实现非公平锁, ReentrantLock 可以自行控制公平和非公平; synchronized 的加锁和释放锁都是隐式的, ReentrantLock 则是显式的控制; ReentrantLock 还支持超时等待, 响应中断等特性

ReentrantLock 的 tryLock () 方法有什么作用?和 lock () 有什么区别?

tryLock 会尝试通过无锁的方式获取锁; 提高了在并发较少的场景下的性能; lock 则是通过阻塞的方式获取锁

ReentrantLock 如何基于 AQS 实现?Sync、NonfairSync、FairSync 的关系是什么?

ReentrantLock 实现了 Lock 接口; ReentrantLock 中的 Sync 内部类继承了 AQS; 然后 Sync 又有两个实现类分别是公平和非公平锁; 在 AQS 中对同步器的方法进行了统一但是开放了 tryAcquire 和 tryRelease 方法提供给上层业务逻辑实现自己的业务获取和释放锁逻辑;

使用 ReentrantLock 的时候如何避免死锁? 出现死锁如何排查

如果在嵌套获取多把锁的场景中一定要排查锁的获取顺序, 避免多个线程同时等待获取对方线程中的锁; 还可以通过 tryLock 设置过期时间的方式避免可能出现的死锁

排查可以通 jps -l 查看故障进程号然后通过 jstack <进程号> 查看进程信息; 或者使用图形化工具 jconsole

什么是 Unsafe?

我们知道 java 如果想操作系统底层, 只能通过 native 方法实现; 但是 JVM 还是有个后门就是 Unsafe 类; 这个类提供了硬件级别的操作; 这个类的操作权限极高有时候可以跨越 java 的语言限制直接操作内存中某个变量即便在 java 中被定为 fianl 或者私有; 此外线程的挂起和唤醒、CAS 操作都是通过这个类实现的; 他带来强大功能和高效的同时也带来了很多安全隐患, 因为直接操作底层可能造成很多内存级别的失误, 所以一般只有在追求高效并且有能力保证数据安全的各种框架中才能看到 Unsafe; 因为他的不安全性所以在 JDK 23 后被移除了 (通过 varHandle API 来全面替代)

什么是 CAS

设计层面,CAS 是一种通过少量自旋等待来实现无锁获取锁的操作; 是乐观锁思想; 实现层面,CAS 是通过 Unsafe 类实现的; 底层则是通过 CMPXCHG 这个 CPU 原语级别的指令来确保原子性的; CAS 简单来说就是线程会先读取某个位置的值并记录下来, 然后尝试修改这个值; 修改之前会先比对此时的值和之前读取记录的值是否相同, 如果相同则说明这个过程中没有其他线程进行并发操作, 那么修改值并返回真; 如果修改时发现值改变了说明已经有线程进行了并发操作当前线程就会记录新的值然后进行一段时间的自旋等待然后重复尝试修改和自旋操作, 直到成功改变到变量值; 通过 CAS 的执行流程就引出了 CAS 的两个缺点; 第一就是自旋实际上就是为了避免重复的线程挂起和唤醒还有线程调度这些开销从而采用一定的 CPU 空转消耗来实现的; 如果同一时刻大量的线程同时自旋或者一个线程长时间持有一把锁那就会出现大量的 CPU 消耗;还有一个问题 ABA 问题简单来说就是线程 1 希望将一个位置上 A 的值变为 C, 但是这个过程中其他线程进入将 A 改为 B 然后可能有被其他线程将 B 改回 A, 那么线程 1 是无法感知这过程中的变化的; 解决这个问题可以通过添加版本号

CAS 是如何保证原子性的

CAS 是通过 CMPXCHG 这条 CPU 指令实现原语级别的原子操作; 在这条指令执行的过程中无法被中断无法被抢占

CAS 使用时有哪些问题

通过 CAS 的执行流程就引出了 CAS 的两个缺点; 第一就是自旋实际上就是为了避免重复的线程挂起和唤醒还有线程调度这些开销从而采用一定的 CPU 空转消耗来实现的; 如果同一时刻大量的线程同时自旋或者一个线程长时间持有一把锁那就会出现大量的 CPU 消耗; 还有一个问题 ABA 问题简单来说就是线程 1 希望将一个位置上 A 的值变为 C, 但是这个过程中其他线程进入将 A 改为 B 然后可能有被其他线程将 B 改回 A, 那么线程 1 是无法感知这过程中的变化的; 解决这个问题可以通过添加版本号

什么是 JMM, 什么是 JMM 的三大特性

JMM 是 java 内存模型; 他是为了屏蔽在不同硬件平台和操作系统上多线程操作内存的差异; JMM 通过对线程操作共享变量的规范来实现了可见、有序、和原子性; 可见性: 可见性规定所有的线程如果操作了主内存中的变量那么就有让这个操作同步让其他线程感知到; 最常见的可见性实现就是 volatile 原子性: 是指某一组指令在执行的过程中不允许被其他线程打断和抢占; 可以简单理解为 monitorenter 和 monitorexit 实现的锁机制 有序性: 程序的执行顺序不能因为重排序而造成在并发环境下的逻辑错误, 因为重排序只能保证单线程下的串行一致性; 最常见的就是单例的双重检查锁; 我们可以通过 volatile 关键字和 happens-before 来实现禁重排和重排的约束; 当然有序性在单线程情况下是天然满足的所以可以通过锁机制来实现

JMM 模型 alt

当一个线程需要读取主内存中变量时会将它拷贝到本地工作内存中; 如果一个线程修改了这个变量的值那么会先从在工作线程中修改然后再修改主内存中的值; 如果一个工作线程需要读取那么会先从主内存读取最新的值

什么是总线嗅探和总线风暴,和 JMM 有什么关系?

总线嗅探主要是为了实现 JMM 中的可见性; 当一个线程更新了主变量中的值, 那么总线嗅探就会感知到然后通知其他线程更新主内存中的最新值; 如果在短时间内突然残生大量的总线广播那么就会导致总线带宽被占满的情况; 解决方法就是在适当的时候通过锁来进行性能优化; ReentratLock 和 Synchronized 等锁都可以保证线程之间的原子性和可见性

介绍一下 volatile 关键字

volatile 是一个轻量级的并发环境下多线程共享数据的同步机制; 所以他解决的是并发场景下的两个问题一就是一个线程对变量的修改无法让其他线程感知到; 二就是重排序后的代码在并发场景中可能出现问题; 为了解决这两个问题, 提供可见性和有序性的 volatile 关键字孕育而生;

volatile 关键字的功能一就是提供了可见性; 功能就是一个线程对共享变量的修改其他线程能立刻感知到;具体实现就是在每次写操作之后都插入写屏障,让每次写入强行同步到主内存; 每次读操作之前插入读屏障,每次都从主内中重新加载; 而有序性则是明确了哪些指令不能被重排序; 通过插入内存屏障; 保证屏障后的操作不会被重排序到屏障之前,;

什么是内存屏障?

内存屏障也被称为内存栅栏; 是指在字节码文件中插入的标记, 这些标记是为了避免 CPU 和编译器对指令的重排, 通过插入特定的内存屏障以此来满足在并发环境下代码可以按照我们的意愿来进行; 内存屏障有四种 StroeStroe;laodStroe; loadLoad; storeLoad

volatile 是线程安全的吗?

volatile 并没有提供原子性也就是说不能保证线程安全; 比较常见的就是一个 volatile 修饰的变量进行自增或则自减操作; 如果多个线程同时进行, 那么可能会因为并发结果和预期不同; 比如 volatile int a = 1; a++; a++操作可以分成三部 1 将主内存中 a 的值读取到工作内存中; 2 将 a 的值+1; 3 将第二步计算的值赋值给 a; 因为 volatile 只是可见性但是不保证原子性那么可能线程 1 将 a 赋值为 1 的同时线程 2 指定到第三步骤同样将 a 赋值为 1; 如果要实现预期的效果可以使用加锁的方式进行同步; 或则通过原子类 AtomicInteger

介绍一下 ThreadLocal

alt

ThreadLoacl 提供了每个线程独享的空间; 一方面起到了安全隔离的作用, 另一方面可以实现上下文透传, 可以让某些参数方便的在同一线程任意位置被使用和修改; 一个 ThreadLocal 的底层实现是将一个 ThreadLocal 封装成为一个 key value 键值对 Entry 类型, 存储在一个 ThreadLocalMap, 这个 threadLocalMap 则是 Thread 对象中的一个变量; 三者关系如下: 每个 Thread 中都有一个 ThreadLocalMap 类型的变量, 这个变量的 key 是 ThreadLocal 类型的

ThreadLocal 的运用场景:

  • 保存用户基本信息
  • 保存数据库 Session 使得每个线程都有独立的数据库连接会话
  • 存储日志上下文

ThreadLocal 为什么会出现内存泄露? 如何解决?

要了解 ThreadLocal 为什么会出现内存泄露那么就要先理清楚 ThreadLocal,Thread,ThreadLocalMap 三者的关系; 简单来说就是每个在堆中的 Thread 对象都有这一个 ThreadLocalMap 类型的对象, 而这个 map 的 key 就是 ThreadLocal 类型的; 先说一下可能内存泄露的情况; 就是一个线程结束了应该将堆中的 Thread 对象清除也就会清除 ThreadLocalMap; 但是如果使用了线程池这种技术, 这种以通过复用线程降低线程创建和清除消耗的技术的话, 那么就会导致 Thread 和 ThreadLocalMap 保留下来并且会由下个线程使用; 这样一来不单数据安全得不到保障而且 map 的体积会越来越大从而造成内存泄露; 对于这个问题 java 底层已经帮我们做了部分优化了, 就是将 ThreadLocalMap 的 Entry 键值对中的 key 设置为了虚引用; 也就是说如果 ThreadLocal 的引用存在那么 key 就是可以正常访问的; 如果一个线程执行完成那么 ThreadLocal 引用就会消失, 那么 key 这个虚引用就会在下次 gc 时被回收; 这样虽然有优化但是依然没有测底释放 value 上的内存; 所以在使用 ThreadLocal 时我们必须手动在使用完成后线程结束前通过 remove 方法释放 value 的值;

聊聊什么是锁擦除, 什么是锁粗化?

如果 JVM 发现一个同步块中不存在被共享的变量操作也就意味着不会出现并发问题, 那么就会将锁进行擦除

如果 JVM 发现多个连续的同步代码块那么就会触发合并来减少频繁获取和释放的开销

创建线程池的七大参数?

核心线程数 corePoolSize 最大线程数 maximumPoolSize 线程空闲时间 keepAliveTime

  • 指的是非核心线程在空闲状态下的存活时间 时间单位 unit 任务队列 workQueue 线程工厂 threadFactory 拒绝策略 handler

为什么不建议通过 Executors 构建线程池

因为 Excutors 默认创建的线程池因为默认参数的问题极可能参数内存移除的问题; 比如他会将请求队列的长度设为 Integer 类型的最大值;

#java##面试#
全部评论
感觉佬总结的真的不错
点赞 回复 分享
发布于 昨天 23:47 广东

相关推荐

评论
点赞
2
分享

创作者周榜

更多
牛客网
牛客企业服务