Skip to content

Java 并发编程 待完善

并发基础

为什么使用并发编程

  1. 提高 CPU 多核利用率
  2. 进行业务拆分,提升性能。

并发编程存在什么问题

  1. 内存泄漏
  2. 上下文切换的消耗
  3. 线程安全问题
  4. 死锁问题

并发编程的三个必要因素

提示

原子性、可见性、有序性

  1. 原子性:一系列操作同成功或同失败
  2. 可见性:一个线程对共享变量的修改,另一个线程能立刻看到
  3. 有序性:程序按照代码顺序执行(处理器会对指令重排序)

并发出现问题的根源是什么?

  1. 线程切换带来原子性问题,通过 synchronized 代码块或锁来解决
  2. 缓存导致可见性问题,通过 synchronized 代码块、volatile 变量或锁来解决
  3. 编译优化导致有序性被破坏,通过 Happen-Before 规则解决

并发和并行的区别?

并发:多个任务在同一个核心交替运行

并行:多个核心同时处理多个线程

进程和线程的区别

进程是系统资源分配的基本单位,实现了操作系统的并发。

线程是进程的子任务,是 CPU 调度的基本单位,实现了进程内部的并发。

什么是上下文切换

任务在使用完时间片,切换到另一个任务之前,会先保存自己的状态。用于切换回这个任务时,可以再加载这个任务的状态。

任务从保存到再加载的过程就是一次上下文切换。

Java 守护线程和用户线程的区别

一旦用户线程全部结束,守护线程也会一同结束。

❓什么是线程死锁?

死锁的四个必要条件

  1. 互斥
  2. 不可剥夺
  3. 请求和保持
  4. 循环等待

如何避免线程死锁?

  1. 避免一个线程获得多个锁
  2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
  3. 尝试使用定时锁,使用 lock.tryLock(timeout) 来替代使用内部锁机制

Java 六种线程状态

根据 Java 中 Thread.State 枚举类划分

  1. new:线程被创建,未调用 start() 方法
  2. runnable:运行状态 + 可运行状态 + 操作系统的阻塞状态(BIO)
  3. terminated:终止状态
  4. blocked:等待 synchronized() 获取锁
  5. waiting:等待 join()
  6. timed_waiting:有时限的阻塞状态,等待 sleep()

线程状态如何流转

多线程有几种实现方式

  1. 继承 Thread 类,重写 run() 方法,创建 Thread 对象
  2. 实现 Runnable 接口,重写 run() 方法,创建 Thread 对象
  3. 实现 Callable 接口,重写 call() 方法,创建 FutureTask 对象

什么是 FutureTask ?

FutureTask 表示异步任务,可以用于获取异步运行的结果

在什么时候使用过 FutureTask ?

需要从多个外部服务获取数据(类似爬虫),然后将这些数据整合后返回给客户端。

可以使用 FutureTask 结合线程池,并行发送请求,提高效率。

Runnable 和 Callable 的区别

  1. 返回值
    1. Runnable 接口 run() 方法无返回值
    2. Callable 接口 call() 方法有返回值,可以用来获取异步执行的结果
  2. 异常
    1. Runnable 接口 run() 方法只能抛出异常,无法捕获
    2. Callable 接口 call() 方法允许抛出异常,可以获取异常信息

wait() 和 sleep() 有什么区别?

两者都可以暂停线程的运行

  1. 所属类
    1. wait() 是 Object 类方法
    2. sleep() 是 Thread 类方法
  2. 是否释放锁
    1. wait() 释放锁
    2. sleep() 不释放锁
  3. 用途
    1. wait() 用于线程间交互/通信
    2. sleep() 用于暂停执行
  4. 苏醒时间
    1. wait() 调用后,线程不会自动苏醒,需要别的线程调用同一个对象的 notify()notifyAll()
    2. sleep() 执行完成后,线程会自动苏醒。

为什么线程通信的方法 wait-notify 被定义在 Object 类里?

