java经典面试题基础篇(持续更新)
多线程概述
package com.vector.mallsearch.thread;
import java.util.Optional;
import java.util.concurrent.*;
/**
* @ClassName ThreadTest
* @Description TODO
* @Author YuanJie
* @Date 2022/8/17 10:13
*/
public class ThreadTest {
// 当前系统中应当只有一两个池,每个异步任务交由线程池执行
/**
* 七大参数
* corePoolSize – the number of threads to keep in the pool, even if they are idle, unless allowCoreThreadTimeOut is set
* maximumPoolSize – the maximum number of threads to allow in the pool
* keepAliveTime – (maximumPoolSize - corePoolSize)when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
* unit – the time unit for the keepAliveTime argument
* workQueue – the queue to use for holding tasks before they are executed. This queue will hold only the Runnable tasks submitted by the execute method.
* threadFactory – the factory to use when the executor creates a new thread
* handler – the handler to use when execution is blocked because the thread bounds and queue capacities are reached
*/
/**
* 工作顺序:
* 1)、线程池创建,准备好core数量的核心线程,准备接受任务
* 1.1、core满了,就将再进来的任务放入阻塞队列中。空闲的core就会自己去阻塞队列获取任务执行
* 1.2、阻塞队列满了,就直接开新线程执行,最大只能开到max指定的数量
* 1.3、max满了就用RejectedExecutionHandLer拒绝任务
* 1.4、max都执行完成,有很多空闲.在指定的时间keepAliveTime以后,释放max-core这些线程
*
* new LinkedBLoclingDeque<>():默认是Integer的最大值。内存不够
*
* 拒绝策略: DiscardOldestPolicy丢弃最旧的任务
* AbortPolicy 丢弃新任务并抛出异常
* CallerRunsPolicy 峰值同步调用
* DiscardPolicy 丢弃新任务不抛异常
* 一个线程池core 7; max 20 , queue: 50,100并发进来怎么分配的;
* 7个会立即得到执行,50个会进入队列,再开13个进行执行。剩下的30个就使用拒绝策略。
*/
// 创建自定义线程
public static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,
200,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) throws ExecutionException, InterruptedException {
// CompletableFuture异步编排,类似vue中promise,Docker-compose容器编排
System.out.println("main...start....");
// 无返回值
// CompletableFuture voidCompletableFuture = CompletableFuture.runAsync(() -> {
// System.out.println("当前线程池: " + Thread.currentThread().getId());
// int i = 10 / 2;
// System.out.println("运行结果: " + i);
// }, threadPoolExecutor);
// 有返回值 且进行异步编排
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程池: " + Thread.currentThread().getId());
String i = String.valueOf(10 / 2);
System.out.println("运行结果: " + i);
return i;
}, threadPoolExecutor)
// 接收上一步的结果和异常
.handle((result,error)->{
if(Optional.ofNullable(result).isPresent()){
return "success";
}
return "error";
});
String s = future.get();
System.out.println("main...end...." + s);
// 线程串行化 thenRunAsync 没法获取上一步执行结果
System.out.println("main---start---");
CompletableFuture<Void> VoidCompletableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程池: " + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果: " + i);
return i;
}, threadPoolExecutor)
.thenRunAsync(()-> {
System.out.println("thenRunAsync()继续执行其他任务,没法获取上一步执行结果");
},threadPoolExecutor);
System.out.println("main---end---");
// 线程串行化 thenAcceptAsync 能接收上一步结果但无返回值
CompletableFuture<Void> NullCompletableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程池: " + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果: " + i);
return i;
}, threadPoolExecutor)
.thenAcceptAsync((result)-> {
System.out.println("thenRunAsync()作为程序的最后执行结果,无返回值, i= "+ result);
},threadPoolExecutor);
// 线程串行化 thenApplyAsync 可以处理上一步结果,有返回值
System.out.println("main---start---");
CompletableFuture<String> stringCompletableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程池: " + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果: " + i);
return i;
}, threadPoolExecutor)
.thenApplyAsync((result)-> {
System.out.println("thenRunAsync()可以处理上一步结果,有返回值");
result = result*2;
return "最新的i = "+result;
},threadPoolExecutor);
System.out.println("main---end---"+stringCompletableFuture.get());
// 线程串行化 多任务组合
System.out.println("main...start....");
CompletableFuture<String> work01 = CompletableFuture.supplyAsync(()-> {
System.out.println("任务work01进行中");
return "info 1";
},threadPoolExecutor);
CompletableFuture<String> work02 = CompletableFuture.supplyAsync(()-> {
System.out.println("任务work02进行中");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "info 2";
},threadPoolExecutor);
CompletableFuture<String> work03 = CompletableFuture.supplyAsync(()-> {
System.out.println("任务work03进行中");
return "info 3";
},threadPoolExecutor);
// work01.get(); work02.get(); work03.get();.... get乃是每个线程都会被阻塞等待结果
// 而allof()是一个非阻塞等待方法
CompletableFuture<Void> allResult = CompletableFuture.allOf(work01,work02,work03);
// 等待最长的任务执行完毕后,获得最终结果
allResult.get();
System.out.println("main...end...."+work01.get()+"=>"+work02.get()+"=>"+work03.get());
// // 一个成功即可
// CompletableFuture
// System.out.println("main...end...."+allResult.get());
}
}
共同点
- wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状
态
不同点
不同点
语法层面
功能层面
性能层面
注意: 加在成员方法上的synchronized关键字相当于 synchronized(this) 锁住的this对象;
加在静态方法上的synchronized关键字相当于synchronized(类.clss)锁住的当前类对象
常规monitor监控锁由操作系统提供.
前置知识java对象头:
在JDK1.6之后 synchronized锁引入了锁升级的过程 无锁、偏向锁、轻量级锁、重量级锁
,4种状态,4种状态会随着竞争的情况逐渐升级,升级的过程是不可逆的
轻量级锁适用场景: 如果一个对象虽然有多线程访问,但多线程访问没有竞争
,那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是synchronized
1.当调用synchronized(obj)每个线程的栈帧会包含锁记录的结构(该结构是jvm层),内部可以存储锁定对象的mark word .让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word并将Mark Word的值存入锁记录.如下图
如果cas失败.
失败情况1 object被其他线程尝试加锁,lock record记录的是当前线程的地址,且状态为00.此时表明有竞争进入锁膨胀过程.
当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头
成功,则解锁成功
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
2.进入锁膨胀流程(为使加锁失败的线程进入阻塞状态等待).即为Object对象申请Monitor锁,让Object指向重量级锁地址
然后自己进入Monitor的EntryList BLOCKED.上图变更为下图所示:
waitSet: 当有A线程获得了锁.但执行条件不满足.调用wait()方法.进入waitSet队列等待(此时其他阻塞线程可以被唤醒获得锁).当条件满足被notify()唤醒重回EntryList阻塞队列.
3 .当Thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给对象头失败。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程
锁膨胀期间
的自旋优化在Java6之后自旋锁是自适应的,比如对象刚网刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
java7之后不能控制是否开启自旋功能
自旋优化核心: 重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了 同步块,释放了锁),这时当前线程就可以避免阻塞。
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。为优化这部分性能synchronized在无锁和轻量级锁中间增加偏向锁.
偏向锁核心: 偏向锁是比轻量级锁性能更高的锁.偏向锁在多线程无竞争状态下,A线程触发偏向锁状态,锁释放时不触发Mark work重置.当由B线程调用时撤销偏向锁.如果少于一定的撤销阈值(即被其他线程调用阈值).B线程调用结束时恢复A线程偏向锁.(其他撤销情况详见下文描述)
重温 java对象头mark work信息
言归正传
从 JDK6 开始,虽然 JVM 默认开启偏向锁,但是默认延时 4 秒开启(JVM 在启动的时候需要加载资源,这些对象加上偏向锁没有任何意义,减少了大量偏向锁撤销的成本).如果想避免延迟,可以加VM参数-Xx:BiasedLockingStartupDelay=0来禁用延迟
如果没有开启偏向锁,那么对象创建后,第一次用到hashcode时才会赋值.
Java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有.
偏向锁适合冲突较少的情况.在多线程竞争激烈时我们可以在添加VM参数-XX: -UseBiasedLocking
禁用偏向锁
轻量级锁调用对象的hashcode()或System.identityHashCode( )方法会导致偏向锁失效降为无锁状态.而重量级锁不会.原因如下
哈希码将放置到 Mark Word 中,内置锁变成无锁状态,偏向锁将被撤销
其他线程(无竞争情况)获取该对象锁,会使偏向锁升级到轻量级锁.有竞争则锁膨胀升级(要保证等待线程进入阻塞态).
批量重偏向: 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重
置对象的Thread ID.当撤销偏向锁阈值超过20次后,会在给这些对象加锁时重新偏向至加锁线程.而不会升级为轻量级锁.当撤销偏向锁阈值超过40次后,整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的.
锁消除核心: 逃逸分析,JIT即时编译器中的C2编译器(JDK19 GraalVM取代C2)会对调用超过一定阈值的热点代码进行优化.C2发现局部变量不会逃逸方法的作用范围,即局部变量不可被共享.
但是经过我的测试,不管禁不禁用JIT都差距都不大. ns捕捉n次。也测不出差距。
锁消除测试示例:
/**
* @ClassName Main
* @Description 锁消除 jdk1.8 测试套件jmh-core jmh-generator-annprocess
* @Author YuanJie
* @Date 2023/1/27 14:01
*/
@Fork(1) // 指定fork出多少个子进程来执行同一基准测试方法。
@BenchmarkMode(Mode.AverageTime) // 求平均时间
@Warmup(iterations = 3) // 预热jvm,防止首次执行造成不准确
@Measurement(iterations = 6) // 测试轮次
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 输出的单位是毫秒
@State(value = Scope.Benchmark) // 代码测试作用域
public class TestLockPerformance {
static int x = 1000;
@Benchmark
public void test1() throws Exception {
for (int i = 0; i < x; i++) {
System.out.println("test1 running... " + i);
}
}
@Benchmark
public void test2() throws Exception {
Object o = new Object();
for (int i = 0; i < x; i++) {
synchronized (o) {
System.out.println("test2 running... " + i);
}
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(TestLockPerformance.class.getSimpleName())
.result("result.json")
.resultFormat(ResultFormatType.JSON).build();
new Runner(opt).run();
}
}
不禁用JIT无锁和偏向锁耗时测试报告
第一次程序启动6论测试 无锁和偏向锁耗时相差不大
第二次程序启动6论测试 无锁和偏向锁耗时差距不大
禁用JIT无锁和偏向锁耗时测试报告
jvm参数指令手册jdk8
编译阶段赋值语句会被拆分成多条。当a线程刚获取静态值,被b线程打断。那么b线程获取的也是原始数据,且b线程执行完,不影响a线程获取的静态值。那么a线程继续执行还是利用a已经获取的值,无法感知b修改的值。造成数据错乱。volatile 并不能解决原子性
要么指令是原子的,要么多条指令作为一个整体,不能被其他线程指令插队。可以使用lock或synchronized锁,cas等
public class AddAndSubtract {
static volatile int balance = 10;
public static void subtract() {
// balance -= 5;
int b = balance;
b -= 5;
balance = b;
}
public static void add() {
// balance += 5;
int b = balance;
b += 5;
balance = b;
}
public static void main(String[] args) throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(2);
new Thread(() -> {
subtract();
latch.countDown();
}).start();
new Thread(() -> {
add();
latch.countDown();
}).start();
latch.await();
}
}
public class ForeverLoop {
static boolean stop = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
stop = true;
System.out.println("stop update true....");
}).start();
// 测试其他线程是否能读到
new Thread(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(stop);
}).start();
foo();
}
static void foo(){
int i=0;
while (!stop){
i++;
}
System.out.println("stop...."+ i);
}
}
为什么会出现这种情况,程序不终止呢?网上大部分传言是主内存模型与工作模型,工作内存中修改的变量并未同步到主内存而相关解释通过上述实践表明不正确。而我们用多个线程(避免是同一个cpu线程)同步打印,获取了主内存中的stop变量。可以发现工作内存修改的值已经同步到主内存了。
这一错误可能是根据周志明前辈书中如下内容引起的
但是请注意
在jvm虚拟机中,HotSpot引入了jit解释器。jit在优化代码时候
解释器JIT对热点代码的优化。发现cpu频繁调用物理内存。那么JIT会大胆预测调了上万次是false,他就把你频繁替换的物理内存替换为false,不让你再频繁调内存了。
也可以降低修改线程时间,让jit认为该循环非热点代码。
根本解决方案还是volatile,被volatile修饰不做优化,这个指令会强制cpu去主存读数据
关于如何优化的可以进一步看周志明第三版《深入理解java虚拟机的内容》
和这位博主的博客
深入理解java虚拟机(十三) Java 即时编译器JIT机制以及编译优化
感谢北京-战斗,大佬说了很多,我断章取义了。详情大佬后续博文
public class SyncVsCas {
static final Unsafe U;
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
U = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
static final long BALANCE;
static {
try {
BALANCE = U.objectFieldOffset(Account.class.getDeclaredField("balance"));
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public static void cas(Account account) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
int o = account.balance;
int i = o + 5;
/**
* debug修改o观察
* 要修改的对象
* 修改偏移量
* 旧值
* 新值
*/
// cas保证原子性
if (U.compareAndSwapInt(account, BALANCE, o, i)) {
break;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
while (true) {
int o = account.balance;
int i = o - 5;
// cas保证原子性
if (U.compareAndSwapInt(account, BALANCE, o, i)) {
break;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(account.balance);
}
static class Account {
// volatile保证可见性,顺序性
volatile int balance = 10;
}
public static void main(String[] args) throws InterruptedException {
Account account = new Account();
cas(account);
}
}
public class SyncVsCas {
static final Unsafe U;
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
U = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
static final long BALANCE;
static {
try {
BALANCE = U.objectFieldOffset(Account.class.getDeclaredField("balance"));
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
static class Account {
// volatile保证可见性,顺序性
volatile int balance = 10;
}
public static void sync(Account account) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (account) {
int old = account.balance;
int n = old - 5;
account.balance = n;
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (account) {
int old = account.balance;
int n = old + 5;
account.balance = n;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(account.balance);
}
public static void main(String[] args) throws InterruptedException {
Account account = new Account();
sync(account);
}
}
什么是ABA:在CAS过程中,线程1、线程2分别从内存中拿到了当前值为A,同时线程2把当前值A改为B,随后又把B改回来变为A,此后线程1检查到当前值仍为A而导致执行cas成功,但这个过程却发生了ABA问题,现场资源可能和当初不一样了(线程2把当前值由A->B->A)
解决方法:版本号机制,利用版本号标记线程1拿到的‘当前值’的版本,若线程2进行了A->B->A操作,则版本号会改变,那线程1再次拿到的‘当前值’的版本和第一次的肯定是不同的,从而判定cas失败;
java代码中AtomicStampedReference类的cas方法实现了版本号机制,可用它来解决ABA问题
8版本扩容细节视频详解
public class TestThreadLocal {
public static void main(String[] args) {
Utils.getConnection();
}
static class Utils {
private static final ThreadLocal<String> tl = new ThreadLocal<>();
public static void getConnection() {
String str = tl.get(); // 到当前线程获取资源
System.out.println(str);
if(str == null){ // 当前线程不存在就创建资源
str =innerGetConnection();
tl.set(str);
}
System.gc();
System.out.println("gc后tl的值->"+ tl.get());
tl.remove();
System.out.println("手动移除后tl->"+tl);
System.out.println("手动移除后tl的值->"+tl.get());
}
}
private static String innerGetConnection(){
return "test";
}
}
ThreadLocalMap 中的 key 被设计为弱引用,原因如下
- Thread 可能需要长时间运行(如线程池中的线程),如果 key 不再使用,需要在内存不足
(GC)时释放其占用的内存
内存释放时机:
不可行,threadLoacl对象一般为静态变量,强引用。GC无法回收
不可行,threadLoacl对象一般为静态变量,强引用。GC无法回收
可行,必须
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部关联的强引用,那么在虚拟机进行垃圾回收时,这个ThreadLocal会被回收,这样,ThreadLocalMap中就会出现key为null的Entry,这些key对应的value也就再无妨访问,但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread销毁时,value才能得到释放。
当有多个线程调用同一个方法.在方法栈中,每个线程会复制一份在自己的栈帧中.而局部变量也是栈帧中私有的.互相独立,互不影响.
成员变量注意变量共享导致的线程问题
错误方法: 1.调用线程的stop()停止.可能造成B未释放锁.出现死锁
2.调用System.exit(int).会直接停止程序
两阶段终止模式:
/**
* @ClassName Main
* @Description TODO
* @Author YuanJie
* @Date 2023/1/27 14:01
*/
@Slf4j
public class Main {
private Thread monitor;
// 启动监控线程
public void start(){
monitor = new Thread(()->{
while (true){
Thread currentThread = Thread.currentThread();
if(currentThread.isInterrupted()){
log.info("当前线程被打断结束");
break;
}
try {
// wait,join,sleep被打断
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
// 重置打断标记
currentThread.interrupt();
}
}
});
monitor.start();
}
public void stop(){
monitor.interrupt();
}
}