1.计算机结构
![QQ截图20220109114051](https://img-blog.csdnimg.cn/img_convert/d860ad5948d4bd291ab58c399b4c00d5.webp?x-oss-process=image/format,png)
输入设备:就是我们的鼠标,键盘
存储器:对应的就是我们的内存,缓存
运算器和控制器共同组成了cpu
而输出设备就比如显示屏,打印机。
我们重点来聊一下缓存:
2.缓存
其实,当我们说计算机运行效率低下,速度慢,往往不是cpu的锅。而问题所在一般都是内存访问速度太慢。
CPU的运算速度和内存的访问速度相差比较大。这就导致CPU每次操作内存都要耗费很多等待时间。内存的读写速度成为了计算机运行的瓶颈。
于是就有了在CPU和主内存之间增加缓存的设计。最靠近CPU的缓存称为L1,然后依次是L2,L3和主内存。
CPU缓存模型如图下图所示。
![QQ截图20220109114428](https://img-blog.csdnimg.cn/img_convert/85c1c8f36c06dfb4f42db359c3b35c21.webp?x-oss-process=image/format,png)
运行速度: L1cache >L2cache >L3cache >内存
所以,系统会先访问L1缓存>L2缓存>L3缓存>内存
具体的速度如下:
![QQ截图20220109114528](https://img-blog.csdnimg.cn/img_convert/e4612a747d345a74412873d4298bde9b.webp?x-oss-process=image/format,png)
3.java内存模型概念
Java内存模型,是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
具体如下:
JMM体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
图解如下:
![QQ截图20220109114829](https://img-blog.csdnimg.cn/img_convert/d472ce8c3072fa9a1cb2da9c639163e8.webp?x-oss-process=image/format,png)
Java内存模型如上面所示:规定了工作内存和主内存的概念及其交互。
对共享数据的可见性、有序性、和原子性的规则和保障。
![QQ截图20220109114927](https://img-blog.csdnimg.cn/img_convert/5597b964171bab7cb1f8bfe9647a19c1.webp?x-oss-process=image/format,png)
工作内存和主内存可能在很多地方(cpu寄存器 缓存 或者主内存)
4.主内存和工作内存的交互
Java内存模型中定义了以下8种操作来完成,主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
具体操作如下图:
![QQ截图20220109115543](https://img-blog.csdnimg.cn/img_convert/f1ab85d086818c50c48f22a3746be3b9.webp?x-oss-process=image/format,png)
5.对主内存操作的三大问题
原子性问题描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n12CMVk2-1645791850665)(https://cdn.jsdelivr.net/gh/EngageRing/images01@master/codeImages/QQ截图20220220171059.51apaq2e26k0.webp)]
解决原子性问题
上锁!
![QQ截图20220220171237](https://img-blog.csdnimg.cn/img_convert/bfed17622eadae0f8ae752480f582410.webp?x-oss-process=image/format,png)
可见性问题描述
可见性问题指的是,因为JMM内存模型规范,线程访问主内存的数据后会将数据复制到一个自己的共享内存里,若此时将主内存里的数据更改,就会引起数据不一致的可见性问题。
例子:
static Boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (run) {
//如果run为真,则一直执行
}
}).start();
Thread.sleep(1000);
System.out.println("改变run的值为false");
run = false;
}
![QQ截图20220220171525](https://img-blog.csdnimg.cn/img_convert/080d4184b229c39873e2ea5dea7bc27c.webp?x-oss-process=image/format,png)
可见性问题的解决方法
用volatile关键字
static volatile boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){ // ....
} });
t.start();
Thread.sleep(1000);
run = false; // 线程t不会如预想的停下来
}
![QQ截图20220220171701](https://img-blog.csdnimg.cn/img_convert/7491371f42fb9b6c7c290211544c6a0f.webp?x-oss-process=image/format,png)
指的注意的是synchronized也可以保证代码内变量的可见性
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){ // ....
System.out.printIn();
} });
t.start();
Thread.sleep(1000);
run = false; // 线程t不会如预想的停下来
}
因为输出的底层:
public void println(String x) {
//使用了synchronized关键字
synchronized (this) {
print(x);
newLine();
}
}
代码也会停下来
但性能更低。。。
两阶段终止模式:用可见性关键字实现
// 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性
// 我们的例子中,即主线程把它修改为 true 对 t1 线程可见
class TPTVolatile {
private Thread thread;
private volatile boolean stop = false;
public void start(){
thread = new Thread(() -> {
while(true) {
Thread current = Thread.currentThread();
if(stop) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("将结果保存");
} catch (InterruptedException e) {
}
// 执行监控操作
}
},"监控线程");
thread.start();
}
public void stop() {
stop = true;
thread.interrupt();
}
}
//使用
TPTVolatile t = new TPTVolatile();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();
11:54:52.003 c.TPTVolatile [监控线程] - 将结果保存
11:54:53.006 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.007 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.502 c.TestTwoPhaseTermination [main] - stop
11:54:54.502 c.TPTVolatile [监控线程] - 料理后事
同步模式之犹豫模式
定义
Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做 了,直接结束返回,本质上是一种懒惰的思想,就是用到的时候创建,然后重复创建。
- 用一个标记来判断该任务是否已经被执行过了
- 需要避免线程安全问题
package com.nyima.day1;
/**
* @author Chen Panwen
* @data 2020/3/26 16:11
*/
public class Test7 {
public static void main(String[] args) throws InterruptedException {
Monitor monitor = new Monitor();
monitor.start();
monitor.start();
Thread.sleep(3500);
monitor.stop();
}
}
class Monitor {
Thread monitor;
//设置标记,用于判断是否被终止了
private volatile boolean stop = false;
//设置标记,用于判断是否已经启动过了
private boolean starting = false;
/**
* 启动监控器线程
*/
public void start() {
//上锁,避免多线程运行时出现线程安全问题
synchronized (this) {
//重点在这行代码,已经启动过,第二次就会进行判断,启动过了,下次就
if (starting) {
//已被启动,直接返回
return;
}
//启动监视器,改变标记
starting = true;
}
//设置线控器线程,用于监控线程状态
monitor = new Thread() {
@Override
public void run() {
//开始不停的监控
while (true) {
if(stop) {
System.out.println("处理后续任务");
break;
}
System.out.println("监控器运行中...");
try {
//线程休眠
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("被打断了");
}
}
}
};
monitor.start();
}
/**
* 用于停止监控器线程
*/
public void stop() {
//打断线程
monitor.interrupt();
stop = true;
}
}
有序性问题描述
我们来看如下代码:
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
}else {
r.r1 = 1;
} }
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
它有集中结果呢?
- 情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
- 情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
- 情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)
这些都是基于我们上面学的分析出来的结果,但实际上结果还有一个 0
线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2
出现这种结果,说明发生了指令重排,引起了有序性问题。
那既然jvm的指令重排会引起有序性问题,那么为什么还要使用指令重排呢?
因为提高了指令地吞吐率
使用指令重排的原因
- 事实上,现代处理器会设计为一个时钟周期完成一条执行时间长的 CPU 指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这5 个阶段
![QQ截图20220225195902](https://img-blog.csdnimg.cn/img_convert/2c3cf6400a1f9f350ec6a9041bd739a1.webp?x-oss-process=image/format,png)
- 在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行
- 指令重排的前提是,重排指令不能影响结果,例如
// 可以重排的例子
int a = 10;
int b = 20;
System.out.println( a + b );
// 不能重排的例子
int a = 10;
int b = a - 5;
现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线。
这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一 条执行时间长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率 (cpu同一时间段不能执行相同阶段的指令,所以同时使用不同代码的不同阶段,吞吐量最高)
![QQ截图20220225200201](https://img-blog.csdnimg.cn/img_convert/de40f1238843d1a7f26182b221f95fda.webp?x-oss-process=image/format,png)
在多线程环境下,指令重排序可能导致出现意料之外的结果
有序性问题解决办法
![QQ截图20220220172545](https://img-blog.csdnimg.cn/img_convert/80cfb73868d2330bc34e6ed8e5e43eb2.webp?x-oss-process=image/format,png)
那volatile是怎么解决有序性和可见性的问题的呢?
volatile 原理
volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
而有了屏障之后,可见性和有序性就有了很好的解决。
- 可见性
-
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
-
读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中新数据
- 有序性
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
![QQ截图20220225201352](https://img-blog.csdnimg.cn/img_convert/06e138a85a1b0a072987ae8109a45c46.webp?x-oss-process=image/format,png)
![QQ截图20220225201402](https://img-blog.csdnimg.cn/img_convert/f2fa11409c627e2b1f3f760e095f7d51.webp?x-oss-process=image/format,png)
![QQ截图20220225201409](https://img-blog.csdnimg.cn/img_convert/d8747dad44a71fd09ae3b747eee1bbe2.webp?x-oss-process=image/format,png)
![QQ截图20220225201422](https://img-blog.csdnimg.cn/img_convert/94053395c8cc84c88b1b3389d8eeb70c.webp?x-oss-process=image/format,png)
volatile实际上使用的时机很少,一般为了处理一个写多个读,或者后面所说的double-checked问题
有序性的理解
![QQ截图20220220172653](https://img-blog.csdnimg.cn/img_convert/a338a77a932fdaa2b6a1bc232695129b.webp?x-oss-process=image/format,png)
有序性引起的double-check问题
![QQ截图20220220172828](https://img-blog.csdnimg.cn/img_convert/ab9b293e5e7d8ccfd67ab1ffa31c4b0a.webp?x-oss-process=image/format,png)
理解下就是:
![QQ截图20220220145712](https://img-blog.csdnimg.cn/img_convert/0c25f649d68ccbeb6cd4a803293721ff.webp?x-oss-process=image/format,png)
这是优点:double-checke机制可以减少重复创建对象的现象,提升效率。
但是忽略了有序性问题
我们创建对象时的字节码如下:
0: new #2 // class cn/itcast/jvm/t4/Singleton 分配空间
3: dup //将引用地址放进操作数栈
4: invokespecial #3 // Method "<init>":()V 将对象完善
7: putstatic #4 // Field INSTANCE:Lcn/itcast/jvm/t4/Singleton; 将完善后的对象放进局部变量表
此时容易发生 4 ,7之间的指令重排,使得对象没完善之前就赋值给了INSTANCE对象,如果对象本来就很复杂,后面的线程就容易得到不完善的对象。
解决办法:
public final class Singleton {
private Singleton() { }
private volatile static Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) {
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) {
INSTANCE = new Singleton();
} } }
return INSTANCE;
} }
要注意:sychronized一定情况下也可以保证有序性的
![QQ截图20220225155251](https://img-blog.csdnimg.cn/img_convert/48a3f2250d2282f0df19fe7c7d06fe56.webp?x-oss-process=image/format,png)
happens-before
![QQ截图20220220173643](https://img-blog.csdnimg.cn/img_convert/7737bc0dab5a8e1670d94c187d462bd5.webp?x-oss-process=image/format,png)
线程单例问题
单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,
单例模式又分为饿汉式和懒汉式
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
我们来分别分析以下的几种情况
// 问题1:为什么加 final
//:以防止子类继承来随意篡改父类方法
// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例
//:创建返回值是Object的readResolve方法,返回我们创建的对象
public final class Singleton implements Serializable {
// 问题3:为什么设置为私有? 是否能防止反射创建新的实例?
//不能防止,私有化是为了满足单例的需要
private Singleton() {}
// 问题4:这样初始化是否能保证单例对象创建时的线程安全?
//可以 final修饰的变量在类加载时会创建,安全性由jvm管理
private static final Singleton INSTANCE = new Singleton();
// 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由
//是一种懒惰的思想,类的静态方法使用的时候才会初始化这个类,将它创建成静态方法,是为了使用时再创建,不使用不创建
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
// 问题1:枚举单例是如何限制实例个数的
//该实例时枚举类里的一个私有化变量,不能再被创建
// 问题2:枚举单例在创建时是否有并发问题
//该单例由final修饰,安全由jvm管理
// 问题3:枚举单例能否被反射破坏单例
//不能,枚举进行了保护
// 问题4:枚举单例能否被反序列化破坏单例
//枚举类对此进行了对应处理
// 问题5:枚举单例属于懒汉式还是饿汉式
//饿汉式
// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
enum Singleton {
INSTANCE;
}
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
// 分析这里的线程安全, 并说明有什么缺点
//线程是安全的,每一次都要使用sychronized锁,效率低下,同时可能因为代码的重新排序,
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
public final class Singleton {
private Singleton() { }
// 问题1:解释为什么要加 volatile ?
//防止创建对象时的重排序问题
private static volatile Singleton INSTANCE = null;
// 问题2:对比实现3, 说出这样做的意义
//创建了对象后就不用再次进入sychronized代码块,加大了效率
public static Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (Singleton.class) {
// 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
//防止首次添加时多个线程进入,多次创建对象
if (INSTANCE != null) { // t2
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}
public final class Singleton {
private Singleton() { }
// 问题1:属于懒汉式还是饿汉式
//懒汉式
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 问题2:在创建时是否有并发问题
//因为是final修饰,所以安全性由jvm管理
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}