Java多线程面试高配问题---线程基础知识(1)🧵
Java多线程
线程基础知识
1. 进程与线程
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。
进程就是用来加载指令、管理内存、管理IO的。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
一个进程之内可以包含一个或者多个线程。
二者对比
- 进程是正在运行程序的实例(比如浏览器、txt文档、ppt等),进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间;(比如打开txt和idea是占用不同的内存空间);
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程);
2. 并发与并行的区别
单核CPU
- 单个CPU线程实际上还是串行执行的
- 操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为15毫秒)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。
- 微观是串行,宏观并行
- 一般会将这种线程轮流使用CPU的做法称为并发(concurrent)
多核CPU
每个核(core)都可以调度运行线程,这时候线程可以是并行的。
总结(现在都是多核CPU,在多核CPU下)
- 并发(concurrent)是同一时间应对(dealing with)多件事情的能力,多个线程轮流使用一个或多个
- 并行(parallel)是同一时间动手做(doing)多件事情的能力(4核CPU同时执行4个线程)
- 举例:
3. 创建线程的方式
共有四种方式可以创建线程,分别是:
- 继承Thread类
- 实现runnable接口
- 实现Callable接口
- 线程池创建线程
继承Thread类
public class MyThread extends Thread{
@Override
public void run(){
System.out.println("MyThread run ...");
}
public static void main(String[] args) {
// 创建MyThread对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
// 调用start方法启动线程
t1.start();
t2.start();
}
}
实现runnable接口
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("MyThread run ...");
}
public static void main(String[] args) {
// 创建MyRunnable对象
MyRunnable mr = new MyRunnable();
// 创建Thread对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
// 调用start方法启动线程
t1.start();
t2.start();
}
}
实现Callable接口
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName());
return "ok";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建MyCallable对象
MyCallable mc = new MyCallable();
// 创建FutureTask
FutureTask<String> ft = new FutureTask<String>(mc);
// 创建Thread对象
Thread t1 = new Thread(ft);
Thread t2 = new Thread(ft);
// 调用start方法启动线程
t1.start();
// 调用ft的get方法获取执行结果
String res = ft.get();
// 输出
System.out.println(res);
}
}
线程池创建线程
public class MyExecutors implements Runnable{
@Override
public void run() {
System.out.println("MyThread run ...");
}
public static void main(String[] args) {
// 创建固定大小的线程池对象
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 提交任务,执行run
threadPool.submit(new MyExecutors());
// 关闭线程池
threadPool.shutdown();
}
}
runable和callable的区别
- Runnable接口run方法没有返回值
- Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
- Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
run()和start()有什么区别
- start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
- run():封装了要被线程执行的代码,可以被调用多次。
总结
创建线程的方式有哪些?
- 继承Thread类
- 实现runnable接口
- 实现callable接口
- 线程池创建线程(项目中使用方式)
runnable和callable有什么区别? - Runnable接口run方法没有返回值
- Callable接口call方法有返回值,需要FutureTask获取结果
- Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
run()和start()有什么区别? - start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
- run():封装了要被线程执行的代码,可以被调用多次。
4. 线程包含哪些状态,状态直接如何变化的?
线程的状态可以参考JDK中的Thread类中的枚举State
- NEW 尚未启动的线程的线程状态
- RUNNABLE 可运行的线程的线程状态
- BLOCKED 线程阻塞等待监视器锁的线程状态
- WAITING 等待线程的线程状态
- TIMED_WAITING 具有指定等待时间的线程状态
- TERMINATED 已经终止的线程的线程状态(线程完成了执行)
如下图所示,枚举类State
状态的变化过程
总结
线程包括哪些状态?
- 新建(NEW)
- 可运行(RUNNABLE)
- 阻塞(BLOCKED)
- 等待( WAITING )
- 时间等待(TIMED_WALTING)
- 终止(TERMINATED)
线程状态之间是如何变化的?
a. 创建线程对象是新建状态
b. 调用了start()方法转变为可执行状态
c. 线程获取到了CPU的执行权,执行结束是终止状态
d. 在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态
5. 新建T1,T2,T3三个线程,如何保证它们按照指定顺序执行?
join():等待线程运行结束
阻塞调用此方法的线程进入timed_waiting直到线程t执行完成后,此线程再继续执行
public class ThreadDemo01 {
public static void main(String[] args) {
// 创建线程:Lambda表达式定义了线程的执行内容
Thread t1 = new Thread(()->{
System.out.println("t1");
});
Thread t2 = new Thread(()->{
try {
t1.join(); // 加入线程t1,只有t1线程执行完毕之后,再次执行该线程
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t2");
});
Thread t3 = new Thread(()->{
try {
t2.join(); // 加入线程t1,只有t1线程执行完毕之后,再次执行该线程
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t3");
});
// 启动线程
t3.start();
t2.start();
t1.start();
}
}
测试不加join的结果
6. notify()和notifyAll()的区别?
- notifyAll:唤醒所有wait的线程
- notify:只随机唤醒一个wait线程
演示Java中多线程编程中的等待和唤醒机制,代码如下:
public class NotifyThread {
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 创建t1线程对象,并通过构造函数的参数传递了一个Runnable对象(Lambda表达式),还给线程指定了一个名称 "t1"
Thread t1 = new Thread(() -> {
synchronized (lock) { // 加锁同步块,使用 lock 对象作为锁。只有一个线程可以进入这个同步块,其他线程必须等待直到锁被释放。
System.out.println(Thread.currentThread().getName() + "...线程等待...");
try {
lock.wait(); // 等待操作,它让当前线程进入等待状态,并释放 lock 锁。线程将一直等待,直到另一个线程调用相同 lock 对象的 notify() 或 notifyAll() 方法,唤醒它。
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "...线程被唤醒了...");
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "...线程等待...");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "...线程被唤醒了...");
}
}, "t2");
t1.start();
t2.start();
Thread.sleep(2000);
synchronized (lock) {
lock.notify();
}
}
}
notify() 运行结果:
notifyAll() 运行结果:
7. java中wait方法和sleep方法有什么不同
共同点:
- wait()/wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权进入阻塞状态
- 它们都可以被interrupted方法中断。
不同点: - 方法归属不同
sleep(long)是 Thread的静态方法;
而wait(), wait(long)都是Object的成员方法,每个对象都有; - 醒来时机不同
执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来;
wait(long)和 wait()还可以被notify唤醒,wait()如果不唤醒就一直等下去;
它们都可以被打断唤醒; - 锁特性不同(重点)
wait方法 的调用必须先获取 wait对象的锁,使用 wait 方法则必须放在 synchronized 块里面,同样需要捕获 InterruptedException 异常,并且需要获取对象的锁。而sleep 则无此限制;
wait方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,但你们还可以用);
而sleep如果在synchronized 代码块中执行,并不会释放对象锁(我放弃cpu,你们也用不了);
代码演示
wait方法 的调用必须先获取 wait对象的锁,配合synchronized使用,运行情况如下:
如果去掉了synchronized就会报错,运行如下:
8. 如何停止一个正在运行的线程
使用interrupt方法中断线程 示例
/**
* @Name TwoPhaseTermination
* @Author xiaoLi
* @Date 2023/8/25 星期五 17:12
* 两阶段终止模式
* 要注意 isInterrupted()方法不会清除打断标记
* 还有一个方法 interrupted() static的 会清除打断标记
*/
@Slf4j
public class interruptTest2 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();
Thread.sleep(3500);
tpt.stop();
}
}
@Slf4j
class TwoPhaseTermination {
private Thread monitor;
public void start() {
monitor = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
// 如果这个标记为真,代表这个程序需要自己体面了
if (current.isInterrupted()) {
log.debug("自己体面~");
break;
}
try {
Thread.sleep(1000); // 情况1 在sleep时被打断
log.info("执行监控任务中..."); // 情况2 执行任务时被打断
} catch (InterruptedException e) {
// 此处如果不重置,程序还会继续运行下去 重新给标记置为true
current.interrupt();
e.printStackTrace();
}
}
});
monitor.start();
}
public void stop() {
if (monitor != null) {
monitor.interrupt();
}
}
}
————————————————
原文链接:https://blog.csdn.net/qq_48592827/article/details/132870125
本文来自博客园,作者:xiaolifc,转载请注明原文链接:https://www.cnblogs.com/xiaolibiji/p/18068646
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)