Java 允许将任何对象当成锁。

为什么 wait-notify 必须在同步方法或者同步块中被调用?

调用 obj.wait()、obj.notify() 方法时,都必须先持有 obj 锁,所以这些方法需要在同步方法或同步块中被调用。

❓线程 sleep() 方法和 yield() 方法的区别?

如何停止一个正在运行的线程?

  1. 使用 stop() 方法强制停止
  2. 使用 interrupt() 方法配合线程停止

interrupted() 和 islnterrupted() 方法的区别?

  • interrupt():打断线程,修改线程中断标志位,需要线程自行处理。
  • interrupted():查看线程打断标志位(是否被打断),并恢复打断标志
  • islnterrupted():查看线程打断标志位(是否被打断),不修改打断标志

什么是阻塞式方法?

阻塞式方法:在执行过程中会暂停,直到某个条件满足或事件完成。

常见的阻塞式方法:

  1. 线程同步:synchronized 代码块
  2. 线程等待:Thread.sleep()
  3. 网络通信:ServerSocket.accept() 等待客户端连接
  4. IO 操作:等待字节流读入

Java 如何唤醒一个阻塞的线程

在 synchronized 代码块中调用 obj.notify()obj.notifyAll() 方法。

notify() 和 notifyAll() 的区别

notify() 会唤醒一个线程,notifyAll() 会唤醒所有线程。

如何实现多线程的通讯和协作?

  1. 使用 synchronized 加锁对象的 wait()、notify() 方法
  2. 使用 ReentrantLock 中 Condition 类的 await()、signal() 方法

同步方法和同步块哪个更好?

同步块更好,同步方法相当于是整个方法用了同步块,并锁住了当前对象。

同步的范围越小越好。

❓什么是线程同步和线程互斥,有哪几种实现方式?

❓在监视器 Monitor 内部,是如何做线程同步的?

线程池提交任务时,核心线程数已达到配置的数量,这时会发生什么?

  1. 对于无界队列:将任务添加到阻塞队列,等待执行。
  2. 对于有界队列:
    1. 将任务添加到阻塞队列,等待执行
    2. 如果阻塞队列满了,增加新线程
    3. 如果线程数已经达到最大线程数,触发拒绝策略

Java 程序如何保证多线程安全?

  1. 使用 JUC 下的类,如 ConcurrentHashMap、AtomicInteger 等
  2. 使用 synchronized 同步代码块
  3. 使用 ReentrantLock 锁

对线程优先级的理解是什么?

一般来说,高优先级的线程会在运行时有优先权。但是操作系统会重新权衡,所以不能在程序中依赖线程优先级。

线程类的构造方法、静态块是被哪个线程调用的?

  1. 线程类的构造方法、静态块是被 new 这个线程类所在的线程所调用的
  2. run() 方法里面的代码才是被线程自身所调用的。

Java 中怎么获取一份线程 dump 文件?

dump 文件是进程内存镜像。

  1. Linux 下使用 jstack -l [pid]
  2. Windows 下按下 Ctrl + Break

线程运行时发生异常会怎样?

如果异常没有被捕获,线程会停止运行。

线程数过多会造成什么问题?

  1. 程序开销较大,消耗过多 CPU 资源
  2. 影响 JVM 稳定性
  3. 如果可运行线程比 CPU 核心数量多,会有线程被闲置,浪费内存资源

❓介绍一下 ThreadLocal

❓ThreadLocal 内存泄露问题了解吗

❓为什么用 ThreadLocal 不用线程成员变量?

并发理论

❓线程之间如何通信及线程之间如何同步

❓什么是 JMM 内存模型

Java 内存模型定义了线程和主内存之间的关系,线程对共享变量的修改对其他线程可见。

共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。

❓什么是 Happens-Before 原则

❓Java怎么进行并发控制?

synchronized 关键字有什么用

