共计 12230 个字符,预计需要花费 31 分钟才能阅读完成。
前言
集体收藏的 80 道 Java 多线程 / 并发经典面试题,当初给出 11-20 的答案解析哈,并且上传 github 哈~
https://github.com/whx123/Jav…
集体收藏的 80 道多线程并发面试题(1-10 答案解析)
11、为什么要用线程池?Java 的线程池外部机制,参数作用,几种工作阻塞队列,线程池类型以及应用场景
答复这些点:
- 为什么要用线程池?
- Java 的线程池原理
- 线程池外围参数
- 几种工作阻塞队列
- 线程池使用不当的问题
- 线程池类型以及应用场景
为什么要用线程池?
线程池:一个治理线程的池子。
- 治理线程,防止减少创立线程和销毁线程的资源损耗。
- 进步响应速度。
- 反复利用。
Java 的线程池执行原理
为了形象形容线程池执行,打个比喻:
- 外围线程比作公司正式员工
- 非核心线程比作外包员工
- 阻塞队列比作需要池
- 提交工作比作提需要
线程池外围参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:线程池外围线程数最大值
- maximumPoolSize:线程池最大线程数大小
- keepAliveTime:线程池中非核心线程闲暇的存活工夫大小
- unit:线程闲暇存活工夫单位
- workQueue:寄存工作的阻塞队列
- threadFactory:用于设置创立线程的工厂,能够给创立的线程设置有意义的名字,可不便排查问题。
- handler:线城池的饱和策略事件,次要有四种类型回绝策略。
四种回绝策略
- AbortPolicy(抛出一个异样,默认的)
- DiscardPolicy(间接抛弃工作)
- DiscardOldestPolicy(抛弃队列里最老的工作,将以后这个工作持续提交给线程池)
- CallerRunsPolicy(交给线程池调用所在的线程进行解决)
几种工作阻塞队列
- ArrayBlockingQueue(用数组实现的有界阻塞队列,按 FIFO 排序量)
- LinkedBlockingQueue(基于链表构造的阻塞队列,按 FIFO 排序工作,容量能够抉择进行设置,不设置的话,将是一个无边界的阻塞队列)
- DelayQueue(一个工作定时周期的提早执行的队列)
- PriorityBlockingQueue(具备优先级的无界阻塞队列)
- SynchronousQueue(一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作始终处于阻塞状态)
线程池使用不当的问题
线程池实用不当可能导致内存飙升问题哦
有趣味能够看我这篇文章哈: 源码角度剖析 -newFixedThreadPool 线程池导致的内存飙升问题
线程池类型以及应用场景
- newFixedThreadPool
实用于解决 CPU 密集型的工作,确保 CPU 在长期被工作线程应用的状况下,尽可能的少的调配线程,即实用执行长期的工作。
- newCachedThreadPool
用于并发执行大量短期的小工作。
- newSingleThreadExecutor
实用于串行执行工作的场景,一个工作一个工作地执行。
- newScheduledThreadPool
周期性执行工作的场景,须要限度线程数量的场景
- newWorkStealingPool
建一个含有足够多线程的线程池,来维持相应的并行级别,它会通过工作窃取的形式,使得多核的 CPU 不会闲置,总会有活着的线程让 CPU 去运行, 实质上就是一个 ForkJoinPool。)
有趣味能够看我这篇文章哈: 面试必备:Java 线程池解析
12、谈谈 volatile 关键字的了解
volatile 是面试官十分喜爱问的一个问题,能够答复以下这几点:
- vlatile 变量的作用
- 古代计算机的内存模型(嗅探技术,MESI 协定,总线)
- Java 内存模型(JMM)
- 什么是可见性?
- 指令重排序
- volatile 的内存语义
- as-if-serial
- Happens-before
- volatile 能够解决原子性嘛?为什么?
- volatile 底层原理,如何保障可见性和禁止指令重排(内存屏障)
vlatile 变量的作用?
- 保障变量对所有线程可见性
- 禁止指令重排
古代计算机的内存模型
- 其中高速缓存包含 L1,L2,L3 缓存~
- 缓存一致性协定,能够理解 MESI 协定
- 总线(Bus)是计算机各种性能部件之间传送信息的公共通信支线,CPU 和其余性能部件是通过总线通信的。
- 处理器应用嗅探技术保障它的外部缓存、零碎内存和其余处理器的缓存数据在总线上保持一致。
Java 内存模型(JMM)
什么是可见性?
可见性就是当一个线程 批改一个共享变量时,另外一个线程能读到这个批改的值。
指令重排序
指令重排是指在程序执行过程中, 为了进步性能, 编译器和 CPU 可能会对指令进行从新排序。
volatile 的内存语义
- 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。
- 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为有效。线程接下来将从主内存中读取共享变量。
as-if-serial
如果在本线程内察看,所有的操作都是有序的;即不管怎么重排序(编译器和处理器为了进步并行度),(单线程)程序的执行后果不会被扭转。
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 语义。
Happens-before
Java 语言中,有一个后行产生准则(happens-before):
- 程序秩序规定:在一个线程内,依照控制流程序,书写在后面的操作后行产生于书写在前面的操作。
- 管程锁定规定:一个 unLock 操作后行产生于前面对同一个锁额 lock 操作
- volatile 变量规定:对一个变量的写操作后行产生于前面对这个变量的读操作
- 线程启动规定 :Thread 对象的 start() 办法后行产生于此线程的每个一个动作
- 线程终止规定 :线程中所有的操作都后行产生于线程的终止检测,咱们能够通过 Thread.join() 办法完结、Thread.isAlive()的返回值伎俩检测到线程曾经终止执行
- 线程中断规定 :对线程 interrupt() 办法的调用后行产生于被中断线程的代码检测到中断事件的产生
- 对象终结规定 :一个对象的初始化实现后行产生于他的 finalize() 办法的开始
- 传递性:如果操作 A 后行产生于操作 B,而操作 B 又后行产生于操作 C,则能够得出操作 A 后行产生于操作 C
volatile 能够解决原子性嘛?为什么?
不能够,能够间接举 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 底层原理,如何保障可见性和禁止指令重排(内存屏障)
volatile 润饰的变量,转成汇编代码,会发现多出一个 lock 前缀指令。lock 指令相当于一个内存屏障,它保障以下这几点:
- 1. 重排序时不能把前面的指令重排序到内存屏障之前的地位
- 2. 将本处理器的缓存写入内存
- 3. 如果是写入动作,会导致其余处理器中对应的缓存有效。
2、3 点保障可见性,第 1 点禁止指令重排~
有趣味的敌人能够看我这篇文章哈:Java 程序员面试必备:Volatile 全方位解析
13、AQS 组件,实现原理
AQS,即 AbstractQueuedSynchronizer,是构建锁或者其余同步组件的根底框架,它应用了一个 int 成员变量示意同步状态,通过内置的 FIFO 队列来实现资源获取线程的排队工作。能够答复以下这几个关键点哈:
- state 状态的保护。
- CLH 队列
- ConditionObject 告诉
- 模板办法设计模式
- 独占与共享模式。
- 自定义同步器。
- AQS 全家桶的一些延长,如:ReentrantLock 等。
state 状态的保护
- state,int 变量,锁的状态,用 volatile 润饰,保障多线程中的可见性。
- getState()和 setState()办法采纳 final 润饰,限度 AQS 的子类重写它们两。
- compareAndSetState()办法采纳乐观锁思维的 CAS 算法操作确保线程平安, 保障状态
设置的原子性。
对 CAS 有趣味的敌人,能够看下我这篇文章哈~
CAS 乐观锁解决并发问题的一次实际
CLH 队列
CLH(Craig, Landin, and Hagersten locks) 同步队列 是一个 FIFO 双向队列,其外部通过节点 head 和 tail 记录队首和队尾元素,队列元素的类型为 Node。AQS 依赖它来实现同步状态 state 的治理,以后线程如果获取同步状态失败时,AQS 则会将以后线程曾经期待状态等信息结构成一个节点(Node)并将其退出到 CLH 同步队列,同时会阻塞以后线程,当同步状态开释时,会把首节点唤醒(偏心锁),使其再次尝试获取同步状态。
ConditionObject 告诉
咱们都晓得,synchronized 管制同步的时候,能够配合 Object 的 wait()、notify(),notifyAll() 系列办法能够实现期待 / 告诉模式。而 Lock 呢?它提供了条件 Condition 接口,配合 await(),signal(),signalAll() 等办法也能够实现期待 / 告诉机制。ConditionObject 实现了 Condition 接口,给 AQS 提供条件变量的反对。
ConditionObject 队列与 CLH 队列的爱恨情仇:
- 调用了 await()办法的线程,会被退出到 conditionObject 期待队列中,并且唤醒 CLH 队列中 head 节点的下一个节点。
- 线程在某个 ConditionObject 对象上调用了 singnal()办法后,期待队列中的 firstWaiter 会被退出到 AQS 的 CLH 队列中,期待被唤醒。
- 当线程调用 unLock()办法开释锁时,CLH 队列中的 head 节点的下一个节点(在本例中是 firtWaiter),会被唤醒。
模板办法设计模式
什么是模板设计模式?
在一个办法中定义一个算法的骨架,而将一些步骤提早到子类中。模板办法使得子类能够在不扭转算法构造的状况下,从新定义算法中的某些步骤。
AQS 的典型设计模式就是模板办法设计模式啦。AQS 全家桶(ReentrantLock,Semaphore)的衍生实现,就体现出这个设计模式。如 AQS 提供 tryAcquire,tryAcquireShared 等模板办法,给子类实现自定义的同步器。
独占与共享模式
- 独占式: 同一时刻仅有一个线程持有同步状态,如 ReentrantLock。又可分为偏心锁和非偏心锁。
- 共享模式: 多个线程可同时执行,如 Semaphore/CountDownLatch 等都是共享式的产物。
自定义同步器
你要实现自定义锁的话,首先须要确定你要实现的是独占锁还是共享锁,定义原子变量 state 的含意,再定义一个外部类去继承 AQS,重写对应的模板办法即可啦
AQS 全家桶的一些延长。
Semaphore,CountDownLatch,ReentrantLock
能够看下之前我这篇文章哈,AQS 解析与实战
14、什么是多线程环境下的伪共享
- 什么是伪共享
- 如何解决伪共享问题
什么是伪共享
伪共享定义?
CPU 的缓存是以缓存行 (cache line) 为单位进行缓存的,当多个线程批改互相独立的变量,而这些变量又处于同一个缓存行时就会影响彼此的性能。这就是伪共享
古代计算机计算模型,大家都有印象吧?我之前这篇文章也有讲过,有趣味能够看一下哈,Java 程序员面试必备:Volatile 全方位解析
- CPU 执行速度比内存速度快好几个数量级,为了进步执行效率,古代计算机模型演变出 CPU、缓存(L1,L2,L3),内存的模型。
- CPU 执行运算时,如先从 L1 缓存查问数据,找不到再去 L2 缓存找,顺次类推,直到在内存获取到数据。
- 为了防止频繁从内存获取数据,聪慧的科学家设计出缓存行,缓存行大小为 64 字节。
也正是因为缓存行,就导致伪共享问题的存在,如图所示:
假如数据 a、b 被加载到同一个缓存行。
- 当线程 1 批改了 a 的值,这时候 CPU1 就会告诉其余 CPU 核,以后缓存行(Cache line)曾经生效。
- 这时候,如果线程 2 发动批改 b,因为缓存行曾经生效了,所以core2 这时会从新从主内存中读取该 Cache line 数据。读完后,因为它要批改 b 的值,那么 CPU2 就告诉其余 CPU 核,以后缓存行(Cache line)又曾经生效。
- 酱紫,如果同一个 Cache line 的内容被多个线程读写,就很容易产生相互竞争,频繁回写主内存,会大大降低性能。
如何解决伪共享问题
既然伪共享是因为互相独立的变量存储到雷同的 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
能够发现利用填充数据的形式,让读写的变量宰割到不同缓存行,能够很好挺高性能~
15、说一下 Runnable 和 Callable 有什么区别?
- Callable 接口办法是 call(),Runnable 的办法是 run();
- Callable 接口 call 办法有返回值,反对泛型,Runnable 接口 run 办法无返回值。
- Callable 接口 call()办法容许抛出异样;而 Runnable 接口 run()办法不能持续上抛异样;
@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
16、wait(),notify()和 suspend(),resume()之间的区别
- wait() 使得线程进入阻塞期待状态,并且开释锁
- notify()唤醒一个处于期待状态的线程,它个别跟 wait()办法配套应用。
- suspend()使得线程进入阻塞状态,并且不会主动复原,必须对应的 resume() 被调用,能力使得线程从新进入可执行状态。suspend()办法很容易引起死锁问题。
- resume()办法跟 suspend()办法配套应用。
suspend()不倡议应用 ,suspend() 办法在调用后,线程不会开释曾经占有的资 源(比方锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。
17.Condition 接口及其实现原理
- Condition 接口与 Object 监视器办法比照
- Condition 接口应用 demo
- Condition 实现原理
Condition 接口与 Object 监视器办法比照
Java 对象(Object),提供 wait()、notify(),notifyAll() 系列办法,配合 synchronized,能够实现期待 / 告诉模式。而 Condition 接口配合 Lock,通过 await(),signal(),signalAll() 等办法,也能够实现相似的期待 / 告诉机制。
比照项 | 对象监督办法 | Condition |
---|---|---|
前置条件 | 取得对象的锁 | 调用 Lock.lock()获取锁, 调用 Lock.newCondition()取得 Condition 对象 |
调用形式 | 间接调用,object.wait() | 间接调用,condition.await() |
期待队列数 | 1 个 | 多个 |
以后线程开释锁并进入期待状态 | 反对 | 反对 |
在期待状态中不响应中断 | 不反对 | 反对 |
以后线程开释锁并进入超时期待状态 | 反对 | 反对 |
以后线程开释锁并进入期待状态到未来的某个工夫 | 不反对 | 反对 |
唤醒期待队列中的一个线程 | 反对 | 反对 |
唤醒期待队列中的全副线程 | 反对 | 反对 |
Condition 接口应用 demo
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();
}
}
}
Condition 实现原理
其实,同步队列和期待队列中节点类型都是同步器的动态外部类 AbstractQueuedSynchronizer.Node,接下来咱们图解一下 Condition 的实现原理~
期待队列的根本结构图
一个 Condition 蕴含一个期待队列,Condition 领有首节点(firstWaiter)和尾节点(lastWaiter)。以后线程调用 Condition.await()办法,将会以以后线程结构节点,并将节点从尾部退出期待队
AQS 结构图
ConditionI 是跟 Lock 一起联合应用的,底层跟同步器(AQS)相干。同步器领有一个同步队列和多个期待队列~
期待
当调用 await()办法时,相当于同步队列的首节点(获取了锁的节点)挪动到 Condition 的期待队列中。
告诉
调用 Condition 的 signal()办法,将会唤醒在期待队列中等待时间最长的节点(首节点),在
唤醒节点之前,会将节点移到同步队列中。
18、线程池如何调优,最大数目如何确认?
在《Java Concurrency in Practice》一书中,有一个评估线程池线程大小的公式
Nthreads=NcpuUcpu(1+w/c)
- Ncpu = CPU 总核数
- Ucpu =cpu 使用率,0~1
- W/C= 等待时间与计算工夫的比率
假如 cpu 100% 运行,则公式为
Nthreads=Ncpu*(1+w/c)
估算的话,酱紫:
- 如果是IO 密集型利用(如数据库数据交互、文件上传下载、网络数据传输等等),IO 操作个别比拟耗时,等待时间与计算工夫的比率(w/c)会大于 1,所以最佳线程数预计就是 Nthreads=Ncpu*(1+1)= 2Ncpu。
- 如果是CPU 密集型利用(如算法比较复杂的程序),最现实的状况,没有期待,w=0,Nthreads=Ncpu。又对于计算密集型的工作,在领有 N 个处理器的零碎上,当线程池的大小为 N + 1 时,通常能实现最优的效率。所以 Nthreads = Ncpu+1
有具体指参考呢?举个例子
比方均匀每个线程 CPU 运行工夫为 0.5s,而线程等待时间(非 CPU 运行工夫,比方 IO)为 1.5s,CPU 外围数为 8,那么依据下面这个公式估算失去:线程池大小 =(1+1.5/05)*8 =32。
参考了网上这篇文章,写得很棒,有趣味的敌人能够去看一下哈:
- 依据 CPU 外围数确定线程池并发线程数
19、假如有 T1、T2、T3 三个线程,你怎么保障 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?
能够应用 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 次
20. LockSupport 作用是?
- LockSupport 作用
- park 和 unpark,与 wait,notify 的区别
- Object blocker 作用?
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 的性能。然而
- park 不须要获取对象锁
- 中断的时候 park 不会抛出 InterruptedException 异样,须要在 park 之后自行判断中断状态
- 应用 park 和 unpark 的时候,能够不必放心 park 的时序问题造成死锁
- LockSupport 不须要在同步代码块里
- unpark 却能够唤醒一个指定的线程,notify 只能随机抉择一个线程唤醒
Object blocker 作用?
不便在线程 dump 的时候看到具体的阻塞对象的信息。
公众号
参考与感激
- 《java 并发编程的艺术》
- 杂谈 什么是伪共享(false sharing)?
- 依据 CPU 外围数确定线程池并发线程数
- LockSupport 的用法及原理
- 探讨缓存行与伪共享