1.锁的定义
为了解决多线程对共享资源的使用存在安全问题,从而提出锁的概念。
2.锁分类
这里引用java 锁分类
3.Java 中常用的锁关键字
关键字 | 分类 | 特点 | 底层实现原理 | 性能分析 |
---|---|---|---|---|
volatile | 轻量级锁 | 可见性 禁止指令重排 happens-before原则 | lock前缀指令会引起处理器缓存回写到内存,导致其他处理器的缓存行失效 volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。 | 由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值。 |
synchronized | 悲观锁 独占锁 可重入锁 非公平锁 重量级锁 | 原子性 可见性(在释放锁之前会将对变量的修改刷新到主存当中) 有序性(独占锁) 可重入性 (线程拥有了锁仍然还可以重复申请锁 即锁得对象为方法的调用者或static的对象) | 无论是同步方法(隐式同步)、同步代码块(显式同步)的同步是依赖monitor对象,同步方法是调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的,有则尝试获取monitor对象成功则执行,否则等待线程被唤醒。同步代码块采用monitorenter和monitorexit这两个字节码指令。 synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁(也有借鉴ReenTrantLock的CAS的原理) | 效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。因此引入其他的锁,来减少使用重量级的频次。 |
Lock(接口) ReentrantLock(类) | 可重入锁 悲观锁 独占锁 | 需要主动释放锁(lock、unlock) 可以中断等待锁的线 | 先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁,非公平采用CAS,公平采用queue实现。 | 避免了使线程进入内核态的阻塞状态 |
ReadWriteLock(接口) ReentrantReadWriteLock | 可降级锁 非公平锁 公平锁 可重入锁 共享锁 | 锁降级 写锁可以获得读锁,
可中断 在读锁和写锁的获取过程中支持中断 | ReadLock 对一个资源的读取可以理解为轻量级锁,多个线程可以同时进行 WriteLock 必须为独占锁 底层是继承 AbstractQueuedSynchronizer ,lock.lock()实现加锁,非公平采用CAS,公平采用queue实现。 | |
CAS(算法) | 无锁算法 乐观锁 | 高效 ABA问题 锁升级 (自旋cpu开销大) 原子性 (单一变量) | 在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步 | ABA问题的解决思路就是在变量前面添加版本号,这样变化过程就从“A-B-A”变成了“1A-2B-3A” CAS操作长时间不成功,会导致其自旋 锁升级 (不可逆) |
volatile 关键字总结
- volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如booleanflag;或者作为触发器,实现轻量级同步。
- volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁_上,所以说它是低成本的。
- volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
- volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓存,始终从主 存中读取。
- volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作。
- volatile可以使得long和double的赋值是原子的。
- volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性
4.缓存
- 缓存段(缓存行) :CPU看到一条读取内存的指令时,它会把内存地址传递给一级数据缓存,一级数据缓存会检查它是否有这个内存地址对应的缓存段,如果没有就把整个缓存段从内存(或更高一级的缓存)中加载进来。注意,这里说的是一次加载整个缓存段,这就是上面提过的局部性原理
- 缓存一致性协议:MESI协议保证了每个缓存中使用的共享变量的副本是一致的。
- 缓存锁:LOCK#会锁总线(不能访问系统内存)效率太低了,单个缓存行(缓存行是缓存中数据存储的基本单元)的数据进行加锁,不会影响到内存中其他数据的读写。
- 缓存行失效 :每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
- 伪共享 :当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享(解决办法:缓存行填充,变量对齐)。 参考博客1 ,参考博客2
在jdk1.7环境下,由于java 7会优化掉无用的字段,需要使用继承的办法来避免填充被优化掉。把填充放在基类里面。
JAVA 8中添加了一个@Contended的注解,添加这个的注解,将会在自动进行缓存行填充,执行时,必须加上虚拟机参数-XX:-RestrictContended,@Contended注释才会生效
对于汇编指令中执行加锁操作的变量,MESI协议在以下两种情况中也会失效:
- CPU不支持缓存一致性协议。
- 该变量超过一个缓存行的大小,缓存一致性协议是针对单个缓存行进行加锁,此时,缓存一致性协议无法再对该变量进行加锁,只能改用总线加锁的方式。
5.锁升级过程
锁升级通常包含锁的几种状态:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁
- 偏向锁:在对象头和栈帧中的锁记录存储偏向线程ID(多数情况下,一个对象总是由一个线程多次获得)。当其他线程尝试获取锁时,持有锁的线程才会释放锁(线程2 CAS 替换对象头的MarkWord,不成功则撤销偏向锁 )。
- 轻量级锁:首先创建锁记录空间(栈帧),copy 对象头的 MarkWord 。尝试CAS替换对象头MarkWord(尝试加锁),失败后自旋;解锁使用CAS displaced MarkWord,失败则锁升级。
- 重量级锁:阻塞未获取锁的线程(互斥)
6.对象头信息