volatile的应用
- volatile修饰类属性(类变量和实例变量),synchronized修饰类方法、代码块,同时volatile在并发中是**不安全**的
- 作用:
- 使共享变量在多线程间可见,如果一个字段被声明成volatile,Java线程内存模型会确保**所有线程**看到这个变量的值都是**一致**的
- 与volatile实现原理相关的CPU术语与说明
- 内存屏障:是一类同步屏障指令,是CPU或者编译器在对内存随机访问的操作中的一个同步点,只有在此点之前的所有读写操作都执行后才可以执行此点之后的操作
- 缓冲行:缓存中可以分配的最小存储单位。处理器填写缓存线时会加载整个缓存线,需要使用多个主内存读周期
- 原子操作:不可中断的一个或一系列操作,比如JDBC的事物就是一个原子操作
- 填充:当一个容器中有很多数据时,数据读取就会变得拥挤、缓慢,所以此时我们对每个数据进行填充,使得一个容器中只能有三个数据甚至只有她自己,这时候数据读取就很快了
- 仅仅是volatile保证不了安全
- volatile的工作过程
- 对声明了volatile的 变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据 写回到系统内存
- 但,如果其他处理器缓存的值还是旧的,为了保证各个处理器的缓存是一致的,就会实现缓存一 致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当 处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状 态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存 里
synchronized的实现原理与应用
synchronized实现同步的基础:Java中的每一个对象都可以作为锁
synchronized是java的内置锁,也叫做内部锁或监视器锁,它的作用就是在同一时刻只有一个线程 能进入synchronized代码块
- 同步:需要排队
- 异步:不需要排队
- 关于同步的3种形式
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是Synchronized括号里配置的对象
- **实现原理**:JVM基于进入和退出Monitor对 象来实现方法同步和代码块同步(这是最底层的原理)。
- 代码块同步是使用monitorenter 和monitorexit指令实现的
- 方法同步是使用另外一种方式实现的,细节在JVM规范里并没有 详细说明。但是,方法的同步同样可以使用这两个指令来实现
- Java对象头
什么时候上锁归Java头对象决定(遇到Java头对象的锁标志),什么时候释放锁由monitor决定
- **锁的升级与对比**
- 升级过程:偏向锁-->轻量级锁-->重量级锁,同时,这个过程是**不可逆**的,当成为重量级锁时就永远停留在重量级锁
- 偏向锁:
- 引入:大多数情况下,锁不仅不存在多线程竞争,而且总是由**同一线程**多次获得
- 在没有多线程竞争时,访问synchronized修饰的同步代码,会先使用偏向锁 ;
- 适合于没有竞争
- 轻量级锁:
- 当其在访问资源时,若遇到资源被上锁,会每隔一段时间就去询问一下该资源是否仍然被占用(就是通过自旋)
- 适合于少量并发
- 重量级锁:
- 当其在访问资源时,若遇到资源被上锁,则会马上释放CPU,并且进入阻塞队列,此时所有的CPU资源都将用于正在占用资源的进程上,就能够更快的执行完成
- 非常适合并发高的情况
- 锁的优缺点对比
原子操作的实现原理
- 原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意 为“不可被中断的一个或一系列操作”。
- 相关术语
- CPU流水线
- 流水线是指在程序执行时多条指令重叠进行操作的一种准并行处理实现技术。即可以同时为**多条指令的不同部分**进行工作,以提高各部件的利用率和指令的平均执行速度。
- 流水线执行时会大大减少系统中电压切换次数,效率会大大的增高,但是会打破原有的执行顺序,导致我们需要进行**指令重排序**
- 处理器实现原子操作
32位IA-32处理器使用基于对**缓存加锁**或**总线加锁**的方式来实现多处理器之间的原子操作
- 使用**总线锁**保证原子性
- 总线锁就是使用处理器提供的一个 **LOCK#**信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该 处理器可以独占共享内存。
- 总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作**其他内存地址**的数据,所以总线锁定的**开销**比较大
- 使用**缓存锁**保证原子性
- 在同一时刻,我们只需保证**对某个内存地址**的操作是原子性即可
- 使用基础
- 频繁使用的内存会缓存在处理器的L1、L2和L3高速缓存里,那么原子操作就可以直接在 处理器内部缓存中进行,并不需要声明总线锁
- 两种种情况下不使用缓存锁定
- 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行 (cache line)时,则处理器会调用总线锁定
- 有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的 内存区域在处理器的缓存行中也会调用总线锁定
- Java实现原子操作
- 使用循环CAS实现原子操作
- 自旋CAS实现的基本 思路就是循环进行CAS操作直到成功为止
- CAS实现原子操作的三大问题
CAS虽然很高效地解决了原子操作,但是CAS仍然存在三 大问题。**ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作**。
- ABA问题
- 由来
- 如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它 的值没有发生变化,但是实际上却变化了
- 解决思路
- ABA问题的解决思路就是使用版本号
- 在变量前面 追加上**版本号**,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A
- 循环时间长开销大
- 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
- 解决:
- pause指令
- 第 一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间 取决于具体实现的版本,在一些处理器上延迟时间是零
- 第二,它可以避免在退出循环的时候 因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而 提高CPU的执行效率。
- 只能保证一个共享变量的原子操作
- 这时候需要使用**锁**
- 有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作