今天这篇是我的好朋友 evil say的投稿,这小伙现在大四,客观来说,大四有这个实力,我觉得很不错。他目前正在找实习,如果看了本文觉得他可以,有公司有坑位、愿意抛出橄榄枝的话。请联系他:hack7458@outlook.com
Java 编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致性的更新,线程应该确保通过排他锁单独获得这个变量。Java 语言提供了 volatile,在某些情况下比锁要更加的方便。如果一个字段被声明成 Volatile,Java 线程内存模型确保所有线程看到这个变量的值是一致的。
instance = new Sigleton; // instance 是 volatile 变量
// 转变成汇编代码
// 0x01a3deld: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0 x 0,(%esp)
Lock 前缀指令:Lock 前缀指令在多核 CPU 下将当前处理器缓存行写回到系统内存,这个写回操作会使其他 CPU 缓存了该内存的地址的数据无效
缓存行填充:当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个高速缓存行到适当的缓存 (L1,L2,L3 或所有)。
如果对声明了 Volatile 的变量进行写操作,JVM 会向处理器发送一条 Lock 前缀指令,将这个变量所在缓存行的数据写回到系统内存当中。在多处理器下,为了保证各个处理器缓存的缓存是一致的,就会实现缓存一致性协议当处理器发现自己缓存行对应的内存地址被修改,就会将当前的处理器缓存设置为无效,处理器对这个数据进行修改操作时,会重新从系统内存中把数据读到处理器缓存里
缓存行:CPU 高速缓存中可以分配的最小存储单位,处理器填写缓存行时会加载整个缓存行。
缓存一致性协议:缓存一致性协议通俗来讲是在多 CPU 的场景下,为了实现多线程同步而采取的一种技术手段,就像多线程同步是一种线程级别间的一致性保证。
特性:
禁止重排序: 编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
缺点:
为了保证处理器中缓存一致性,会将当前的处理器缓存设置为无效的,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。注意的是这里的修改操作,是指的一个操作。可以知道自增操作是三个原子操作组合而成的复合操作。在一个操作中,读取了 inc 变量后,是不会再读取的 inc 的,所以它的值还是之前读的 10,它的下一步是自增操作。
synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。JavaSE1.6 之后相继为 Synchronized 为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor (monitor 对象存在于每个 Java 对象的对象头中,synchronized 锁便是通过这种方式获取锁(这也是为什么 Java 中任意对象可以作为锁的原因)的持有权。当计数器为 0 则可以成功获取,获取后将锁计数器设为 1 也就是加 1。相应的在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
在 JVM 存储大量存储对象同时,存储时为了实现一些额外的功能,需要在对象头添加一些标记字段用于增强对象功能,这些标记字段组成了对象头,Java 对象头分为数组类型跟非数组类型一个占用 4 字节,一个占用 8 字节。
//双重检查锁
public class Singleton(
//使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
private volatile static Singleton instance
private Singleton()
public Singleton getInstance(
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
)
)
在多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获取,为了让其获得锁的代价更低而引入了偏向锁。
引入轻量级锁的主要目的是在多线程竞争不激烈的情况下,通过 CAS 竞争锁,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
重量级锁通过对象内部的监视器(monitor)实现,其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程使用自旋会消耗 CPU | 追求响应时间,锁占用时间很短 |
重量级锁 | 线程竞争不使用自旋,不会消耗 CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,锁占用时间较长 |
全称是 Compare and Swap,即比较并交换。是通过原子指令将获取存储在内存地址的原值和指定的内存地址进行比较,只有当它们值相等时,交换指定的预期值和内存中的值,这个操作是原子操作,若不相等,则重新获取存储在内存地址的原值。
CAS 是一种无锁算法,有 3 个关键操作数,内存地址,旧的内存中预期值,要更新的新值,当内存值和旧的内存中预期值相等时,将内存中的值更新为新值。
比较著名有 ABA 问题,当 CAS 在操作的时候会检查变量的值是 A,接着变成 B,最后又变成 A,实际上这个值已经是被修改过的,为了解决这个问题,JDK 中提供了 AtomicStampedReference 类解决 ABA 问题,用 Pair 这个内部类实现,包含两个属性,分别代表版本号和引用,在 compareAndSet 中先对当前引用进行检查,再对版本号标志进行检查,只有全部相等才更新值。
线程的挂起和恢复会极大的影响开销,很多线程在等待锁的时候,在很短的一段时间就获得了锁,所以它们在线程等待的时候,并不需要把线程挂起,而是让他无目的的循环,一般设置 10 次。这样就避免了线程切换的开销,极大的提升了性能。
而适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋 10 次一下。他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。
-END-