个人珍藏的80道Java多线程/并发经典面试题,现在给出11-20的答案解析哈,并且上传github哈~
❝https://github.com/whx123/JavaHome ❞
回答这些点:
线程池:一个管理线程的池子。
为了形象描述线程池执行,打个比喻:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
「四种拒绝策略」
线程池适用不当可能导致内存飙升问题哦
有兴趣可以看我这篇文章哈:源码角度分析-newFixedThreadPool线程池导致的内存飙升问题[2]
❝适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。 ❞
❝用于并发执行大量短期的小任务。 ❞
❝适用于串行执行任务的场景,一个任务一个任务地执行。 ❞
❝周期性执行任务的场景,需要限制线程数量的场景 ❞
❝建一个含有足够多线程的线程池,来维持相应的并行级别,它会通过工作窃取的方式,使得多核的 CPU 不会闲置,总会有活着的线程让 CPU 去运行,本质上就是一个 ForkJoinPool。) ❞
有兴趣可以看我这篇文章哈:面试必备:Java线程池解析[3]
volatile是面试官非常喜欢问的一个问题,可以回答以下这几点:
可见性就是当一个线程 修改一个共享变量时,另外一个线程能读到这个修改的值。
指令重排是指在程序执行过程中,为了提高性能, 编译器和CPU可能会对指令进行重新排序。
如果在本线程内观察,所有的操作都是有序的;即不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不会被改变。
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C
步骤C依赖于步骤A和B,因为指令重排的存在,程序执行顺讯可能是A->B->C,也可能是B->A->C,但是C不能在A或者B前面执行,这将违反as-if-serial语义。
Java语言中,有一个先行发生原则(happens-before):
不可以,可以直接举i++那个例子,原子性需要synchronzied或者lock保证
public class Test {
public volatile int race = 0;
public void increase() {
race++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<100;j++)
test.increase();
};
}.start();
}
//等待所有累加线程结束
while(Thread.activeCount()>1)
Thread.yield();
System.out.println(test.race);
}
}
volatile 修饰的变量,转成汇编代码,会发现多出一个lock前缀指令。lock指令相当于一个内存屏障,它保证以下这几点:
2、3点保证可见性,第1点禁止指令重排~
有兴趣的朋友可以看我这篇文章哈:Java程序员面试必备:Volatile全方位解析[4]
AQS,即AbstractQueuedSynchronizer,是构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。可以回答以下这几个关键点哈:
对CAS有兴趣的朋友,可以看下我这篇文章哈~CAS乐观锁解决并发问题的一次实践[5]
❝「CLH(Craig, Landin, and Hagersten locks) 同步队列」 是一个FIFO双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node。AQS依赖它来完成同步状态state的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。 ❞
我们都知道,synchronized控制同步的时候,可以配合Object的wait()、notify(),notifyAll() 系列方法可以实现等待/通知模式。而Lock呢?它提供了条件Condition接口,配合await(),signal(),signalAll() 等方法也可以实现等待/通知机制。ConditionObject实现了Condition接口,给AQS提供条件变量的支持 。
ConditionObject队列与CLH队列的爱恨情仇:
什么是模板设计模式?
❝在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。 ❞
AQS的典型设计模式就是模板方法设计模式啦。AQS全家桶(ReentrantLock,Semaphore)的衍生实现,就体现出这个设计模式。如AQS提供tryAcquire,tryAcquireShared等模板方法,给子类实现自定义的同步器。
你要实现自定义锁的话,首先需要确定你要实现的是独占锁还是共享锁,定义原子变量state的含义,再定义一个内部类去继承AQS,重写对应的模板方法即可啦
Semaphore,CountDownLatch,ReentrantLock
可以看下之前我这篇文章哈,AQS解析与实战[6]
伪共享定义?
❝CPU的缓存是以缓存行(cache line)为单位进行缓存的,当多个线程修改相互独立的变量,而这些变量又处于同一个缓存行时就会影响彼此的性能。这就是伪共享 ❞
现代计算机计算模型,大家都有印象吧?我之前这篇文章也有讲过,有兴趣可以看一下哈,Java程序员面试必备:Volatile全方位解析[7]
也正是因为缓存行,就导致伪共享问题的存在,如图所示:
假设数据a、b被加载到同一个缓存行。
既然伪共享是因为相互独立的变量存储到相同的Cache line导致的,一个缓存行大小是64字节。那么,我们就可以「使用空间换时间」,即数据填充的方式,把独立的变量分散到不同的Cache line~
共享内存demo例子:
public class FalseShareTest {
public static void main(String[] args) throws InterruptedException {
Rectangle rectangle = new Rectangle();
long beginTime = System.currentTimeMillis();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100000000; i++) {
rectangle.a = rectangle.a + 1;
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100000000; i++) {
rectangle.b = rectangle.b + 1;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("执行时间" + (System.currentTimeMillis() - beginTime));
}
}
class Rectangle {
volatile long a;
volatile long b;
}
运行结果:
执行时间2815
一个long类型是8字节,我们在变量a和b之间不上7个long类型变量呢,输出结果是啥呢?如下:
class Rectangle {
volatile long a;
long a1,a2,a3,a4,a5,a6,a7;
volatile long b;
}
运行结果:
执行时间1113
可以发现利用填充数据的方式,让读写的变量分割到不同缓存行,可以很好挺高性能~
@FunctionalInterface
public interface Callable<V> {
/**
* 支持泛型V,有返回值,允许抛出异常
*/
V call() throws Exception;
}
@FunctionalInterface
public interface Runnable {
/**
* 没有返回值,不能继续上抛异常
*/
public abstract void run();
}
看下demo代码吧,这样应该好理解一点哈~
/*
* @Author 捡田螺的小男孩
* @date 2020-08-18
*/
public class CallableRunnableTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
Callable <String> callable =new Callable<String>() {
@Override
public String call() throws Exception {
return "你好,callable";
}
};
//支持泛型
Future<String> futureCallable = executorService.submit(callable);
try {
System.out.println(futureCallable.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("你好呀,runnable");
}
};
Future<?> futureRunnable = executorService.submit(runnable);
try {
System.out.println(futureRunnable.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
executorService.shutdown();
}
}
运行结果:
你好,callable
你好呀,runnable
null
「suspend()不建议使用」,suspend()方法在调用后,线程不会释放已经占有的资 源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。
Java对象(Object),提供wait()、notify(),notifyAll() 系列方法,配合synchronized,可以实现等待/通知模式。而Condition接口配合Lock,通过await(),signal(),signalAll() 等方法,也可以实现类似的等待/通知机制。
对比项 | 对象监视方法 | Condition |
---|---|---|
前置条件 | 获得对象的锁 | 调用Lock.lock()获取锁,调用Lock.newCondition()获得Condition对象 |
调用方式 | 直接调用,object.wait() | 直接调用,condition.await() |
等待队列数 | 1个 | 多个 |
当前线程释放锁并进入等待状态 | 支持 | 支持 |
在等待状态中不响应中断 | 不支持 | 支持 |
当前线程释放锁并进入超时等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态到将来的某个时间 | 不支持 | 支持 |
唤醒等待队列中的一个线程 | 支持 | 支持 |
唤醒等待队列中的全部线程 | 支持 | 支持 |
public class ConditionTest {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
}
其实,同步队列和等待队列中节点类型都是同步器的静态内部类 AbstractQueuedSynchronizer.Node,接下来我们图解一下Condition的实现原理~
「等待队列的基本结构图」
❝一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点 (lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队 ❞
「AQS 结构图」
ConditionI是跟Lock一起结合使用的,底层跟同步器(AQS)相关。同步器拥有一个同步队列和多个等待队列~
「等待」
❝当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。 ❞
「通知」
❝调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在 唤醒节点之前,会将节点移到同步队列中。 ❞
在《Java Concurrency in Practice》一书中,有一个评估线程池线程大小的公式
❝「Nthreads=NcpuUcpu(1+w/c)」
❞
假设cpu 100%运转,则公式为
Nthreads=Ncpu*(1+w/c)
「估算的话,酱紫:」
有具体指参考呢?举个例子
❝比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:线程池大小=(1+1.5/05)*8 =32。 ❞
参考了网上这篇文章,写得很棒,有兴趣的朋友可以去看一下哈:
可以使用「join方法」解决这个问题。比如在线程A中,调用线程B的join方法表示的意思就是**:A等待B线程执行完毕后(释放CPU执行权),在继续执行。**
代码如下:
public class ThreadTest {
public static void main(String[] args) {
Thread spring = new Thread(new SeasonThreadTask("春天"));
Thread summer = new Thread(new SeasonThreadTask("夏天"));
Thread autumn = new Thread(new SeasonThreadTask("秋天"));
try
{
//春天线程先启动
spring.start();
//主线程等待线程spring执行完,再往下执行
spring.join();
//夏天线程再启动
summer.start();
//主线程等待线程summer执行完,再往下执行
summer.join();
//秋天线程最后启动
autumn.start();
//主线程等待线程autumn执行完,再往下执行
autumn.join();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
class SeasonThreadTask implements Runnable{
private String name;
public SeasonThreadTask(String name){
this.name = name;
}
@Override
public void run() {
for (int i = 1; i <4; i++) {
System.out.println(this.name + "来了: " + i + "次");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
春天来了: 1次
春天来了: 2次
春天来了: 3次
夏天来了: 1次
夏天来了: 2次
夏天来了: 3次
秋天来了: 1次
秋天来了: 2次
秋天来了: 3次
LockSupport是个工具类,它的主要作用是挂起和唤醒线程, 该工具类是创建锁和其他同步类的基础。
public static void park(); //挂起当前线程,调用unpark(Thread thread)或者当前线程被中断,才能从park方法返回
public static void parkNanos(Object blocker, long nanos); // 挂起当前线程,有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 挂起当前线程,直到某个时间
public static void park(Object blocker); //挂起当前线程
public static void unpark(Thread thread); // 唤醒当前thread线程
看个例子吧:
public class LockSupportTest {
public static void main(String[] args) {
CarThread carThread = new CarThread();
carThread.setName("劳斯劳斯");
carThread.start();
try {
Thread.currentThread().sleep(2000);
carThread.park();
Thread.currentThread().sleep(2000);
carThread.unPark();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class CarThread extends Thread{
private boolean isStop = false;
@Override
public void run() {
System.out.println(this.getName() + "正在行驶中");
while (true) {
if (isStop) {
System.out.println(this.getName() + "车停下来了");
LockSupport.park(); //挂起当前线程
}
System.out.println(this.getName() + "车还在正常跑");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void park() {
isStop = true;
System.out.println("停车啦,检查酒驾");
}
public void unPark(){
isStop = false;
LockSupport.unpark(this); //唤醒当前线程
System.out.println("老哥你没酒驾,继续开吧");
}
}
}
运行结果:
劳斯劳斯正在行驶中
劳斯劳斯车还在正常跑
劳斯劳斯车还在正常跑
停车啦,检查酒驾
劳斯劳斯车停下来了
老哥你没酒驾,继续开吧
劳斯劳斯车还在正常跑
劳斯劳斯车还在正常跑
劳斯劳斯车还在正常跑
劳斯劳斯车还在正常跑
劳斯劳斯车还在正常跑
劳斯劳斯车还在正常跑
LockSupport的park和unpark的实现,有点类似wait和notify的功能。但是
❝
❞
Object blocker作用?
❝方便在线程dump的时候看到具体的阻塞对象的信息。 ❞
[1]
个人珍藏的80道多线程并发面试题(1-10答案解析): https://juejin.im/post/6854573221258199048
[2]
源码角度分析-newFixedThreadPool线程池导致的内存飙升问题: https://juejin.im/post/6844903930502070285
[3]
面试必备:Java线程池解析: https://juejin.im/post/6844903889678893063
[4]
Java程序员面试必备:Volatile全方位解析: https://juejin.im/post/6859390417314512909
[5]
CAS乐观锁解决并发问题的一次实践: https://juejin.im/post/6844903869340712967#comment
[6]
AQS解析与实战: https://juejin.im/post/6844903903188746247
[7]
Java程序员面试必备:Volatile全方位解析: https://juejin.im/post/6859390417314512909
[8]
根据CPU核心数确定线程池并发线程数: https://www.cnblogs.com/dennyzhangdd/p/6909771.html
[9]
杂谈 什么是伪共享(false sharing)?: https://juejin.im/post/6844903841964507150
[10]
根据CPU核心数确定线程池并发线程数: https://www.cnblogs.com/dennyzhangdd/p/6909771.html
[11]
LockSupport的用法及原理: https://www.jianshu.com/p/f1f2cd289205