1.Java 内存模型
1.什么是 Java 内存模型?
Java 内存模型简称为 JMM(Java Memory Model),是和多线程相关的一组规范,需要各个 JVM 来遵守实现
2.为什么需要 JMM?
有了 JMM 就可以让程序在 windows 和 Linux 上有一样的执行效果,即屏蔽了底层的差异,实现 Write Once,Run Anywhere !,并且解决了 CPU 多级缓存、处理器优化、指令重排等导致的结果不可预期的问题。
3.什么是指令重排序?
编译器、JVM 或者 CPU 都有可能出于优化等目的,对于实际指令执行的顺序进行调整,这就是重排序
4.为什么要重排序?
提高整体的运行速度
5.重排序的 3 种情况
- 编译器优化
- CPU 重排序
- 内存的“重排序”
6.主内存和工作内存
- CPU 多级缓存示意图
线程间对于共享变量的可见性问题,是由我们刚才讲到的这些 L3 缓存、L2 缓存、L1 缓存,也就是多级缓存引起的:每个核心在获取数据时,都会将数据从内存一层层往上读取,同样,后续对于数据的修改也是先写入到自己的 L1 缓存中,然后等待时机再逐层往下同步,直到最终刷回内存。
- 什么是主内存和工作内存?
主内存和工作内存的关系
JMM 有以下规定:
- 所有的变量都存储在主内存中,同时每个线程拥有自己独立的工作内存,而工作内存中的变量的内容是主内存中该变量的拷贝;
- 线程不能直接读 / 写主内存中的变量,但可以操作自己工作内存中的变量,然后再同步到主内存中,这样,其他线程就可以看到本次修改;
- 主内存是由多个线程所共享的,但线程间不共享各自的工作内存,如果线程间需要通信,则必须借助主内存中转来完成。
7.volatile 和 synchronized 有什么区别?
volatile 是 Java 中的一个关键字,是一种同步机制,它可以保证共享变量的可见性(禁用 CPU 缓存),也可以禁止指令重排序,保障有序性
synchronized 是由 CPU 原语层面支持的锁机制,既复合 happens-before 规则保证了可见性,也保证了操作的原子性
8.并发编程 Bug 的源头
1.线程切换带来的原子性问题-->使用同步锁
2.多核 CPU 带来的缓存可见性问题-->利用好 happen-before 原则
3.编译优化带来的指令重排序问题--->使用 volatile
2.线程
创建线程的 3 种方式
public class HelloThread {
// 1.继承 Thread 类,重写 Run 方法
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("hello extends thread");
}
}
//2.实现 Runnable 接口,重写 Run 方法
static class MyThread01 implements Runnable {
@Override
public void run() {
System.out.println("hello implements runnable");
}
}
//3.使用 lambda 表达式
public static void main(String[] args) {
new MyThread().start();
new Thread(new MyThread01()).start();
//lambda 表达式
new Thread(() -> {
System.out.println("hello lambda");
}).start();
}
}
启动线程的 4 种方式
1.继承 Thread 类
2.实现 Runnable 接口(推荐,主要是因为有利于类的扩展)
3.使用 lambda 表达式
4.使用线程池,让一个线程启动
3.如何正确停止线程?(使用 interrupt)
使用 interrupt 通知线程停止,禁止使用已经被舍弃的 stop()、suspend() 和 resume()
但是 interrupt 仅仅起到通知线程停止的作用,线程可以选择停止,也可以选择不停止
为什么 Java 不提供强制停止线程的能力呢?
Java 希望程序间能够相互通知、相互协作地管理线程,因为如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题,为了避免造成问题就需要给对方一定的时间来整理收尾工作
如何用 interrupt 停止线程
Sleep 期间是否可以感受到中断信号?
- 可以的,并且会抛出 InterruptException 异常,但是需要注意抛出异常的同时,会清除中断的标记位,所以在 catch 块里需要重新打上中断标记位,如上图所示。
- 如果在 sleep 期间,响应了中断,那么当前线程会抛出中断异常,并继续往下执行,在某种程度上也算是一种唤醒
休眠期间响应中断的 2 种最佳处理方式
- 抛出异常,让本方法的调用者继续处理
- 在 catch 块里重新打上中断标记位
为什么 volatile 标记位的停止方法在某些场景下错误的?
正确的场景
public class VolatileInterrupt implements Runnable {
private volatile boolean cancled = false;
@Override
public void run() {
int num = 0;
while (!cancled) {
System.out.println(num++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
VolatileInterrupt runable = new VolatileInterrupt();
Thread task = new Thread(runable);
task.start();
Thread.sleep(5000);
runable.cancled = true;
System.out.println("Main Thread Is Over");
}
}
在生产者消费者模型下失效的场景
原因:生产者在执行 storage.put(num) 时发生阻塞,在它被叫醒之前是没有办法进入下一次循环判断 canceled 的值的,所以在这种情况下用 volatile 是没有办法让生产者停下来的,相反如果用 interrupt 语句来中断,即使生产者处于阻塞状态,仍然能够感受到中断信号,并做响应处理
// 生产者
class Producer implements Runnable {
public volatile boolean canceled = false;
BlockingQueue storage;
public Producer(BlockingQueue storage) {
this.storage = storage;
}
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !canceled) {
if (num % 50 == 0) {
storage.put(num);
System.out.println(num + "50 的倍数,被放到仓库中了。");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者结束运行");
}
}
}
//消费者
class Consumer {
BlockingQueue storage;
public Consumer(BlockingQueue storage) {
this.storage = storage;
}
public boolean needMoreNums() {
if (Math.random() > 0.97) {
return false;
}
return true;
}
}
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue storage = new ArrayBlockingQueue(8);
Producer producer = new Producer(storage);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(500);
Consumer consumer = new Consumer(storage);
while (consumer.needMoreNums()) {
System.out.println(consumer.storage.take() + "被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了。");
//一旦消费不需要更多数据了,我们应该让生产者也停下来,但是实际情况却停不下来
producer.canceled = true;
System.out.println(producer.canceled);
}
}
4.六种线程状态之间的转换(必须要熟练背过!!!)
- New(新创建)
- Runnable(可运行)
- Blocked(被阻塞)
- Waiting(等待)
- Timed Waiting(计时等待)
- Terminated(被终止)
Yield 方法
临时暂停执行,再次回到 CPU 时间竞争队列中,等待 CPU 分配时间片
Synchronized 是可重入的
//在指定对象上加锁
synchronized doSomething1() {
count++;
}
/**
* 锁定当前对象
*
*/
synchronized doSomething2() {
count++;
doSomething1();
}
5.wait/notify/notifyAll 方法的使用注意事项?
- 为什么 wait 方法必须在 synchronized 保护的同步代码中使用?
源代码注释:
英文部分的意思是说,在使用 wait 方法时,必须把 wait 方法写在 synchronized 保护的 while 代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须先持有对象的 monitor 锁
这样设计有什么好处呢?分析如下代码
class BlockingQueue {
Queue<String> buffer = new LinkedList<String>();
public void give(String data) {
buffer.add(data);
notify(); // Since someone may be waiting in take
}
public String take() throws InterruptedException {
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
}
由于这段代码在 CPU 层面并不是原子操作,可能会存在这样的场景:判断完 isEmpty 返回 true,发生线程切换,此时完整执行了 give()方法,因此也执行了 notify()方法,但此时 take 线程还没有执行到 wait()方法,也就是 notify()方法是没有效果的,而此时 take 获得了 CPU 时间片,执行了 wait()方法,那么这种情况下 take 线程会陷入无休止的等待状态,因为他完美的错过了 notify 的唤醒
- 为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?
- 在 Java 中,每个对象都有一个可以上锁的叫做 monitor 的监视器锁,,在对象头中有一个位置来保存锁信息,这个锁是对象级别的,Object 是所有对象的父类,所以把这些方法定义在 Object 类中较为合适
- 一个线程可能会持有多把锁,来实现较为复杂的逻辑,把锁定义在 Thread 类中,不合适
- wait/notify 和 sleep 方法的异同?
相同点
- 它们都可以让线程阻塞。
- 它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。
不同点
- 获取锁:wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
- 释放锁:在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
- 设置时间:sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
- 所属类****:wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。
6.三种实现生产者消费者模型的方法
1.BlockQueue(最简单)
public static void main(String[] args) {
//线程安全的阻塞队列
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
Thread producer = new Thread(new Runnable() {
@Override
public void run() {
while(true){
try {
Thread.sleep(1000);
queue.put(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "Producer");
Thread consumer = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
try {
Thread.sleep(1000);
queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "consumer");
producer.start();
consumer.start();
}
2.Condition
要点:使用可重入锁,unlock 一定要写在 finally 里面,new 两个 condition,注意 while 自旋检查队列长度
public class ConditionTest {
static LinkedList<Integer> queue = new LinkedList<>();
static ReentrantLock lock = new ReentrantLock();
static Condition notEmpty = lock.newCondition();
static Condition notFull = lock.newCondition();
static class Consumer implements Runnable {
@Override
public void run() {
while(true) {
lock.lock();
try {
Thread.sleep(1000);
while (queue.size() == 0) {
notEmpty.await();
}
queue.pollLast();
notFull.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
static class Producer implements Runnable {
@Override
public void run() {
while(true) {
lock.lock();
try {
Thread.sleep(1000);
while (queue.size() == 10) {
notFull.await();
}
queue.offer(10);
notEmpty.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
public static void main(String[] args) {
new Thread(new Producer(), "Producer-1").start();
new Thread(new Producer(), "Producer-2").start();
new Thread(new Consumer(), "Consumer>>>>>1").start();
new Thread(new Consumer(), "Consumer>>>>>2").start();
}
}
3.Wait/notify
要点:一把锁与 Synchronized 搭配使用,注意 While 自旋检查队列长度
public class WaitNotifyTest {
private static final Object lock = new Object();
private static LinkedList<Integer> queue = new LinkedList<>();
static class Producer implements Runnable {
@Override
public void run() {
while(true) {
synchronized (lock) {
while (queue.size() == 10) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.offer(1);
lock.notifyAll();
}
}
}
}
static class Consumer implements Runnable {
@Override
public void run() {
while(true) {
synchronized (lock) {
while (queue.size() == 0) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.pollLast();
lock.notifyAll();
}
}
}
}
public static void main(String[] args) {
new Thread(new Producer(),"Producer-1").start();
new Thread(new Producer(),"Producer-2").start();
new Thread(new Consumer(),"Consumer>>>1").start();
new Thread(new Consumer(),"Consumer>>>2").start();
}
}
7.为什么多线程会带来性能问题?
什么是性能问题?
表现为响应时间慢,吞吐量低,内存占用过高等
为什么多线程会带来性能问题?
1.调度开销(会发生上下文切换,和可能发生缓存失效)
上下文切换:在实际开发中,线程数远远高于 CPU 核数,为了尽量让每一个线程都得到执行,操作系统会按照调度算法给每一个线程分配时间片,让每一个线程都有机会得到执行。进行调度时就会引起上下文切换,上下文切换会挂起当前正在执行的线程并保存当前的状态,然后寻找下一处即将恢复执行的代码,唤醒下一个线程,以此类推,反复执行。但上下文切换带来的开销是比较大的
缓存失效:一旦进行了线程调度,切换到其他线程,CPU 就会去执行不同的代码,原有的缓存就很可能失效了,需要重新缓存新的数据,这也会造成一定的开销,所以线程调度器为了避免频繁地发生上下文切换,通常会给被调度到的线程设置最小的执行时间,也就是只有执行完这段时间之后,才可能进行下一次的调度,由此减少上下文切换的次数
2.协作开销
线程之间如果有共享数据,为了避免数据错乱,为了保证线程安全,就有可能禁止编译器和 CPU 对其进行重排序等优化,也可能出于同步的目的,反复把线程工作内存的数据 flush 到主存中,然后再从主内存 refresh 到其他线程的工作内存中,等等
那么什么情况会导致密集的上下文切换?
- 程序频繁地竞争锁,
- IO 读写等原因导致频繁阻塞
8.synchronized
Synchronized 不能使用的锁对象
String 常量,
Integer
Long 等基础数据类型
Synchronized 优化
细化锁,即减小锁的范围
锁对象发生变化,则锁失效
要避免将锁对象发生变化
锁定方法和非锁定方法是可以同时执行的
Synchronized 有锁升级的概念
偏向锁->自旋锁->重量级锁,因此 Synchronized 的性能在某些场景下性能并不比 Atomicxx 这些类差,反而可能更好