在看完《Java多线程编程核心技术》
与《Java并发编程的艺术》
之后,对于多线程的理解到了新的境界. 先拿如下的题目试试手把.
Q1: 现在有线程 T1、T2 和 T3。你如何确保 T2 线程在 T1 之后执行,并且 T3 线程在 T2 之后执行?
答案: 使用Thread.join()方法即可.当然JUC包内提供了CountDownLatch
与CyclicBarrier
工具类供我们选择.
如果我是面试官, 我会进行深入询问.
Q: 什么是CountDownLatch
?什么是CyclicBarrier
?两者的区别?
A: 可以重置, 接口类型不同.
Q: 再深入一点,实现的机制?
A: 使用锁的Condition进行完成
Q:Condition的实现机制 ->
A: AQS ->CAS .刨根问底总是会将问题复杂化.
Q2: Java 中新的 Lock 接口相对于同步代码块(synchronized block)有什么优势?如果让你实现一个高性能缓存,支持并发读取和单一写入,你如何保证数据完整性。
A2-1:
Lock
相比与synchronized
在使用时更加的灵活.Lock
的底层实现使用的是AQS -> CAS
.会更加高效.Lock
实现了共享锁与独占锁两种机制.- 我们可以通过
AQS
自定义实现Lock
.而synchronized
关键字则较为难以更改.- 使用
Lock
,可以创建不同的Condition
.以用于不同的唤醒工作.这是synchronized
的wait/notify
难以实现的.- 深入点: 还是
Lock
的实现AQS
.A2-2:
保证数据完整性与高性能缓存是两个问题.
保证数据的完整性. 可以使用读锁和写锁来进行完成,比较常见的就是
ReentReadWriteLock
.读锁共享,写锁互斥.读读共享,写写/写读互斥.高性能缓存. 可以使用局部锁.类似
ConcurrentHashMap -> Segment -> HashEntry
的类型结构.反例HashTable
与SynchronizedMap
完整性的深入在于AQS如何实现共享锁与互斥锁的.以及
ReentReadWriteLock
的基本实现. 我的话会将其与数据库内的读写操作进行询问.(行级锁 -> 表级锁 -> Mysql内优化 )
高性能的深入只要掌握ConcurrentHashMap
数据结构即可.
Q3: Java 中 wait 和 sleep 方法有什么区别?
A: wait 与 sleep都是线程等待. 值得一提的是, wait与sleep都会使当前线程处于阻塞状态.不同点在于:
- wait()后需要其他线程进行唤醒, sleep()后只需要等待一段时间即可;
- wait()后会释放当前持有的锁, sleep()后不会进行释放.
Q4: 如何在 Java 中实现一个阻塞队列?
A: 实现阻塞队列之前先要理解什么是阻塞队列?
- 队列: 满足先进先出
FIFO
的特性即可.- 阻塞: 满足队列空时阻塞读线程, 队列满时阻塞写线程.
根据上述提示不难写出如下的代码(使用ReentrantLock独占锁):class Test{ ArrayList list; volatile int count; Lock lock; Condition fullCondition; Condition emptyCondition; public Test(){ list = new CopyOnWriteArrayList(); count = 0; lock = new ReentrantLock(); fullCondition = lock.newCondition; emptyCondition = lock.newCondition; } // 弹出队列 public void offer(){ try{ lock.lock(); while(count == 0){ emptyCondition.await(); } list.get(i); count--; }finally{ lock.unlock(); } // 压入队列 public void offer(int var){ try{ lock.lock(); while(count == list.size()){ fullCondition.await(); } list.add(var); count++; }finally{ lock.unlock(); } } }
Q5: 如何在 Java 中编写代码解决生产者消费者问题?
A: 生产者与消费者问题.非常类似上方的阻塞队列.这里提供一个使用LinkedBlockingQueue
实现的生产者与消费者.import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class ProducerConsumerSolution { public static void main(String[] args) { BlockingQueue
sharedQ = new LinkedBlockingQueue (); Producer p = new Producer(sharedQ); Consumer c = new Consumer(sharedQ); Consumer c2 = new Consumer(sharedQ); p.start(); c.start(); c2.start(); } } class Producer extends Thread { private BlockingQueue sharedQueue; public Producer(BlockingQueue aQueue) { super("PRODUCER"); this.sharedQueue = aQueue; } public void run() { // no synchronization needed for (int i = 0; i < 10; i++) { try { System.out.println(getName() + " produced " + i); sharedQueue.put(i); Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Consumer extends Thread { private BlockingQueue sharedQueue; public Consumer(BlockingQueue aQueue) { super("CONSUMER"); this.sharedQueue = aQueue; } public void run() { try { while (true) { Integer item = sharedQueue.take(); System.out.println(Thread.currentThread().getName() + " consumed " + item); } } catch (InterruptedException e) { e.printStackTrace(); } } } 因为上述的代码内直接使用了
LinkedBlockingQueue
是线程安全的.所以不需要更多的进行处理.
Q5-2: 深入,LinkedBlockingQueue
的实现原理.见上.LinkedBlockingQueue
的读数据和取数据的操作都是需要加锁的.
Q5-3: 是否有使用过其他的线程安全集合类?ConcurrentHashMap
的读操作和写操作都需要加锁么?ConcurrentLinkedQueue
呢?
A5-3:ConcurrentHashMap
的读操作不加锁.使用的是volatile
变量.ConcurrentLinkedQueue
读操作和写操作都不加锁.使用CAS
进行操作.
Q6: 写一段死锁代码。你在 Java 中如何解决死锁?
A6-1: 死锁发生是因为相互资源等待,而不释放自身的锁资源.举个例子class ThreadA extends Thread{ Lock lockA; Lock lockB; public void run(){ lockA.lock(); Thread.sleep(1000); lockB.lock(); lockA.unlock(); lockB.unlock(); } } class ThreadB extends Thread{ Lock lockA; Lock lockB; public void run(){ lockB.lock(); Thread.sleep(1000); lockA.lock(); lockB.unlock(); lockA.unlock(); } }
可以看到上述的线程,
- 线程A获取LockA后等待1s后又要获取LockB;
- 线程B获取LockB后等待1s后又要获取LockA;
这样就会造成死锁等待现象.
死锁-操作系统的经典问题:
形成条件(1.互斥条件 2. 不可剥夺条件 3.请求与保持条件 4. 循环等待条件)
应对死锁, 通常有4种处理方法(1. 预防死锁 2. 避免死锁 3. 检测死锁 4. 解除死锁)- 预防死锁: 主要是破坏死锁的4个形成条件. 主要是破坏2/3/4点.
- 对于2, 当线程无法获取到使用的资源时,即释放资源.
- 对于3, 策略1获取所有资源后才开始运行 / 策略2 获取一定的资源开始运行.
- 对于4, 线性运行资源.(个人感觉这样效率比较差).
- 对于
Java
, 我们一般使用tryLock(long time)
.主要处理请求和保持条件
.- 死锁避免 - 使用银行家算法进行调度
- 检测死锁 - 检测是否有环路
- 解除死锁 - 关闭所有线程 / 关闭部分线程 - 逐个终止代价最小的线程
死锁的原理以及避免算法
避免死锁的几种常见方法Q6-2: 深入会问
哲学家就餐问题
和银行家算法
?
A6-2:
哲学家就餐问题
:5个哲学家6只筷子.
解决措施:AND
策略,当获取左右2只筷子才进食.一次性获取所有的锁./记录策略
4个哲学家拿筷子,这样至少一个人可以进食.记录策略
奇偶排序, 5个人都先争取奇数筷子, 再争取偶数筷子.
Q7-1: 什么是原子操作?Java 中有哪些原子操作?
A1: 原子操作是指在Java
执行过程中, 要么全部成功, 要么全不成功.Java
内一共提供了13种原子操作.原子操作的原理是CAS
.
Q7-2: 你需要同步原子操作吗?
A2: 不需要同步原子操作. 原子操作是通过CAS
进行控制的.CAS
根据操作系统底层的不同而不同.例如Linux
系统的底层脚本与Windows
系统的底层脚本就不一样.
Q8: Java 中 volatile 关键字是什么?你如何使用它?它和 Java 中的同步方法有什么区别?
A8:volatile
关键字是将线程内的局部变量与进程内的公共变量同步.(JMM模型)
可见性 / 一致性
-线程局部变量与进程变量共享 /有序性
-happen-before
原则, 使被volatile
关键字修饰的变量不会进行重排序.
Java开发中的volatile你必须要了解一下
Q9: 什么是竞态条件?你如何发现并解决竞态条件?
A9: 竞态条件非常简单, 两个线程同时竞争同一个资源变量.
举个最简单的例子:class CompareThread extends Thread{ public int count; public CompareThread(int count){this.count = count;} public void run(){count++;} }
当启动两个线程的时候,
count++
不一定是需要的值.
- 线程1 count=0; count+1;暂停;
- 线程2 count=0; count+1;暂停;
- 线程1 count=1;赋值
- 线程2 count=1;赋值
预计输出为2, 但实际输出因为竞态为1.
解决措施: 加锁Lock / synchronized关键字 / CAS使用原子操作类
什么是竞态条件? 举个例子说明。
Q10: 在 Java 中你如何转储线程(thread dump)?如何分析它?
通过jstack -l
即可. 分析: 直接阅读.或者使用相应的分析工具.
Q11: 既然 start() 方法会调用 run() 方法,为什么我们调用 start() 方法,而不直接调用 run() 方法?
A11:start()
方法在另启动一个子线程进行执行.run()
方法不会启动子线程,而是在当前线程后顺序执行.
Q12: Java 中你如何唤醒阻塞线程?
A12:
- 如果是通过
sleep()
方法的阻塞,等待其时间到了即唤醒.- 如果是
join()
方法的阻塞, 当其join()
的线程运行完毕后即会唤醒.- 如果是
wait()
方法的阻塞, 当其notify()
的时候即会唤醒.- 如果是因为
IO
资源等问题的阻塞, 当资源获取后即会唤醒.- 注意: 我们有时可以使用中断, 抛出中断异常的方式让其强行唤醒.
Q13: Java 中 CyclicBarriar 和 CountdownLatch 有什么区别?
CountdownLatch
的屏障点不可以重置,CyclicBarriar
可以重置.CountdownLatch
当await()
结束后;CyclicBarrier
可以在构造函数时,指定屏障打开后的运行线程Runnable
.
Q14: 什么是不可变类?它对于编写并发应用有何帮助?
A: 不可变类应当是final
修饰的类.无法被继承.
Q14-1: 深入:String
类型是不可变类. JVM的常量池.
Q15: 你在多线程环境中遇到的最多的问题是什么?你如何解决的?
A15: 就个人而言, 多线程遇到最多的是资源的调优与使用. 包括数据库线程池.Spark
内的每个Executor
获取的资源数目.
内存干扰、竞态条件、死锁、活锁、线程饥饿是多线程和并发编程中比较有代表性的问题。这类问题无休无止,而且难于定位和调试。
这是基于经验给出的 Java 面试题。你可以看看Java 并发实战课程来了解现实生活中高性能多线程应用所面临的问题。
Q16: 线程和进程的区别?
A16: 两者都是单位. 线程是操作系统的任务单位. 而线程是进程的子单位. 我们操作系统的应用通常就是一个进程.在应用内,还有许多的子线程.
Q17: 多线程的上下文切换是什么?
A17: 多个线程因时间片使用完而造成的运行程序上下问直接的切换.举个例子: 线程A -> 线程B -> 线程A
Q18: 死锁和活锁的区别?死锁和饥饿的区别?
A18: 活锁即我们常用的锁. 死锁是获取不到锁而是当前线程造成的死循环.死锁会造成资源的大量消耗及线程阻塞.
Q19: Java 中使用什么线程调度算法?
A19: FIFO / 时间片轮转
linux进程/线程调度策略(SCHED_OTHER,SCHED_FIFO,SCHED_RR)
Q20: 线程中如何处理某个未处理异常?
A20:try-catch
. 设置默认异常处理器UncaughtExceptionHandler
.Future
的Get
方法. 若无处理, 子线程会直接退出程序.
Java子线程中的异常处理(通用)
Q21: 什么是线程组?为什么 Java 中不建议使用线程组?
A21:ThreadGroup
.
- 其中的
stop()/resume()
等方法已被废弃.- 会出现线程安全问题.(为啥?)
java-为什么不推荐使用线程组
Q22: 为什么使用 Executor 框架比直接创建线程要好?
A22:
- 统一接口,管理方便.线程池的切换方便.
- 性能高.
Q22-2: 深入问题, 能讲下Executor
内的基本类与基本组成么?
Q23: Java 中 Executor 和 Executors 的区别?
A23:Executor
接口,主要接口方法为execute()
;常用的是ExecutorService
, 主要接口为submit()/shutdown()/isShutDown()
.
Executors
静态类, 主要是用于创建线程池Executors.newFixedThreadPool(4)
.
Q24: 在 windows 和 linux 系统上分别如何找到占用 CPU 最多的线程?
A24: Linux.使用top
命令即可. Windows. 使用任务管理器.
进程和线程是两个单位.进程通常是我们说的运行程序,是相对于操作系统而言的,通常可以使用ps -ef / jps
进行查询得出.而线程,通常称为子线程,也就是一个进程能够分为一个或多个子线程.线程通常是为提升进程的效率而设定的.
在一个进程中,我们同时开启多个线程,让多个线程去完成某些任务.(比如后台服务,就可以用多个线程响应多个客户请求.)
时间片轮转.
实现Runnable
接口和继承Thread
类.
thread.start()
和thread.run()
方法有什么区别?start()
方法会启动子线程,及新线程运行run()
方法;
run()
方法,不会生成子线程(子线程)进行运行;
PS: 2019-06-12已经更正. 谢谢博友指证. 其实记也好记, run()
其实直接调用Thread类中重写的run()
方法, start()
是新启动一个子线程, 运行run()
方法.
synchronized
关键字?
synchronized
缺陷?x
程序阻塞.如何才会释放?效率低下.
Lock在一定时间内未获取,会自动进行释放;
Lock在使用wait/notify
的时候,可以使用不同的Condition
进行控制唤醒的进程;
Lock可以将读锁
和写锁
进行分离,提升系统的运行效率.
runnable
与callable
.线程的回调函数.
[1] Java面试:投行的15个多线程和并发面试题
[2] 40个Java多线程问题总结