一、什么是CAS?
CAS就是比较并交换(compare and swap)的意思,属于乐观锁的一种。通俗点说,当我们想修改一个值时,我们会先将这个值和原先的值进行比较,如果发现和原先的值一样,那么我们再进行修改。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量时,只有当预期值A和内存地址V中的实际值相同时,才会将内存地址对应的值修改为B。
如果发现不一致,则会重新进行尝试,这个尝试的过程被称为自旋。
二、CAS的缺点
1.cpu开销大
自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。在高并发的情况下,如果多个线程同时更新一个值不成功,就会一直自旋,增大cpu压力。
2.不能保证代码块的原子性
cas保证的是一个变量的原子性操作,如果要保证多个变量或者代码块的原子性,就要使用synchronized。
3.ABA问题(加版本号)
对于某一个数据,有一个线程将它操作后又恢复原状,而我们无法分辨。
三、底层原理
以AtomicInteger当中常用的自增方法 incrementAndGet为例
public final int incrementAndGet() {
//这里调用了unsafe类的getAndAddInt方法
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
那么问题是,getAndAddInt是如何保证原子性的呢?接着往下看
Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
// 循环调用compareAndSwapInt(),直到设置成功
// 如果设置失败,则通过getIntVolatile()方法从内存中获取新的值,然后再进行CAS操作
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
总结:CAS机制通过调用底层unsafe类的compareAndSwapInt保证了操作的原子性。compareAndSwapInt()方法是一个native方法,具体的实现是由JVM来完成的,native方法会有对应的c++方法。
四、自旋锁会一直自旋吗?如何解除?
如果一直自旋的话会消耗大量cpu资源,肯定是不行的,那么该如何解除自旋呢?
1.破坏for循环
一般可以在for循环中加控制语句,满足某个条件之后就跳出循环。
2.jvm针对当前cpu负荷对自旋时间做限制
- 如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞
- 自旋时会适当放弃线程优先级之间的差异
jvm关于自旋周期的选择,在jdk1.5是写死的,在1.6引入了适应性自旋锁,且默认开启。
适应性自旋锁:
自适应意味着自旋的次数不在固定,而是由前一次在同一个锁上的自旋时间和锁的拥有者的状态共同决定。
- 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很可能再次成功的,进而它将会允许线程自旋相对更长的时间。
- 如果对于某个锁,线程很少成功获得过,则会相应减少自旋的时间甚至直接进入阻塞的状态,避免浪费处理器资源。
关于java中的锁优化:
- 减少锁持有时间:只在有线程安全的程序上加锁
- 减小锁粒度:将大对象拆分成小对象,增加并行度,减小锁竞争(concurrenthashmap)
- 锁分离:读写锁
- 锁粗化:为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短。一个程序对同一个锁不间断、高频地请求与释放,会消耗掉一定的系统资源。锁粗化就是有些情况下我们反而希望把很多次锁的请求合并成一个请求,增大锁的粒度,以降低性能损耗。
- 锁消除:编译器级别。如果编译器发现不可能被共享的对象,那么会消除这些对象的锁操作。
四、锁总结
1.对象的5种状态
首先看一下对象是怎么存储在64位jvm堆中的:
1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;
2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;
3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;
4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;
5.对齐字是为了减少堆内存的碎片空间(不一定准确)。
对象的状态主要靠Mark Word来标记
biased_lock和lock一起,表达的锁状态
- lock:2位的锁状态标记位。
- biased_lock:对象是否启用偏向锁标记,只占1个二进制位。
- age:4位的Java对象年龄。
- identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。
- thread:持有偏向锁的线程ID。
- epoch:偏向锁的时间戳。
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。
锁变化的过程
1.初期锁对象刚创建时,还没有任何线程来竞争,对象的Mark Word是下图的第一种情形,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)。
2.当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word会记录自己偏爱的线程的ID,把该线程当做自己的熟人。
3.当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。
4.如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程。