并发编程初级面试题
一. 什么是进程和什么是线程
进程是操作系统资源分配和调度的基本单位, 也可以说一个程序就是一个进程, 可以看成是程序的实例
线程是操作系统资源分配和调度的最小单位, 他被包含在进程之中, 是进程中的实际运作单位
二. 线程的状态
- new(新建)
- runnable(运行)
- blocked(阻塞)
- waiting(等待)
- timed_waiting(超时等待)
- terminated(终结)
三. wait 和 sleep 的区别
- wait 是 Object 的方法, 调用 wait 会释放锁
- sleep 是 Thread 的方法, 调用 sleep 不会释放锁
四. 创建线程的方式
- 继承 Thread, 重写 run 方法
- 实现 Runnable, 扔到 thread 里运行
- 实现 Callable, 扔到 FutureTask 中, 再把 FutureTask 扔到 thread 里运行
- 使用线程池创建
五. 线程池的创建方式和 7 个参数
-
Executors.newCachedThreadPool()
全是救急线程的线程池
-
Executors.newFixedThreadPool()
可以设置线程数量的线程池
-
Executors.newScheduledThreadPool()
定时执行任务的线程池
-
Executors.newSingleThreadExecutor()
创建只有 1 个线程的线程池
-
手动 new ThreadPoolExecutor(...)
根据阿里巴巴开发手册, 不准使用以上方法创建线程池, 所以需要手动创建线程池, 手动创建线程池就需要了解创建线程池的 7 个参数
- 核心线程数量
- 最大线程数量
- 存活时间
- 时间单位
- 阻塞队列
- 线程工厂
- 拒绝策略(队列满了之后的行为)
- 丢弃任务, 并且抛异常
- 在调用者线程执行任务, 如果线程池已经 shutdown, 则丢弃任务
- 丢弃等待最久的任务, 把当前任务加入进去
- 丢弃任务, 不抛异常, 什么都不做
六. ForkJoinPool
ForkJoinPool 的核心思想是大任务拆成小任务, 最终合并结果
ForkJoinPool 使用工作窃取算法, 当某个线程的任务队列中没有可执行任务的时候, 从其他线程的任务队列中窃取任务来执行, 可以充分的压榨 CPU 资源
七. 常用的并发工具类
- ReentrantLock
- AtomicInteger
- LongAdder
- CopyOnWriteArrayList
- CopyOnWriteArraySet
- ConcurrentHashMap
- CountDownLatch
- CyclicBarrier
- Semaphore
- BlockingQueue
- 线程池
八. CountDownLatch 和 CyclicBarrier 的区别
CountDownLatch | CyclicBarrier |
---|---|
减数字 | 加数字 |
数字为 0 时释放所有等待的线程 | 数字加到规定的值时释放所有等待线程, 并且可以执行构造函数传入的任务(可选) |
数字无法重置 | 数字加到规定的值时, 重新从 0 开始加 |
调用 await()方法只进行阻塞 | 调用 await()方法数字加 1, 如果加完数字还是没有达到规定的值, 阻塞等待 |
不可重复利用 | 可重复利用 |
九. CAS 和 volatile
CAS 是乐观锁, 先比较, 再修改, 内存位置(JAVA 中的内存地址, V), 旧的预期值(A)和新值(B), 如果 V 和 A 一样就改成 B, JDK9 之前 CAS 是通过 Unsafe 对象操作, JDK9 之后推荐通过 VarHandle, 所以 JUC 包里面的 CAS 操作都变了
volatile 是 java 的关键字, 作用主要是保证内存可见性和禁止指令重排序
十. synchronized 和 ReentrantLock 的区别
- synchronized 是 java 关键字, ReentrantLock 是 Lock 接口的实现类
- synchronized 在发生异常时, 会释放锁, ReentrantLock 不会
- ReentrantLock 可以打断等待, synchronized 不行
- ReentrantLock 可以知道有没有获取锁, synchronized 不行
- ReentrantLock 是乐观锁, 而且可以控制是否公平, synchronized 严格来说是悲观锁(锁升级之后)
- ReentrantLock 有 Condition, 可以唤醒特定的组
十一. ReentrantReadWriteLock 读写锁
- 读读共享, 读写, 写写互斥
- 可能造成线程饥饿
- 写锁可以降级到读锁, 读锁不能升级成写锁
- JDK8 加入了新的读写锁, StampLock, 支持乐观读
十二. Java 中如何获取到线程 dump 文件
- 获取到线程的 pid,可以通过使用 jps 命令,在 Linux 环境下还可以使用 ps -ef | grep java
- 打印线程堆栈,可以通过使用 jstack pid 命令,在 Linux 环境下还可以使用 kill -3 pid
- 使用 jconsole 查看
十三. Java 中用到的线程调度算法是什么
Java 虚拟机采用抢占式调度模型
十四. LockSupport
LockSupport.park 不需要获取锁资源就能阻塞线程, 而且可以精确控制唤醒的线程, 并且可以在线程没有阻塞之前提前 unpark, 这时候线程运行到 park 方法的时候不会阻塞, JUC 工具包里的阻塞等待就是靠 park 方法实现
十五. ThreadLocal
每个线程内部有 1 个 ThreadLocalMap, 当调用 threadLocal 的 get, set, remove 方法时, 在方法内部拿到当前线程, 从线程中获取 ThreadLocalMap, 最后以 threadLocal(弱引用)为 key, 从中取出数据
使用 ThreadLocal 是线程安全的, 但是使用完需要及时删除, 否则会造成一定程度的内存泄漏或浪费(因为 ThreadLocalMap 的 key 使用的是弱引用包装的 threadLocal)
十六. synchronized 锁升级
-
无锁
无锁就是正常对象状态, 一般新创建的对象状态为无锁可偏向
-
偏向锁
在对象头的 mark word 上记录线程 ID, 代表当前线程获取了锁
- 如果发生锁竞争, 会升级成轻量级锁
- 如果在 synchronized 之外调用 hashcode 方法, 锁会升级成轻量级锁
- 如果在 synchronized 之内调用 hashcode 方法, 锁会升级成重量级锁
- 偏向锁同一线程 ID 修改次数达到 20 次后会发生批量重偏向(剩余锁对象的偏向全部修改成该线程 ID)
- 偏向锁同一线程 ID 修改次数达到 40 次后会关闭整个类的所有对象的偏向锁, 变成不可偏向状态
-
轻量级锁 + 自旋锁
对象头的 mark word 上会记录线程栈中的锁记录引用, 如果多次 CAS 操作失败, 会升级成重量级锁
CAS 次数 JDK6 之前是默认 10 次, JDK6 的时候是自适应次数
-
重量级锁(管程 Monitor)
在对象头的 mark word 上记录 Monitor 的引用, 没有获取锁的线程会加入 Monitor 的 EntryList 中, 调用 wait 的线程会加入 WaitSet 中, Owner 记录的是锁的拥有者
-
锁消除
synchronized 的对象是局部变量, 并且 JVM 发现没有逃出, 会优化成不加锁
-
锁粗化
当一段代码中有 2 个或多个 synchronized 代码块, 并且中间的代码非常少, 而且运行的很快, 会优化成一个 synchronized
十七. AQS
AbstractQueuedSynchronizer 简称 AQS, 是个抽象类, JUC 大部分工具类都依赖他实现, 内部拥有 1 个 volatile 修饰是 state 和 1 个等待队列一对头尾节点(链表实现队列), 通过 CAS 修改 state 来判断是否获取锁, 如果没有获取锁, 把当前线程加入等待队列并且 park 阻塞
AQS 支持两种同步方式, 公平和非公平
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于