用于解决多个线程对共享资源竞争的问题,保证代码块或方法只有一个线程在执行。

  1. 每个对象都存在一个与之关联的 Monitor。
  2. 当一个线程进入 synchronized 方法或代码块时,它会尝试获取该对象的监视器锁。如果成功,则进入同步块执行代码;否则,线程将被阻塞,直到锁被释放。
  3. 线程在退出同步方法或代码块时会自动释放 Monitor

synchronized 都可以在哪里使用

  1. 代码块:对指定对象加锁
  2. 成员方法:对当前对象 this 加锁
  3. 静态方法:对当前类对象加锁

说一下 synchronized 底层实现原理?

  1. 每个对象都存在一个与之关联的 Monitor。
  2. 当一个线程进入 synchronized 方法或代码块时,它会尝试获取该对象的监视器锁。如果成功,则进入同步块执行代码,将重入数设置为 1,该线程为该 Monitor 的所有者;否则,线程将被阻塞,直到锁被释放。
  3. 如果线程已经占有该 Monitor,只是重新进入,则进入monitor的重入数+1。
  4. 线程在退出同步方法或代码块时会将重入数 -1 并释放 Monitor。

synchronized 可重入的原理

维护一个重入数,当线程获取该锁时重入数 +1,再次获得该锁时继续 +1,释放锁 时,重入数 -1,当重入数值为 0 时,释放锁。

什么是自旋

多个线程竞争时,加锁是重量级操作,而且加锁的代码执行普遍较快。等待锁的线程可以进行忙循环,不阻塞自己等待锁释放,如果多次等待仍未释放,再加锁。

忙循环:不进入阻塞态,不放弃 CPU 时间片和缓存。运行空循环等待释放锁,避免加锁导致内核态切换和阻塞后重建缓存

多线程中 synchronized 锁升级的原理是什么?

偏向锁 -> 轻量级锁 -> 重量级锁

  1. 偏向锁:单个线程多次使用。

    1. 无线程使用:锁对象的对象头保存 threadid
    2. 有线程正在使用:暂停原线程,升级为轻量级锁,并自旋等待。
  2. 轻量级锁:有其他线程使用。让锁记录指向锁对象,并尝试使用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

  3. 重量级锁:多个线程竞争。使用并竞争 Monitor。

线程 B 怎么知道线程 A 修改了变量

  1. 使用 volatile 修饰变量
  2. synchronized 修饰修改变量的方法(对一个变量解锁之前,必须先把此变量同步回主存中)
  3. 使用 wait/notify

❓synchronized、volatile、CAS 的比较

synchronized 和 Lock 有什么区别?

  1. synchronized 是关键字,Lock 是 Java 对象
  2. synchronized 可以给方法、代码块加锁,Lock 只能给代码块加锁
  3. synchronized 使用简单,不需要自行是否锁,Lock 需要手动 unlock
  4. Lock 可以知道是否成功获取锁,可以实现锁超时机制,synchronized 不能实现

synchronized 和 ReentrantLock 区别是什么?

  1. synchronized 可以给方法、代码块加锁,ReentrantLock 只能给代码块加锁
  2. synchronized 使用简单,不需要自行是否锁,ReentrantLock 需要手动 unlock
  3. ReentrantLock 可以指定公平锁,synchronized 是非公平锁
  4. synchronized 是通过 JVM 的 Monitor 实现,ReentrantLock 基于 AQS 实现
  5. ReentrantLock 支持多个条件变量,synchronized 只支持一个条件变量

volatile 关键字的作用

  1. 保证可见性:volatile 变量会立即更新到主存
  2. 禁止代码重排序

volatile 关键字的原理

  1. 对 volatile 变量的写指令后会加入写屏障
  2. 对 volatile 变量的读指令前会加入读屏障

Java中能创建 volatile 数组吗?

能,但是 volatile 只对数组引用有效。

❓volatile 变量和 atomic 变量有什么不同?

❓volatile 能使得一个非原子操作变成原子操作吗?

Released under the MIT License.