JavaEE 面试复习:多线程(初阶)
计算机基础
操作系统中进程是进行资源分配的基本单位
进程唯一的标识 -pid
进程调度-内存管理-进程间通信
PCB进程的核心属性,状态,优先级,上下文,记账信息
进程间通信,进程和进程之间,内存空间是隔离的
进程之间要相互通信,无法通过内存直接完成
多线程
线程是什么
多个线程可以并发执行多份代码
线程的创建,销毁,调度都比进程要快
线程比进程轻量,因此又有了线程池和协程
区别:
进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
进程是系统分配资源的最小单位,线程是系统调度的最小单位。
一个进程挂了一般不会影响到其他进程. 但是一个线程挂了, 可能把同进程内的其他线程一起带走(整个进程崩溃).
多线程程序(线程启动,start,run)
java
//继承Thread类
class MyThread extends Thread{
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
MyThread t = new MyThread();//创建MyThread实例
t.start();
}
//实现Runnable接口
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
Thread t = new Thread(new MyRunnable());
t.start();
}
这两个一个是继承,一个是接口实现
继承Thread类,直接使用this就表示当前线程对象的引用 this.
实现Runnable接口,this表示的是MyRunnable的引用,需要使用Thread.currentThread()

变形
java
//匿名类写法
Thread t1 = new Thread() { }
//匿名内部类创建Runnable子类对象
Thread t2 = new Thread(new Runnable() { })
//lambda创建Runnable子类对象
Thread t3 = new Thread(()->{ });

多线程优势
代码效率速度快,两个线程做事情,肯定比原来效率高
Thread常见属性

后台线程: JVM会在一个进程的所有非后台线程结束后,才会结束运行
start、中断、join等待、获取、休眠、当前线程
线程启动必须调用start
中断线程 thread.interrupt(),调用这个方法其实就设置了一个标志位,通过改变标志位来中断线程
中断线程,法1,建立一个标志位,比如 public volatile boolean isQuit = false;使用Thread.interrupted() 或者Thread.currentThread().isInterrupted() 代替自定义标志位
thread.join
Thread thread = Thread.currentThread(); 获取当前线程
Thread.sleep(time) 休眠当前线程
sleep(0)虽然不休眠,但也放弃了cpu的执行权




前台线程与后台线程的区别



Thread类中run和start的区别
start会创建并启动新线程,在新线程中调用run方法
直接调用run只是普通的方法调用,不会创建新线程,仍在当前线程执行
锁可实现原子性
线程的状态
NEW: 安排了工作, 还未开始行动 Thread对象创建,但未start
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.** 就绪了,在CPU执行中或随时可以调度到CPU执行**
BLOCKED: 这几个都表示排队等着其他事情
WAITING: 这几个都表示排队等着其他事情
TIMED_WAITING: 这几个都表示排队等着其他事情
TERMINATED: 工作完成了.Thread对象还在,但是系统中的线程已经销毁
线程安全与不安全
线程安不安全得看输出代码是否符合我们的预期
不安全:线程调度是随机的
修改共享数据
原子性,线程操作时,被另一个线程突然插入
线程在读取变量的时候,读的顺序
指令重排序不按照我们想要的顺序来,造成线程安全问题
可见性是指一个线程对共享变量值的修改,能够及时地被其他线程看到
synchronized
进入synchronized修饰的代码就加锁,退出synchronized就解锁,多个线程竞争同一把锁,一个线程不释放锁,其余线程就会阻塞等待,可能造成死锁
C++的死锁,必须要释放,这是不可重入锁
java的synchronized是可重入锁,不需要担心
两个及以上线程竞争同一把锁,才会出现阻塞等待
java
//修饰代码块
public void method() {
synchronized (locker) {
}
synchronized(this) {} //锁当前对象
//修饰普通方法
public synchronized void method() {
}
//修饰静态方法
public synchronized static void method() {
}



锁在保证了安全性的同时,也大大降低了效率,尤其是锁竞争发生线程阻塞等待的时候
volatile
这个关键字其实就是为了防止编译器对内存进行优化,解决内存可见性问题
volatile 不保证原子性
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.



面试题:volatile

wait和notify方法
wait让线程等待,释放当前的锁,需要搭配synchronized使用
notify唤醒线程等待的方法
wait结束等待的条件:
其他线程调用该对象的 notify 方法.
wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
其他线程调用该等待线程的 interrupt 方法, 导致 wait 抛出 InterruptedException 异常.
notify唤醒线程,唤醒后需要重新获取锁
notifyAll,唤醒所有线程
wait和sleep的对比
1.wait需要搭配synchronized使用,sleep不需要
2.wait是Object的方法,sleep是Thread的静态方法
3.wait,notify,notifyAll都是Object类的方法
wait会释放锁,sleep不涉及锁


单例模式
单例模式保证某个类在程序中只存在唯一一份实例,不会创建多个实例
饿汉模式
饿汉模式当类创建时,实例也创建好了
懒汉模式
懒汉模式不会在类创建的时候创建实例,这就会出现实例未创建,或创建多个实例的情况。
判断有没有实例,没有实例创建实例,但这会带来线程安全问题,需要加锁
static修饰方法和属性,此时这样的方法是类方法/类属性,不带static称为实例方法/实例属性
饿汉模式提前把资源加载好懒汉模式,边使用边加载资源
阻塞队列
当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
阻塞队列的一个典型应用场景就是 "生产者消费者模型". 这是一种非常典型的开发模型.
1. 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力. (削峰填谷)
2. 阻塞队列也能使生产者和消费者之间 解耦.
通过 "循环队列" 的方式来实现.
使用 synchronized 进行加锁控制.
put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一定队列就不满了, 因为同时可能是唤醒了多个线程).
take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
阻塞队列削峰填谷
阻塞队列让生产者和消费者解耦



定时器


线程池
线程池最大的好处就是减少每次启动、销毁线程的损耗。


线程与进程对比















