线程1——javaEE 附面题
目录
引入
进程
进程和线程的关系
线程
创建线程
1.通过创建 Thread 子类
2.通过实现 Runnable接口
3.子类匿名表达式
4.匿名内部类(Runnable)
启动线程
Thread 类的常用属性(方法)
线程的状态
线程休眠
打断线程
线程等待 join
线程安全问题
锁synchronized()
先从使用入手
特性
wait/notify
使用
wait使用
两者搭配
面经:
引入
线程的概念离不开cpu(中央处理器),CPU 作为电脑的“脑”,其算力是非常夸张的但它是如何工作的呢,在认识线程前我们先来了解一下 CPU。
计算机祖师爷冯诺依曼提出的冯诺依曼体系结构(运算器,控制器,存储设备,输入设备,输出设备)奠定了现代计算机的硬件基础。运算器,控制器便是CPU最基础,最核心的功能。
cpu 执行是很复杂的 可以简化成
1. 读取指令 ------------------------------------------------- 指令(机器语言 )
2. 解析指令 ------------------------------------------------- 从指令表中对应查找指令是什么意思
3. 执行指令 ------------------------------------------------- 运算
现代 多核 cpu 下诞生了进程
进程
进程是操作系统中资源分配的基本单位。
操作系统是一个描述系统资源,通过一定数据结构组织资源的管理软件。 、
系统通过PCB 来描述进程
PID | 同一台机器,同一时间,不重复 |
内容指针 | 内存资源分为数据/指令 操作系统可通过其找到数据/指令 |
文件描述符表 | 硬盘资源 打开文件可以得到一个文件描述符,打开多个就可以用数组/顺序表表示 |
状态 | 就绪状态 / 阻塞状态 |
优先级 | 依据重要性给进程分配资源 |
上下文 | 进程调度出 cpu 时保存一个中间状态,保证进程再调度回来时可以恢复之前的工作 |
记账信息 | PCB 会记录下进程在CPU上执行的次数,分配写资源给使用资源少的进程 |
进程的创建/销毁开销(销毁时间和系统资源)非常大,在创建时申请资源(大开销操作),为了提高效率降低开销引入了线程。
进程和线程的关系
1.线程是更轻量的进程。(进程太重了大开销,低效率)
2.进程包含线程,一个线程有大于等于一个线程,不能没有。
3.同一个进程上的线程共享进程的资源。
4.每个线程都可以执行独立的逻辑,并在cpu上独立调度
5.当进程已经有了,在进程上创建线程可以省去申请资源的开销。
线程
线程是系统调度执行的基本单位。
线程满足了“并发编程” 使一个服务器可以同时处理多个客户端的访问。
线程虽更轻量,多线程可以提升效率,但过犹不及。
线程过多带来的问题:
1.线程过多时,线程的创建和销毁时的开销就不可忽视了。
2.多线程环境下,多个线程对同一个变量同时进行操作。
多对一 可读不可取
一对一 可读又可取3.线程中断会抛出异常,如果没有被捕获到,进程就会崩溃,线程会全挂掉。
创建线程
1.通过创建 Thread 子类
重写 run(); 方法,创建Thread 对象,调用start();
代码实现:
class myThread extends Thread{public void run (){System.out.println("hello thread");}
}
public class demo1 {public static void main(String[] args) {Thread thread = new myThread();thread.start();System.out.println("hello main");}
}
👀输出:
通过写入无限循环观察下:
代码:
class myThread extends Thread{@Overridepublic void run() {while(true){try {System.out.println("hello thread");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}public class demo1 {public static void main(String[] args) {Thread thread = new myThread();thread.start();while(true){try {System.out.println("hello main");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
👀输出
观察结果可以发现 main 主线程 和 我们自己创建的 thread 线程是并行执行的,顺序由cpu调度决定,随机出现。
2.通过实现 Runnable接口
再将 runnable 实例作为参数传给Thread 构造方法
代码:
class myRunnable implements Runnable{@Overridepublic void run() {System.out.println("hello thread");}
}
public class demo2 {public static void main(String[] args) {Runnable runnable = new myRunnable(); Thread thread = new Thread(runnable);thread.start();System.out.println("hello main");}
}
👀输出:
这样的写法分离了任务逻辑和线程管理,不依赖于具体的类,使得后续可以轻松替换任务实现,降低程序的耦合度。
3.子类匿名表达式
代码: Thread t = new Thread () {
public void run(){
}
};
public class demo3 {public static void main(String[] args) {// 匿名内部类 是Thread的子类 重写了run方法Thread t = new Thread(){public void run(){while(true){try {System.out.println("hello thread");Thread.sleep(1000); // 休眠1000ms 降低打印速度方便观察} catch (InterruptedException e) {e.printStackTrace();}}}};t.start();System.out.println("hello main");}}
👀输出:
4.匿名内部类(Runnable)
代码:
Runnable runnable = new Runnable({
});
Thread t = new Thread(runnable);
public class demo4 {public static void main(String[] args) {Runnable runnable = new Runnable(){@Overridepublic void run() {while(true){try {System.out.println("hello thread");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}; Thread t = new Thread(runnable);t.start();while(true){try {System.out.println("hello main");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
👀输出:
启动线程
Thread.start(); start() 是 Thread 类的一个静态方法,可以直接用类名调用。
1.调用 start 会真正调用系统中创建线程的 api start 执行不会产生阻塞,按代码顺序立刻向下执行。
2.一个线程只能 start 一次。 start 后 线程要么是就绪,要么是阻塞 不能重新 启动 了。
3.start 执行会自动执行 run() 方法。
Thread 类的常用属性(方法)
方法类别 | 方法名 | 功能描述 | 补充说明 |
---|---|---|---|
ID | getId() | 获取线程唯一标识符 | 每个线程都有一个唯一的标识符,由 JVM 分配,从 1 开始递增 |
名称 | getName() | 获取线程名称 | 线程创建时可以指定名称 |
状态 | getState() | 获取线程状态 | 返回线程当前状态(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED) |
优先级 | getPriority() | 获取线程优先级 | 线程优先级设置效果受操作系统调度机制影响,"改了不一定有用", |
守护线程 | isDaemon() | 判断是否为守护线程 | 守护线程会在所有非守护线程结束后自动终止,主要用于后台支持任务 |
存活状态 | isAlive() | 判断线程是否存活 | 返回 false 表示线程未启动(NEW 状态)或已结束(TERMINATED 状态) |
中断状态 | isInterrupted() | 判断线程是否被中断 | 不会清除中断标记,需要通过 Thread.interrupted () 静态方法清除中断标记 |
注:isDaemon() 是否为后台线程
后台线程: 当线程没执行完时,进程要结束,线程无法阻止当前进程结束。
前台线程: 当前线程没执行完,进程要结束要等线程执行完,这样的线程成为前台线程。
线程的状态
New | 创建了线程对象但还没start isAlive false |
TERMINED | 执行完成了(run完了)但对象还在 isAlive false |
WAITING | 死等 join 无参未设置超时时间 |
TIME_WAITING | 有时间的等 join 设置了超时时间 |
BLOCKED | 锁竞争产生的阻塞 |
线程休眠
sleep(时间 ms) Thread 类的静态方法 让线程休眠多少毫秒后(进入Time_WAITING)
sleep 线程进入阻塞,调度出CPU ,唤醒后变为就绪状态,但不会立即执行,等待CPU调度。
打断线程
希望线程提前结束(sleep 时提前唤醒)
1.通过变量修改
2.通过 isInterrupted 标志位
查看当前中断状态:Thread.currentThread().isInterrupted() 不清除中断标志
检查中断状态:Thread.interrupted() 清除中断标志若线程处于休眠sleep,会抛出InterruptedException 异常,需要 catch
try{Thread.sleep(1000); }catch (InterruptedException e) {Thread.currentThread.interrupt(); }
线程等待 join
控制线程之间的执行顺序。
有两个版本的join
1.join(); 死等
2.join(超时时间) 等待到超时时间后就不等了线程 t1 线程t2
t1.join(); t2等t1执行完
t1.join(1000) t2等待t1执行完,等了1000ms t1还没结束就不等了⭐谁调用谁被等
线程安全问题
why:❓❓❓
1. 【根本原因】操作系统的随即调度,抢占式执行。
2. 操作不是原子的。 (原子的:不可再分的最小操作)
3. 多个变量同时操作同一个变量。
4. 内容不可见
4. 指令重排序
面对非原子的操作,多线程就会出现多线程做同一个操作但做的是这个操作的不同部分
怎么理解呢? 就好像把一个大象放进冰箱需要几步。(这就是非原子操作是可拆分的)
1.打开冰箱门
2.把大象放进去
3.关上冰箱门
这时候如果有多个线程同时进行把同一只大象放进冰箱的操作。就有可能线程一打开了冰箱门被调度出CPU,线程2也执行了打开冰箱门,重复开门冰箱们受不了,线程2把大象放进冰箱并关上了冰箱门然后被调度出CPU,线程一被调度回CPU读取了中间状态继续之前的操作,把大象放进去,关上冰箱门。
把一只大象放进去了两遍也就是BUG出现了,有人要问最后不还是大象在冰箱里面吗,但无效的操作消耗了资源,在这虽然没造成什么严重后果但这要是转账操作呢,同时扣了两次款呢?
how:
那怎么保证安全呢?
既然是非原子操作造成的那可以把操作打包成原子的,java中提供了synchronized 可以给操作加锁保证线程一次把该执行完的逻辑执行完,这时有其他线程来执行这个操作就会触发锁竞争,产生阻塞等待上一个执行此操作的线程执行完解锁才能拿到锁,开始执行该操作。
锁synchronized()
先从使用入手
synchronized 有两个大括号
进入第一个大括号表示锁已经加上了,
从第二个大括号出来就表示解锁了。
通过加锁操作可以把操作变成原子的。
原理:加同一把锁的线程(锁对象是相同的)会竞争同一把锁(锁竞争)没抢到的阻塞等待抢到的解锁再抢。
锁对象 Object locker = new Object();
1.锁
synchronized(锁对象){// 操作}
2.修饰普通方法
synchronized public void 方法(){// this是锁对象}
3.修饰静态方法
synchronized public static void 方法(){// static 没有this ,所以锁对象是类对象// 类对象 .class
}
特性
1.互斥
当一个线程已经拥有锁的时候,该线程的锁不能被抢占。
2.可重入
当一个线程已经拥有一把锁的情况下,对于已有的这把锁可以重复加锁多次(连续加同一把锁)且不会触发死锁。
wait/notify
Object类的方法
协调线程之间执行的顺序 区别 join 控制线程间结束顺序
wait 会使线程释放锁主动阻塞等待,直到被notify 唤醒
eg:
希望t1先执行再让t2执行
使用wait主动让t2阻塞让t1先参与调度,等t1执行完用notify唤醒t2.
使用
Object object = new Object ();
object.wait(); // wait() 会阻塞 可能会抛出 InterruptedException 当执行wait时其内部会第一时间把锁放了
放锁的前提得先有一把锁,wait放锁后当前线程就会进入阻塞状态 WAITING ,等待被唤醒,被唤醒后会再重新尝试去获取之前的锁,就会引发锁竞争 BLOCKED (被唤醒了,等拿到锁就会继续执行)
所以wait 应该搭配synchronized使用
wait使用
Object object = new Object();
try{synchronized(object){object.wait();}
}catch(InterruptedExecption e){
两者搭配
创建两个线程,线程t1等待,线程t2来唤醒t1
import java.util.Scanner;public class demo_notify {public static Object locker = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println("t1 等待");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1 等待后");});Thread t2 = new Thread(() -> {System.out.println("请输入任意内容唤醒t1");Scanner scanner = new Scanner(System.in);scanner.next();synchronized (locker) { locker.notify();}});t1.start();t2.start(); }}
👀输出
当多个线程都在 wait 时(同一个对象),此时 notify 会随机唤醒一个。使用 notifyAll 可以唤醒所有的。
import java.util.Scanner;public class demo_notifyAll {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {System.out.println("t1 等待前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1 等待后");});Thread t2 = new Thread(() -> {System.out.println("t2 等待前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t2 等待后");});Thread t3 = new Thread(() -> {System.out.println("请输入任意内容唤醒t1或t2");Scanner scanner = new Scanner(System.in);scanner.next();synchronized (locker) {locker.notify();}});t1.start();t2.start();t3.start();}}
输出:此时t3只会唤醒t1或t2其中一个。我这里运行唤醒了t2
想同时唤醒t1,t2就需要用notifyAll
import java.util.Scanner;public class demo_notifyAll {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {System.out.println("t1 等待前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1 等待后");});Thread t2 = new Thread(() -> {System.out.println("t2 等待前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t2 等待后");});Thread t3 = new Thread(() -> {System.out.println("请输入任意内容唤醒t1或t2");Scanner scanner = new Scanner(System.in);scanner.next();synchronized (locker) {locker.notifyAll();}});t1.start();t2.start();t3.start();}}
输出:
面经:
谈谈sleep 和 wait 的区别。
答:
1.wait 的设计是为了提前唤醒,超时时间只是作为Plan B。
sleep 的设计就是为了到时间唤醒,虽可用 interrupt 提前唤醒但这样的唤醒会产生异常。2.wait 需要搭配锁使用,因为执行时会先释放锁。(避免其他线程一直拿不到锁)
sleep 就不需要搭配锁使用,当sleep 被放到synchronized中时,不会释放锁而是抱着锁睡。
多线程实用但充满陷阱未完待续。
爱是个什么东西,它太理想主义,爱有什么了不起,我充满许多怀疑
爱是个什么东西 DT
⭐❤️👍