1. CAS(Compare And Swap)
(1)CAS 的由来
默认情况下,加锁都是依赖于OS 的 mutex 这个互斥锁(内核态代码)。一旦这个锁请求失败,线程就会进入阻塞状态,放弃CPU,下一次执行的时间就不确定。因此,就想了一些办法,如何不通过OS 来实现按加锁功能。
(2)实现
1)CAS伪代码
上述整个CAS流程,对应了一个指令。
2)CAS就是乐观锁,或说是乐观锁的一种体现。是解决问题的一种思路,也可以是一种方法。
PS:被volatile 修饰的变量,其它线程一定可见;但不被volatile修饰的变量,具体什么时候可见,不确定。并不是不被volatile修饰的变量一定不可见。
3)CAS省去了加锁解锁的过程,也保证了线程安全,相比于OS的 mutex 更加高效。
CAS没有线程阻塞。
在竞争不激烈的情况下,CAS更加高效;若竞争激烈,原子类底层是 while 的循环,CPU就会一直空转。
如:
===》修改成原子类操作:
(3)基于CAS实现的自旋锁
1)伪代码的实现
2)自旋锁,是乐观锁,也是轻量级锁。
当前这把锁没有立即拿到,但预期很快就能拿到,所以一直自旋。短暂自旋几次之后,当其它线程释放锁之后,就可以立马拿到锁。
(4)CAS 的 ABA问题
1)CAS 先比较,再交换。
当前值和与气质一样时,就认为中间没有发生过变化,但存在一个漏洞。当前值和与气质一样,可能是因为其他线程进行了修改,然后又修改了回去,这个过程,仿佛中间没有发生过变化,但真实是发生了变化。
2)解决:通过版本号。
在比较时,不仅仅比较值,还要比较版本号,这个版本号,只能增加,不能减少。
版本号只是一个概念,可以通过时间戳等方式来实现。
PS:在大部分情况下,CAS即使存在ABA 问题,也没有bug 。但在一些特定情况下,AB就会由问题,所以就会需要进行处理。
2. sychronized 原理(是一个抽象的东西)
(1)sychronized
是一个乐观锁,也是一个悲观锁(看锁的竞争程度);
不是读写锁,是普通的互斥锁;
即是轻量级锁,又是重量级锁(看锁的竞争程度,逐渐升级);
最开始是自旋锁(自旋锁也是轻量级锁),后面逐渐升级;
非公平锁;
可重入锁。
PS:广义上只考虑到JDK8(也即JDK1.8)。
PS:sychronized 的实现是基于对象的monitor 锁,每一个对象都有一个monitor对象进行能关联,底层依赖于OS 的 Mutex锁进行实现(最终也是依赖于硬件实现的(硬件先实现了一些功能,软件在硬件的基础上再去实现一些功能))。
(2)加锁工作过程
大部分情况下,锁不仅不存在竞争,而且锁还经常由同一个对象获得。
1)偏向锁
偏向锁是一个标志,在对象头存储当前锁偏向的线程。当一个线程访问时,会先判断这个标志释放等于当前线程,若等于当前线程,就直接获得锁;若为空,就把当前锁的对象头设置成自己。
PS:并不是一把真正的锁,局势在锁对象的对象头里,设置了偏向的线程。当线程加锁时,会先判断对象头这个偏向锁是否偏向自己,是则直接使用,不是则要求取消偏向锁再升级锁进行锁的竞争。
2)自旋锁(轻量级锁)
随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁)。此处的轻量级锁就是通过 CAS 来实现。
3)重量级锁
如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁。此处的重量级锁就是指用到内核提供的 mutex 。
(3)锁消除
把锁消除,在一些不必要加锁的地方加了锁,编译器或JVM会帮助优化,把锁消掉。
PS:StringBuffer 线程安全与锁消除没有关系,是因为StringBuffer 的方法使用了sychronized 修饰。(Java里有很多数据结构是线程安全的,原因就是因为使用了sychronized修饰。)
(4)锁粗化
若加的锁粒度太小,就把加锁的范围扩大。粗细,是指代码粒度的粗细,即加锁的代码多或少。若加锁的代码非常多,就认为这个锁粒度较粗,反之则较细。
若一段代码,我们频繁加锁,sychronized就会帮我们进行锁的粗化。