主题
Java 并发编程 待完善
并发基础
为什么使用并发编程
- 提高 CPU 多核利用率
- 进行业务拆分,提升性能。
并发编程存在什么问题
- 内存泄漏
- 上下文切换的消耗
- 线程安全问题
- 死锁问题
并发编程的三个必要因素
提示
原子性、可见性、有序性
- 原子性:一系列操作同成功或同失败
- 可见性:一个线程对共享变量的修改,另一个线程能立刻看到
- 有序性:程序按照代码顺序执行(处理器会对指令重排序)
并发出现问题的根源是什么?
- 线程切换带来原子性问题,通过 synchronized 代码块或锁来解决
- 缓存导致可见性问题,通过 synchronized 代码块、volatile 变量或锁来解决
- 编译优化导致有序性被破坏,通过 Happen-Before 规则解决
并发和并行的区别?
并发:多个任务在同一个核心交替运行
并行:多个核心同时处理多个线程
进程和线程的区别
进程是系统资源分配的基本单位,实现了操作系统的并发。
线程是进程的子任务,是 CPU 调度的基本单位,实现了进程内部的并发。
什么是上下文切换
任务在使用完时间片,切换到另一个任务之前,会先保存自己的状态。用于切换回这个任务时,可以再加载这个任务的状态。
任务从保存到再加载的过程就是一次上下文切换。
Java 守护线程和用户线程的区别
一旦用户线程全部结束,守护线程也会一同结束。
❓什么是线程死锁?
死锁的四个必要条件
- 互斥
- 不可剥夺
- 请求和保持
- 循环等待
如何避免线程死锁?
- 避免一个线程获得多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用
lock.tryLock(timeout)
来替代使用内部锁机制
Java 六种线程状态
根据 Java 中 Thread.State 枚举类划分
- new:线程被创建,未调用
start()
方法 - runnable:运行状态 + 可运行状态 + 操作系统的阻塞状态(BIO)
- terminated:终止状态
- blocked:等待
synchronized()
获取锁 - waiting:等待
join()
- timed_waiting:有时限的阻塞状态,等待
sleep()
线程状态如何流转
多线程有几种实现方式
- 继承 Thread 类,重写
run()
方法,创建 Thread 对象 - 实现 Runnable 接口,重写
run()
方法,创建 Thread 对象 - 实现 Callable 接口,重写
call()
方法,创建 FutureTask 对象
什么是 FutureTask ?
FutureTask 表示异步任务,可以用于获取异步运行的结果
在什么时候使用过 FutureTask ?
需要从多个外部服务获取数据(类似爬虫),然后将这些数据整合后返回给客户端。
可以使用 FutureTask 结合线程池,并行发送请求,提高效率。
Runnable 和 Callable 的区别
- 返回值
- Runnable 接口
run()
方法无返回值 - Callable 接口
call()
方法有返回值,可以用来获取异步执行的结果
- Runnable 接口
- 异常
- Runnable 接口
run()
方法只能抛出异常,无法捕获 - Callable 接口
call()
方法允许抛出异常,可以获取异常信息
- Runnable 接口
wait() 和 sleep() 有什么区别?
两者都可以暂停线程的运行
- 所属类
wait()
是 Object 类方法sleep()
是 Thread 类方法
- 是否释放锁
wait()
释放锁sleep()
不释放锁
- 用途
wait()
用于线程间交互/通信sleep()
用于暂停执行
- 苏醒时间
wait()
调用后,线程不会自动苏醒,需要别的线程调用同一个对象的notify()
或notifyAll()
sleep()
执行完成后,线程会自动苏醒。
为什么线程通信的方法 wait-notify 被定义在 Object 类里?
Java 允许将任何对象当成锁。
为什么 wait-notify 必须在同步方法或者同步块中被调用?
调用 obj.wait()、obj.notify() 方法时,都必须先持有 obj 锁,所以这些方法需要在同步方法或同步块中被调用。
❓线程 sleep() 方法和 yield() 方法的区别?
如何停止一个正在运行的线程?
- 使用
stop()
方法强制停止 - 使用
interrupt()
方法配合线程停止
interrupted() 和 islnterrupted() 方法的区别?
- interrupt():打断线程,修改线程中断标志位,需要线程自行处理。
- interrupted():查看线程打断标志位(是否被打断),并恢复打断标志
- islnterrupted():查看线程打断标志位(是否被打断),不修改打断标志
什么是阻塞式方法?
阻塞式方法:在执行过程中会暂停,直到某个条件满足或事件完成。
常见的阻塞式方法:
- 线程同步:
synchronized
代码块 - 线程等待:
Thread.sleep()
- 网络通信:
ServerSocket.accept()
等待客户端连接 - IO 操作:等待字节流读入
Java 如何唤醒一个阻塞的线程
在 synchronized 代码块中调用 obj.notify()
或 obj.notifyAll()
方法。
notify() 和 notifyAll() 的区别
notify()
会唤醒一个线程,notifyAll()
会唤醒所有线程。
如何实现多线程的通讯和协作?
- 使用 synchronized 加锁对象的 wait()、notify() 方法
- 使用 ReentrantLock 中 Condition 类的 await()、signal() 方法
同步方法和同步块哪个更好?
同步块更好,同步方法相当于是整个方法用了同步块,并锁住了当前对象。
同步的范围越小越好。
❓什么是线程同步和线程互斥,有哪几种实现方式?
❓在监视器 Monitor 内部,是如何做线程同步的?
线程池提交任务时,核心线程数已达到配置的数量,这时会发生什么?
- 对于无界队列:将任务添加到阻塞队列,等待执行。
- 对于有界队列:
- 将任务添加到阻塞队列,等待执行
- 如果阻塞队列满了,增加新线程
- 如果线程数已经达到最大线程数,触发拒绝策略
Java 程序如何保证多线程安全?
- 使用 JUC 下的类,如 ConcurrentHashMap、AtomicInteger 等
- 使用 synchronized 同步代码块
- 使用 ReentrantLock 锁
对线程优先级的理解是什么?
一般来说,高优先级的线程会在运行时有优先权。但是操作系统会重新权衡,所以不能在程序中依赖线程优先级。
线程类的构造方法、静态块是被哪个线程调用的?
- 线程类的构造方法、静态块是被 new 这个线程类所在的线程所调用的
run()
方法里面的代码才是被线程自身所调用的。
Java 中怎么获取一份线程 dump 文件?
dump 文件是进程内存镜像。
- Linux 下使用
jstack -l [pid]
- Windows 下按下 Ctrl + Break
线程运行时发生异常会怎样?
如果异常没有被捕获,线程会停止运行。
线程数过多会造成什么问题?
- 程序开销较大,消耗过多 CPU 资源
- 影响 JVM 稳定性
- 如果可运行线程比 CPU 核心数量多,会有线程被闲置,浪费内存资源
❓介绍一下 ThreadLocal
❓ThreadLocal 内存泄露问题了解吗
❓为什么用 ThreadLocal 不用线程成员变量?
并发理论
❓线程之间如何通信及线程之间如何同步
❓什么是 JMM 内存模型
Java 内存模型定义了线程和主内存之间的关系,线程对共享变量的修改对其他线程可见。
共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。
❓什么是 Happens-Before 原则
❓Java怎么进行并发控制?
synchronized 关键字有什么用
用于解决多个线程对共享资源竞争的问题,保证代码块或方法只有一个线程在执行。
- 每个对象都存在一个与之关联的 Monitor。
- 当一个线程进入
synchronized
方法或代码块时,它会尝试获取该对象的监视器锁。如果成功,则进入同步块执行代码;否则,线程将被阻塞,直到锁被释放。 - 线程在退出同步方法或代码块时会自动释放 Monitor
synchronized 都可以在哪里使用
- 代码块:对指定对象加锁
- 成员方法:对当前对象
this
加锁 - 静态方法:对当前类对象加锁
说一下 synchronized 底层实现原理?
- 每个对象都存在一个与之关联的 Monitor。
- 当一个线程进入
synchronized
方法或代码块时,它会尝试获取该对象的监视器锁。如果成功,则进入同步块执行代码,将重入数设置为 1,该线程为该 Monitor 的所有者;否则,线程将被阻塞,直到锁被释放。 - 如果线程已经占有该 Monitor,只是重新进入,则进入monitor的重入数+1。
- 线程在退出同步方法或代码块时会将重入数 -1 并释放 Monitor。
synchronized 可重入的原理
维护一个重入数,当线程获取该锁时重入数 +1,再次获得该锁时继续 +1,释放锁 时,重入数 -1,当重入数值为 0 时,释放锁。
什么是自旋
多个线程竞争时,加锁是重量级操作,而且加锁的代码执行普遍较快。等待锁的线程可以进行忙循环,不阻塞自己等待锁释放,如果多次等待仍未释放,再加锁。
忙循环:不进入阻塞态,不放弃 CPU 时间片和缓存。运行空循环等待释放锁,避免加锁导致内核态切换和阻塞后重建缓存
多线程中 synchronized 锁升级的原理是什么?
偏向锁 -> 轻量级锁 -> 重量级锁
偏向锁:单个线程多次使用。
- 无线程使用:锁对象的对象头保存 threadid
- 有线程正在使用:暂停原线程,升级为轻量级锁,并自旋等待。
轻量级锁:有其他线程使用。让锁记录指向锁对象,并尝试使用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
重量级锁:多个线程竞争。使用并竞争 Monitor。
线程 B 怎么知道线程 A 修改了变量
- 使用 volatile 修饰变量
- synchronized 修饰修改变量的方法(对一个变量解锁之前,必须先把此变量同步回主存中)
- 使用 wait/notify
❓synchronized、volatile、CAS 的比较
synchronized 和 Lock 有什么区别?
- synchronized 是关键字,Lock 是 Java 对象
- synchronized 可以给方法、代码块加锁,Lock 只能给代码块加锁
- synchronized 使用简单,不需要自行是否锁,Lock 需要手动 unlock
- Lock 可以知道是否成功获取锁,可以实现锁超时机制,synchronized 不能实现
synchronized 和 ReentrantLock 区别是什么?
- synchronized 可以给方法、代码块加锁,ReentrantLock 只能给代码块加锁
- synchronized 使用简单,不需要自行是否锁,ReentrantLock 需要手动 unlock
- ReentrantLock 可以指定公平锁,synchronized 是非公平锁
- synchronized 是通过 JVM 的 Monitor 实现,ReentrantLock 基于 AQS 实现
- ReentrantLock 支持多个条件变量,synchronized 只支持一个条件变量
volatile 关键字的作用
- 保证可见性:volatile 变量会立即更新到主存
- 禁止代码重排序
volatile 关键字的原理
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
Java中能创建 volatile 数组吗?
能,但是 volatile 只对数组引用有效。