- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
王有志 ,一个分享硬核Java技术的互金摸鱼侠 加入Java人的提桶跑路群: 共同富裕的Java人 。
平时我在网上冲浪的时候,收集了不少八股文和面试文,内容虽然多,但质量上良莠不齐,主打一个不假思索的互相抄,使得很多错误内容一代代得“传承”了下来。所以,我对收集的内容做了归纳和整理,通过查阅资料重新做了解答,并给出了每道八股文评分.
好了,废话不多说我们进入正题,今天的主题是 Java 面试中线程相关的八股文 ,主要涉及以下内容:
由于本人水平有限,解答过程中难免出现错误,还请大家以批评指正为主,尽量不要喷~~ 。
Tips :
这部分是并发编程中的基础概念和理论基础,整体难度较低,并且当你有了一定的工作年限后,很少会涉及这类问题,大家以了解为主.
并发 ,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行.
并行 ,在操作系统中是指,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。对比地,并发是指:在同一个时间段内,两个或多个程序执行,有时间上的重叠(宏观上是同时,微观上仍是顺序执行) 。
并发在宏观上是同时执行,但微观上是交替执行,而并行无论是宏观还是微观,都是同时执行.
Tips :打个比方,并行像是打开了两盏灯,它们同时处于亮起的状态;而并发就是一盏灯,肉眼看起来是“常亮”状态,但实际在交流电的作用下,灯一直再闪烁,只是肉眼无法观察到.
参考资料 : 并发(百度百科) , 并行(百度百科) 。
同步 :同步,可以理解为在通信时、函数调用时、协议栈的相邻层协议交互时等场景下,发信方与收信方、主调与被调等双方的状态是否能及时保持状态一致。如果一方完成一个动作后,另一方立即就修改了自己的状态,就是同步.
异步 :是指调用方发出请求就立即返回,请求甚至可能还没到达接收方,比如说放到了某个缓冲区中,等待对方取走或者第三方转交;而调用结果是通过接收方主动推送,或调用方轮询来得到.
参考资料 : 同步(维基百科) 。
阻塞与非阻塞指的是程序在等待调用结果时的状态.
阻塞(Blocking) :被调用时, 线程会被挂起/暂停/阻塞 ,直到该操作完成,返回结果后再执行后续操作。此时,程序无法进行其它操作,会一直等到调用结果返回.
非阻塞(Non-blocking) :被调用时,即便操作尚未完成和拿到结果, 线程也不会被挂起/暂停/阻塞 ,程序可以继续执行后序操作.
进程(process) ,曾经是分时系统的基本运作单位。在面向进程设计的系统中,是程序的基本执行实体;在面向线程设计的系统中, 进程本身不是基本执行单位,而是线程的容器 .
线程(thread) ,在计算机科学中,是将进程划分为两个或多个线程(实例)或子进程,由单处理器(单线程)或多处理器(多线程)或多核处理系统并发执行.
进程与线程之间的差别:
进程 | 线程 |
---|---|
进程拥有自己的内存空间,文件句柄,系统信号和环境变量等 | 所有线程共享进程的资源,包括内存空间,文件句柄,系统信号等 |
进程是独立的执行单元 ,拥有自己的堆栈空间,需要使用进程间通信机制进行数据交换 | 线程是进程内部的执行单元 ,共享进程的地址空间,可以直接访问进程的全局变量和堆空间 |
进程间切换开销较大 。进程间的切换比线程间的切换耗时和开销都大得多,因为进程切换需要保存和恢复更多的状态信息,如内存映像、文件句柄、系统信号等 | 线程间切换开销较小 。线程的切换只需要保存和恢复少量的寄存器和堆栈信息 |
进程间资源隔离明显,进程间安全性较高 | 线程间共享资源,容易引起竞态条件, 线程间安全性较低 |
参考资料 : 进程(维基百科) , 线程(维基百科) 。
原子性 :指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行; 可见性 :软件工程中,是指对象间的可见性,含义是一个对象能够看到或者能够引用另一个对象的能力; 有序性 :有序性是指对于多个线程或进程执行的操作,其执行顺序与程序代码中的顺序保持一致或符合预期的规则.
Tips :未在维基百科和百度百科中查找到有序性的解释,这里采用了 ChatGPT 的解释.
参考资料 : 关于线程你必须知道的8个问题(上) , 原子性(百度百科) , 可见性(百度百科) 。
线程饥饿 (Thread Starvation),指的是在多线程的竞争环境中,某个线程长时间无法获取所需资源,或长时间无法得到调度,导致任务无法完成的状态.
常见产生的线程饥饿的原因如下:
多个线程共用同一个 CPU 时,CPU 时间从一个线程切换到另一个线程的过程。在这个过程中,需要保存线程的上下文信息(如:程序计数器,寄存器状态,堆栈指针等),同时加载另一个线程的上线文信息,使得系统能够正确执行.
Tips :
参考资料 : 上下文切换(维基百科) 。
面试公司 :苏宁,质数金融,网易 。
死锁 (deadlock),当两个以上的运算单元,双方都在等待对方停止执行,以获取系统资源,但是没有一方提前退出时,就称为死锁.
形成死锁需要 4 个条件:
解决死锁问题的核心是打破4项条件其中的一项即可:
参考资料 : 死锁(维基百科) , 关于线程你必须知道的8个问题(下) 。
面试公司 :有利网 。
并发编程领域常见的 2 个线程间通信模型: 共享内存和消息传递 .
共享内存 :指的是多个线程运行在不同核心上,任何核心缓存上的数据修改后,刷新到主内存后,其他核心更新自己的缓存.
消息传递 :多个线程可以通过消息队列进行通信,线程可以将消息发送到队列中,其他线程可以从队列中获取消息并进行处理.
传统面向对象编程语言通常会采用共享内存的方式进行线程间的通信,如 Java,C++等。但 Java 可以通过 Akka 实现 Actor 模型的消息传递。Golang 则是消息传递的忠实拥趸,《 Go Proverbs 》中第一句便是:
Don't communicate by sharing memory, share memory by communicating. 。
参考资料 : 共享内存(维基百科) , 消息传递(维基百科) , 管道(维基百科) , 共享内存(百度百科) , 消息传递(百度百科) 。
面试公司 :苏宁 。
运用多线程的根本原因是“压榨”硬件性能, 提高程序效率 .
但引入多线程也带来了一些挑战:
参考资料: 关于线程你必须知道的8个问题(下) , Java并发编程的艺术(豆瓣) 。
线程安全 :指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的公用变量,使程序功能正确完成.
通俗点可以理解为,程序在多线程环境中与单线程环境中的执行结果一致.
Tips :这与 JMM 中提到的终极目标 as-if-serial 语义稍有差别,as-if-serial 语义强调 无论如何重排序,单线程场景下的语义不能被改变(或者说执行结果不变).
参考资料 : 线程安全(维基百科) , 线程安全(百度百科) , 深入理解JMM和Happens-Before 。
接下来是 Java 应用篇,主要是关于 Java 中线程,Thread 类,Runnable 接口,Callable 接口以及 Future 接口的内容.
早期的 Linux 系统并不支持线程,但可以通过编程语言模拟实现“线程”,但其本质还是进程,这时我们认为 Java 中的线程是用户线程。到了 2003 年,RedHat 初步完成了 NPTL(Native POSIX Thread Library)项目,通过轻量级进程实现了服务号 POSIX 标准的线程,这时 Java 中的线程是内核线程。因此运行在现代服务器上的 Java 程序,使用的 Java 线程都会映射的到一个内核线程上 .
所以我们可以得到这样一个式子: \(Java线程 \approx 操作系统内核线程 \approx 操作系统轻量级进程\) .
那么对于线程的调度方式来说,我们可以得到: \(Java线程的调度方式 \approx 操作系统进程的调度方式\) .
恰好, Linux中使用了抢占式进程调度方式 。因此,并不是JVM中实现了抢占式线程调度方式,而是 Java使用了Linux的进程调度方式,Linux选择了抢占式进程调度方式 .
参考资料 : 关于线程你必须知道的8个问题(下) 。
面试公司 :苏宁 。
Java 中只有一种创建线程的方式 。从 Java 层面来看,可以认为执行Thread thread = new Thread()就创建了线程;而调用Thread#start则是操作系统层面的线程创建于启动.
// 创建Java层面的线程
Thread thread = new Thread();
// 创建系统层面的线程
thread.start();
Tips :通常网上会给出至少 4 种创建线程的方式:
但这是一个错误的结论,实现Runnable接口或是实现Callable接口,其主要目的是为了重写 Runnable#run 方法,以实现业务逻辑,而要真正的创建并启动一个 Java 线程还是要创建 Thread 对象,并调用 Thread# start方法.
Tips :还有的资料中搞出了 6 种创建线程的方式~~ 。
参考资料 : 关于线程你必须知道的8个问题(上) 。
面试公司 :京东,百度 。
Java 中定义了 6 种线程状态:
线程状态定义为 Thread 的内部类 state:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
线程状态的转换请参考下图:
参考资料 : 关于线程你必须知道的8个问题(上) 。
面试公司 :苏宁 。
Object#wait 使线程等待,同时释放锁,线程进入 WAITING 或 TIMED_WAITING 状态 。 Object#wait 有 3 个重载方法:
public final void wait() throws InterruptedException;
public final native void wait(long timeoutMillis) throws InterruptedException;
public final void wait(long timeoutMillis, int nanos) throws InterruptedException;
由于 Object#wait 释放锁,因此需要在同步块(synchronized 块)中调用,因为只有先获得锁,才有的释放.
Tips :面试中常常用来与 Thread#sleep 进行比较.
参考资料 : 关于线程你必须知道的8个问题(中) 。
Object#notify 与 Object#notifyAl l都是用来唤醒线程的。 Object#notify 随机唤醒一个等待中的线程 , Object#notifyAll 唤醒所有等待中的线程。通过 Object#notify与 Object#notifyAll 唤醒的线程并不会立即执行,而是加入了争抢内置锁的队列,只有成功获取到锁的线程才会继续执行.
参考资料 : 关于线程你必须知道的8个问题(中) 。
如果不在循环中检查等待条件,等待状态中的线程可能会被错误的唤醒,此时跳过等待条件的检查可能会造成意想不到的问题。例如:生产者与消费者的场景.
public static void main(String[] args) {
Product product = new Product(0);
new Thread(() -> {
for (int i = 0; i < 3; i++) {
try {
product.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + ",状态:" + Thread.currentThread().getState());
}, "consumer-1").start();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
try {
product.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "consumer-2").start();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
try {
product.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "consumer-3").start();
new Thread(() -> {
for (int i = 0; i < 9; i++) {
try {
product.increment();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
, "producer").start();
}
static class Product {
private int count;
private Product(int count) {
this.count = count;
}
/**
* 生产
*/
private synchronized void increment() throws InterruptedException {
if (this.count > 0) {
this.wait();
}
count++;
System.out.println("[" + Thread.currentThread().getName() + "]生产产品,当前总数:" + this.count);
this.notifyAll();
}
/**
* 消费
*/
private synchronized void decrement() throws InterruptedException {
if (this.count == 0) {
this.wait();
}
count--;
System.out.println("[" + Thread.currentThread().getName() + "]消费产品,当前总数:" + this.count);
this.notifyAll();
}
}
修改方案非常简单, 只需要在循环中检进入等待的条件即可 ,代码修改后如下:
static class Product {
private synchronized void increment() throws InterruptedException {
while (this.count > 0) {
this.wait();
}
count++;
System.out.println("[" + Thread.currentThread().getName() + "]生产产品,当前总数:" + this.count);
this.notifyAll();
}
private synchronized void decrement() throws InterruptedException {
while (this.count == 0) {
this.wait();
}
count--;
System.out.println("[" + Thread.currentThread().getName() + "]消费产品,当前总数:" + this.count);
this.notifyAll();
}
}
参考资料 : 关于线程你必须知道的8个问题(中) 。
Java 提供的内置锁(ObjectMonitor)是对象级别的,即每个对象都有一个内置锁。而 Obejct#wait , Obejct#notify 和 Obejct#notifyAll 涉及到内置锁的操作,这与线程无关,只与对象有关,因此将它们放在所有对象的父类 Object 中.
参考资料 : 关于线程你必须知道的8个问题(中) 。
因为这 3 个方法都涉及到对内置锁的操作.
Object#wait 方法释放锁,而 Object#notify 和 Object#notifyAll 用于通知其它线程当前锁可用,而执行这些操作的奇谭提是持有锁,或知道锁的状态,因此必须在 synchronized 中调用.
参考资料 : 关于线程你必须知道的8个问题(中) 。
Thread#start 创建了操作系统层面的线程,并启动线程调用 Thread#run 。而 Thread#run 只是Runnable接口的实现,并不会创建并启动线程.
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
// 调用JNI方法,创建系统层面线程
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
第 8 行中调用了 JNI 方法private native void start0(),在 JVM 的实现中,该方法创建了操作系统层面的线程.
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run();
}
}
Thread#run 只是对Runnable接口的实现,并调用了成员变量 target 的 run 方法.
参考资料 : 关于线程你必须知道的8个问题(中) 。
面试公司 :苏宁 。
多次调用 Thread#start 方法会抛出IllegalThreadStateException异常.
Thread#start 方法的源码:
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
调用 Thread#start 方法,会先对threadStatus进行判断,只有当 threadStatus == 0 时, Thread#start 才能正常执行,否则抛出IllegalThreadStateException异常.
threadStatus实际上是 Thread 内部类 state 的映射,以下涉及java.lang.Thread类和jdk.internal.misc.VM类的相关代码:
public class Thread implements Runnable {
public State getState() {
return jdk.internal.misc.VM.toThreadState(threadStatus);
}
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
}
public class VM {
private static final int JVMTI_THREAD_STATE_ALIVE = 0x0001; // 1 , 0000 0000 0001
private static final int JVMTI_THREAD_STATE_TERMINATED = 0x0002; // 2 , 0000 0000 0010
private static final int JVMTI_THREAD_STATE_RUNNABLE = 0x0004; // 4 , 0000 0000 0100
private static final int JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER = 0x0400; // 1024, 0100 0000 0000
private static final int JVMTI_THREAD_STATE_WAITING_INDEFINITELY = 0x0010; // 16 , 0000 0001 0000
private static final int JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT = 0x0020; // 32 , 0000 0010 0000
public static Thread.State toThreadState(int threadStatus) {
if ((threadStatus & JVMTI_THREAD_STATE_RUNNABLE) != 0) {
return RUNNABLE;
} else if ((threadStatus & JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER) != 0) {
return BLOCKED;
} else if ((threadStatus & JVMTI_THREAD_STATE_WAITING_INDEFINITELY) != 0) {
return WAITING;
} else if ((threadStatus & JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT) != 0) {
return TIMED_WAITING;
} else if ((threadStatus & JVMTI_THREAD_STATE_TERMINATED) != 0) {
return TERMINATED;
} else if ((threadStatus & JVMTI_THREAD_STATE_ALIVE) == 0) {
return NEW;
} else {
return RUNNABLE;
}
}
}
参考资料 : 关于线程你必须知道的8个问题(中) 。
可以直接调用 Thread#run 方法,和调用普通方法一样,会在调用线程中执行,例如:
public static void main(String[] args) {
Thread t1 = new Thread(()-> System.out.println("直接调用run方法"));
t1.run();
}
上述代码中, Thread#run 方法由主线程执行,并不会启动新线程,因为 Thread#run 只是对接口方法 Runnable#run 的实现:
public class Thread implements Runnable {
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
参考资料 : 关于线程你必须知道的8个问题(中) 。
面试公司 :苏宁 。
Thread#sleep 使线程进入休眠,但不会释放锁(锁指的是 synchronized 中使用的 Java 内置错,即 ObjectMonitor),线程进入 TIMED_WATING 状态。确切的说 , Thread#sleep 在 JVM 的实现中,并不执行锁相关操作的逻辑,所以实际中也谈不上释放不释放.
Thread#sleep 有 2 个重载方法:
public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos) throws InterruptedException;
Object#wait 与 Thread#sleep 的区别:
Object#wait | Thread#sleep | |
---|---|---|
作用 | 线程进入暂停状态(WAITING/TIMED_WAITING) | 线程进入休眠状态(TIMED_WATING) |
CPU 资源 | 释放 | 释放 |
内置锁(ObjectMonitor) | 释放 | 不释放 |
重点关注在 CPU 资源和内置锁的持有与释放上的差别即可.
参考资料 : 关于线程你必须知道的8个问题(中) 。
调用 Thread#sleep(0) 会 真实的让出 CPU 时间 ,从而触发 CPU 时间片的竞争.
jvm.cpp 源码:
JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
HOTSPOT_THREAD_SLEEP_BEGIN(millis);
EventThreadSleep event;
if (millis == 0) {
os::naked_yield();
} else {
ThreadState old_state = thread->osthread()->get_state();
thread->osthread()->set_state(SLEEPING);
if (os::sleep(thread, millis, true) == OS_INTRPT) {
if (!HAS_PENDING_EXCEPTION) {
if (event.should_commit()) {
post_thread_sleep_event(&event, millis);
}
HOTSPOT_THREAD_SLEEP_END(1);
}
}
thread->osthread()->set_state(old_state);
}
HOTSPOT_THREAD_SLEEP_END(0);
JVM_END
第 4 行判断 millis == 0 成功后会执行os::naked_yield(),此时作用与 Thread#yield 相同.
参考资料 : 关于线程你必须知道的8个问题(中) 。
调用 Thread#yield 使当前线程让出 CPU 时间,从而触发 CPU 时间片的竞争 。另外,与 Thread#sleep 一样, Thread#yield 也不会释放锁.
JVM 中的实现:
JVM_ENTRY(void, JVM_Yield(JNIEnv *env, jclass threadClass))
if (os::dont_yield()) {
return;
}
os::naked_yield();
JVM_END
与调用Thread#sleep(0)的实现一样.
Tips : Thread#yield 只是让出了时间片,但又可能会立即抢夺回来,例如:所有线程必须持有锁才能执行,持有锁的线程调用 Thread#yield 让出 CPU 时间片,但并未释放锁,其他线程无法执行,只能持有线程的锁继续获取 CPU 时间片.
参考资料 : 关于线程你必须知道的8个问题(中) 。
Thread#join 等待其他线程运行结束,线程进入 WAITING 或TIMED_WAITING 状态 。假如我们右如下代码:
Thread t1 = new Thread(()- >{
// 业务逻辑
});
Thread t2 = new Thread(()- >{
t1.join();
// 业务逻辑
});
t1.start();
t2.start();
上述代码中,主线程启动线程 t1 和 t2 后,线程 t2 会等待线程 t1 执行结束后再继续执行,即谁执行了线程 t2 执行了t1.join后等待线程 t1 执行完毕后再执行.
参考资料 : 关于线程你必须知道的8个问题(中) 。
Thread#interrupt方法表示中断线程,但 JVM 并不会立即中断线程,仅仅是将线程标记为中断状态,随后尝试唤醒处于 sleep/wait/park 中的线程,真正的中断线程的执行是在操作系统获取到该线程的中断状态标记开始的.
需要注意,当线程调用了 Object#wait , Thread#join 或 Thread#sleep 方法后再调用 Thread#interrupt 方法会抛出异常 InterruptedException。这点在 Java 源码的注释上也有说明:
If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException. 。
Thread#interrupt0 方法在JVM的核心源码位于 thread.cpp 中:
void os::interrupt(Thread* thread) {
OSThread* osthread = thread->osthread();
if (!osthread->interrupted()) {
osthread->set_interrupted(true);
OrderAccess::fence();
ParkEvent * const slp = thread->_SleepEvent ;
if (slp != NULL)
slp->unpark() ;
}
if (thread->is_Java_thread())
((JavaThread*)thread)->parker()->unpark();
ParkEvent * ev = thread->_ParkEvent ;
if (ev != NULL)
ev->unpark() ;
}
参考资料 : 关于线程你必须知道的8个问题(中) , thread.cpp 。
Thread#interrupt
方法 Thread#stop
方法(该方法已被废弃) Runnable 和 Callable 都是线程中用来执行业务逻辑方法的接口.
Runnable 接口:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Callable 接口:
public interface Callable<V> {
V call() throws Exception;
}
Runnable 接口没有返回值,而 Callable 接口是有返回值的,可以借助 Futur 或 FutureTask 获取线程运行的结果,举个例子:
public class ByCallable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main的线程:" + Thread.currentThread().getName());
Callable<String> callable = new MyCallable();
FutureTask <String> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
System.out.println("MyCallable的执行线程:" + futureTask.get());
}
static class MyCallable implements Callable<String> {
@Override
public String call() {
System.out.println("MyCallable的线程:" + Thread.currentThread().getName());
return Thread.currentThread().getName();
}
}
}
这部分是考察线程相关内容的应用,常见的是对线程的等待与唤醒的使用.
创建变量同步状态 state,通过同步状态确认何时打印对应的字母:
代码实现如下:
private static final AtomicInteger STATE = new AtomicInteger(1);
private static void useAtomicSyncState() {
new Thread(() -> {
for (int i = 0; i < 100; ) {
if (STATE.get() % 3 == 1) {
System.out.print("A,");
STATE.compareAndSet(i * 3 + 1, i * 3 + 2);
i++;
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 100; ) {
if (STATE.get() % 3 == 2) {
System.out.print("B,");
STATE.compareAndSet(i * 3 + 2, i * 3 + 3);
i++;
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 100; ) {
if (STATE.get() % 3 == 0) {
System.out.print("C");
System.out.println();
STATE.compareAndSet(i * 3 + 3, i * 3 + 4);
i++;
}
}
}).start();
}
Tips :使用AtomicInteger类型是出于以下两点考虑:
如果要使用 private static int state 类型的同步状态,我们可以引入synchronized,代码如下:
private static int state = 1;
private static final Object OBJ_LOCK = new Object();
private static void useSynchronized() {
new Thread(() -> {
for (int i = 0; i < 100; ) {
synchronized (OBJ_LOCK) {
while (state % 3 == 1) {
System.out.print("A,");
state++;
i++;
}
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 100; ) {
synchronized (OBJ_LOCK) {
while (state % 3 == 2) {
System.out.print("B,");
state++;
i++;
}
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 100; ) {
synchronized (OBJ_LOCK) {
while (state % 3 == 0) {
System.out.print("C");
System.out.println();
state++;
i++;
}
}
}
}).start();
}
Tips :synchronized保证了修饰代码块中内容的可见性和原子性.
以上方法的问题是,每个线程都会执行大量的“空转”,state 不满足进入 while 循环的情况,变量 i 尚未发生变化时,导致 for 循环成为“死循环”。通过引入等待 Object#wait 与唤醒 Object#notifyAll 来减少“空转”的次数,代码如下:
private static int state = 1;
private static final Object OBJ_LOCK = new Object();
private static void useSynchronizedWithNotify() {
new Thread(() -> {
synchronized (OBJ_LOCK) {
for (int i = 0; i < 100; i++) {
while (state % 3 != 1) {
try {
OBJ_LOCK.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.print("A,");
state++;
OBJ_LOCK.notifyAll();
}
}
}).start();
new Thread(() -> {
synchronized (OBJ_LOCK) {
for (int i = 0; i < 100; i++) {
while (state % 3 != 2) {
try {
OBJ_LOCK.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.print("B,");
state++;
OBJ_LOCK.notifyAll();
}
}
}).start();
new Thread(() -> {
synchronized (OBJ_LOCK) {
for (int i = 0; i < 100; i++) {
while (state % 3 != 0) {
try {
OBJ_LOCK.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.print("C");
System.out.println();
state++;
OBJ_LOCK.notifyAll();
}
}
}).start();
}
与方法 3 基本一致,ReentrantLock 代替synchronized, Condition#await 和 Condition#signalAll 代替 Object#wait 与 Object#notify All,代码如下:
private static int state = 1;
private static final ReentrantLock REENTRANT_LOCK = new ReentrantLock();
private static final Condition CONDITION = REENTRANT_LOCK.newCondition();
private static void useReentrantLockWithCondition() {
new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
REENTRANT_LOCK.lock();
while (state % 3 != 1) {
CONDITION.await();
}
System.out.print("A,");
state++;
CONDITION.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
REENTRANT_LOCK.unlock();
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
REENTRANT_LOCK.lock();
while (state % 3 != 1) {
CONDITION.await();
}
System.out.print("B,");
state++;
CONDITION.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
REENTRANT_LOCK.unlock();
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
REENTRANT_LOCK.lock();
while (state % 3 != 1) {
CONDITION.await();
}
System.out.print("C");
System.out.println();
state++;
CONDITION.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
REENTRANT_LOCK.unlock();
}
}
}).start();
}
Tips :除了以上 4 中方法外,还可以通过其它的同步工具实现,如:Semphore,CountDownLatch,CyclicBarrier 等.
参考资料 : 关于线程你必须知道的8个问题(中) , 详解AQS家族的成员:Semaphore , 详解AQS家族的成员:CountDownLatch , AQS家族的“外门弟子”:CyclicBarrier 。
上一题的翻版,我这里只演示 ReentrantLock 的实现方式,代码如下:
static int state = 1;
static ReentrantLock reentrantLock = new ReentrantLock();
static Condition condition = reentrantLock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 25; i++) {
try {
reentrantLock.lock();
if (state % 4 != 1) {
condition.await();
}
System.out.println("Thread-1 :" + state);
state++;
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
reentrantLock.unlock();
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 25; i++) {
try {
reentrantLock.lock();
if (state % 4 != 2) {
condition.await();
}
System.out.println("Thread-2 :" + state);
state++;
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
reentrantLock.unlock();
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 25; i++) {
try {
reentrantLock.lock();
if (state % 4 != 3) {
condition.await();
}
System.out.println("Thread-3 :" + state);
state++;
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
reentrantLock.unlock();
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 25; i++) {
try {
reentrantLock.lock();
if (state % 4 != 0) {
condition.await();
}
System.out.println("Thread-4 :" + state);
state++;
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
reentrantLock.unlock();
}
}
}).start();
}
参考资料: 关于线程你必须知道的8个问题(中) 。
可以使用 Thread#join 方法,在线程中启动另一个线程,同时阻塞当前线程,代码如下:
Thread t1 = new Thread(()-> System.out.println("线程[t1]执行!"));
Thread t2 = new Thread(()-> {
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程[t2]执行!");
});
Thread t3 = new Thread(()-> {
try {
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程[t3]执行!");
});
t3.start();
t2.start();
t1.start();
Thread#join 方法的作用是,阻塞执行线程,等待调用 join 方法的线程执行完毕 。如:在上述代码中,t2 中执行 t1.join() ,那么线程 t2 就需要等待线程 t1 执行完毕后再执行.
参考资料: 关于线程你必须知道的8个问题(中) 。
面试公司 :美团 。
参考“如保证 3 个线程按照执行的顺序执行?”的解答.
参考资料: 关于线程你必须知道的8个问题(中) 。
如果本文对你有帮助的话,还请多多点赞支持。如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核技术的金融摸鱼侠 王有志 ,我们下次再见! 。
最后此篇关于面霸的自我修养:Java线程专题的文章就讲到这里了,如果你想了解更多关于面霸的自我修养:Java线程专题的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
简单跳转专题 个人建议重新练习一遍搭建的过程,如果感觉麻烦你可以直接复制上一个工程,但是需要修改pom.xml中的一点信息 ?
2020 年 04 月 10 日,《中共中央国务院关于构建更加完善的要素市场化配置体制机制的意见》正式公布,将数据确立为五大生产要素(土地、资本、劳动力以及技术)之
我是一名优秀的程序员,十分优秀!