学习路线指引(点击解锁) | 知识定位 | 人群定位 |
---|---|---|
Python实战微信订餐小程序 | 进阶级 | 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。 |
Python量化交易实战 | 入门级 | 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统 |
相当于提前加载,猜测你可能会用到硬盘相邻存储地址的数据,它会提前进行加载到缓存中,后面你需要时,CPU就不需要去硬盘读取数据,直接读取缓存中的数据传输到内存中就OK了,由于读取缓存的速度远远高于读取硬盘时磁头读写的速度,所以能够明显的改善性能。
2. 对写入动作进行缓存
硬盘接到写入数据的指令之后,并不会马上将数据写入到盘片上,而是先暂时存储在缓存里,然后发送一个“数据已写入”的信号给系统,这时系统就会认为数据已经写入,并继续执行下面的工作,而硬盘则在空闲(不进行读取或写入的时候)时再将缓存中的数据写入到盘片上。
3. 换到应用程序层面也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据同步到主存当中。
举个简单的例子,比如下面的这段代码:
| | i = i + 1; |
比如有两个线程像下列执行顺序:
i = i + 1
,线程二执行var = i
i
,线程一只是在高速缓存中更新了变量,还未将变量i
写会主存i
不是最新值,此时多线程导致数据不一致 类似上面这种情况即为缓存一致性问题。读写场景、双写场景都会存在缓存一致性问题,但读读不会。前提是需要在多线程运行的环境下,并且需要多线程去访问同一个共享变量。
这里的共享又可以回到上文中,即为上面所说,他们每个线程都有自己的高速缓存区,但是都是从同一个主存同步获取变量。
那么这种问题应该怎样解决呢?
i = i + 1
整个命令过程中,其他线程是无法访问主存的。 问题:线程为什么会不安全?
答:共享资源不能及时同步更新,归根于 分时系统 上下文切换时 指令还未执行完毕 (没有写回结果) 更新异常
众所周知现在的互联网大型项目,都是采用分布式架构同时具有其**“三高症状”,高并发、高可用、高性能。高并发为其中最重要的特性之一,在高并发场景下并发编程就显得尤为重要,其并发编程的特性为原子性、可见性、有序性**。
原子性指的是一个或多个操作要么全部执行成功要么全部执行失败,期间不能被中断,也不存在上下文切换,线程切换会带来原子性的问题。
变量赋值问题:
| | int b = 10; |
可见性指的是当前线程对共享变量的修改对其他线程来说是可见的。以下案例中假设不会出现多线程原子性问题(比如多个线程写入覆盖问题等),即保证一次变量操作底层执行指令为原子性的。
例如上述变量在读写场景下,不能保证其可见性,导致写线程完成修改指令时但为同步到主存中,读线程并不能获得最新值。这就是对于B线程来说没有满足可见性。
案例解析:final关键字
| | final a = 10; int b = 10; |
final修饰的变量在其指令后自动加入了写屏障,可以保证其变量的可见性
a 可以保证其他线程获取的值唯一;b 不能保证其他线程获取到的值一定是 10,有可能为 0。
读取 final 变量解析 :
final 可以加强线程安全,而且符合面向对象编程开闭原则中的close,例如子类不可继承、方法不可重写、初始化后不可改变、非法访问(如修饰参数时,该参数为只读模式)等
有序性指的是程序执行的顺序按照代码的先后顺序执行。
在Java中有序性问题会时常出现,由于我们的JVM在底层会对代码指令的执行顺序进行优化(提升执行速度且保证结果),这只能保证单线程下安全,不能保证多线程环境线程安全,会导致指令重排发生有序性问题。
案例:排名世界第一的代码被玩坏了的单例模式
DCL(double checked):加入 volatile 保证线程安全,其实就是保证有序性。
上代码:其中包括了三个问题并且有详细注释解释。(鸣谢itheima满一航老师)
| | final class SingletonLazyVolatile { |
| | private SingletonLazyVolatile() { } |
| | // 问题1:为什么加入 volatile 关键字? |
| | // 答: 防止指令重排序 造成返回对象不完整。 如 TODO |
| | private static volatile SingletonLazyVolatile INSTANCE = null; |
| | // 问题2:对比实现3(给静态代码块加synchronized) 说出这样做的意义? |
| | // 答:没有锁进行判断、效率较高 |
| | public static SingletonLazyVolatile getInstance() { |
| | if (INSTANCE != null) { |
| | return INSTANCE; |
| | } |
| | // 问题3:为什么要在这里加空判断,之前不是判断过了吗? |
| | // 答:假入t1 先进入判断空成立,先拿到锁, 然后到实例化对象这一步(未执行) |
| | // 同时 线程 t2 获取锁进入阻塞状态,若 t1 完成创建对象后,t2 没有在同步块这进行判空,t2 会再新创建一个对象, |
| | // 导致 t1 的对象被覆盖 造成线程不安全。 |
| | synchronized (SingletonLazyVolatile.class) { // t1 |
| | if (INSTANCE != null) { |
| | return INSTANCE; |
| | } |
| | INSTANCE = new SingletonLazyVolatile(); // t1 这行代码会发生指令重排序,需要加入 volatile |
| | // 如:先赋值指令INSTANCE = new SingletonLazyVolatile,导致实例不为空,下一个线程会判空失败直接返回该对象 |
| | // 但是构造方法()指令还没执行,返回的就是一个不完整的对象。 |
| | return INSTANCE; |
| | } |
| | } |
| | } |
通过对并发编程的三要素介绍,也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
补充volatile知识:
volatile
只保证可见性(多线程下对变量的修改是可见的)、有序性(禁止进行指令重排序)
volatile 的底层实现原理是内存屏障(内存栅栏),Memory Barrier(Memory Fence),内存屏障会提供3个功能:
volatile
修饰之后的变量会加入读写屏障
关于volatile
的用途像两阶段终止、单例双重锁等等:
两阶段终止–volatile
| | @Log |
| | public class TwoPhaseStop { |
| | |
| | // 监控线程 |
| | private Thread monitorThread; |
| | |
| | // 多线程共享变量 单线程写入(停止线程) 多线程读取 使用 volatile |
| | private volatile boolean stop = false; |
| | |
| | // 启动监控线程 |
| | public void start() { |
| | monitorThread = new Thread(() -> { |
| | log.info("开始监控"); |
| | while (true) { |
| | log.info("监控中"); |
| | Thread currentThread = Thread.currentThread(); |
| | if (stop) { |
| | log.info("正在停止"); |
| | break; |
| | } |
| | try { |
| | log.info("正常运行"); |
| | Thread.sleep(5000); |
| | } catch (InterruptedException e) { |
| | // sleep出现被打断异常后、被打断后会清除打断标记 |
| | // 需要重新打断标记 |
| | currentThread.interrupt(); |
| | } |
| | } |
| | log.info("已停止"); |
| | },"monitor"); |
| | monitorThread.start(); |
| | } |
| | |
| | // 停止监控线程 |
| | public void stop() { |
| | stop = true; |
| | monitorThread.interrupt(); |
| | } |
| | |
| | } |
·
·
·
·
下篇预告:synchronized 和 volatile 区别和底层原理