Java 多线程面试题

版权声明:欢迎转载,转载请说明出处. 大数据Github项目地址https://github.com/SeanYanxml/bigdata。 https://blog.csdn.net/u010416101/article/details/88841200

前言

在看完《Java多线程编程核心技术》《Java并发编程的艺术》之后,对于多线程的理解到了新的境界. 先拿如下的题目试试手把.


投行面试

Q1: 现在有线程 T1、T2 和 T3。你如何确保 T2 线程在 T1 之后执行,并且 T3 线程在 T2 之后执行?
答案: 使用Thread.join()方法即可.当然JUC包内提供了CountDownLatchCyclicBarrier工具类供我们选择.
如果我是面试官, 我会进行深入询问.
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.以用于不同的唤醒工作.这是synchronizedwait/notify难以实现的.
  • 深入点: 还是Lock的实现AQS.

A2-2:
保证数据完整性与高性能缓存是两个问题.

  • 保证数据的完整性. 可以使用读锁和写锁来进行完成,比较常见的就是ReentReadWriteLock.读锁共享,写锁互斥.读读共享,写写/写读互斥.

  • 高性能缓存. 可以使用局部锁.类似ConcurrentHashMap -> Segment -> HashEntry的类型结构.反例HashTableSynchronizedMap

  • 完整性的深入在于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<Integer> sharedQ = new LinkedBlockingQueue<Integer>();
		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<Integer> sharedQueue;
	public Producer(BlockingQueue<Integer> 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<Integer> sharedQueue;
	public Consumer(BlockingQueue<Integer> 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 <pid>即可. 分析: 直接阅读.或者使用相应的分析工具.

Q11: 既然 start() 方法会调用 run() 方法,为什么我们调用 start() 方法,而不直接调用 run() 方法?
A11: start()方法在另启动一个子线程进行执行.run()方法不会启动子线程,而是在当前线程后顺序执行.

Q12: Java 中你如何唤醒阻塞线程?
A12:

  • 如果是通过sleep()方法的阻塞,等待其时间到了即唤醒.
  • 如果是join()方法的阻塞, 当其join()的线程运行完毕后即会唤醒.
  • 如果是wait()方法的阻塞, 当其notify()的时候即会唤醒.
  • 如果是因为IO资源等问题的阻塞, 当资源获取后即会唤醒.
  • 注意: 我们有时可以使用中断, 抛出中断异常的方式让其强行唤醒.

Q13: Java 中 CyclicBarriar 和 CountdownLatch 有什么区别?

  • CountdownLatch的屏障点不可以重置, CyclicBarriar可以重置.
  • CountdownLatchawait()结束后;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. FutureGet方法. 若无处理, 子线程会直接退出程序.
Java子线程中的异常处理(通用)

Q21: 什么是线程组?为什么 Java 中不建议使用线程组?
A21: ThreadGroup.

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. 使用任务管理器.


面试题2

  • 什么是进程?什么是线程?

进程和线程是两个单位.进程通常是我们说的运行程序,是相对于操作系统而言的,通常可以使用ps -ef / jps进行查询得出.而线程,通常称为子线程,也就是一个进程能够分为一个或多个子线程.线程通常是为提升进程的效率而设定的.

  • 什么是多线程?

在一个进程中,我们同时开启多个线程,让多个线程去完成某些任务.(比如后台服务,就可以用多个线程响应多个客户请求.)

  • 多线程原理?

时间片轮转.

  • 线程如何启动(Java)?

实现Runnable接口和继承Thread类.

  • 线程的thread.start()thread.run()方法有什么区别?

start()方法会启动子线程,及新线程运行run()方法;
run()方法,不会生成子线程(子线程)进行运行;

PS: 2019-06-12已经更正. 谢谢博友指证. 其实记也好记, run()其实直接调用Thread类中重写的run()方法, start()是新启动一个子线程, 运行run()方法.

  • synchronized关键字?

  • synchronized缺陷?x

程序阻塞.如何才会释放?效率低下.

  • 非无期限的等待? 使用Lock的优势?

Lock在一定时间内未获取,会自动进行释放;
Lock在使用wait/notify的时候,可以使用不同的Condition进行控制唤醒的进程;
Lock可以将读锁写锁进行分离,提升系统的运行效率.

  • 常见的线程池种类 和 基本使用?

runnablecallable.线程的回调函数.


Reference

[1] Java面试:投行的15个多线程和并发面试题
[2] 40个Java多线程问题总结

展开阅读全文

没有更多推荐了,返回首页