*还是要使用才能暴露问题呀!
并发与锁的知识整理
目录
什么是重量级锁?
重量级锁是指线程上锁需要向内核态申请锁资源,这中间会有一个申请的过程,所以很重。jdk早期是这种锁,要上锁得通过kernel申请锁资源。
什么是轻量级锁
轻量级锁也叫自旋锁、无锁或者一般来说CAS也是指轻量级锁
轻量级锁在程序中实现锁,避免了向内核态申请锁的操作。
正如它的名字所说,其核心在于“先写入再交换”,当写入的时候会校验输入的参数与获取时是否一致,如果不一致,那么重新来再跑一遍程序,如此往复循环就想在原地转圈,所以又叫自旋锁。
轻量级锁的问题
1. 轻量级锁什么时候升级为重量级锁
1.6之前,当线程自旋超过10次,或者正在自旋的线程超过CPU核数的一半,就会升级。不过,1.6之后,java已经加入了自适应自旋,由JVM自己控制了。
2. 著名的ABA问题
轻量级锁是通过比较输入参数来确定自己获取的锁没有被其他线程抢走的,但是如果其他线程做的事情是这样的——线程1将值从A改为B,线程2将值从B改为A——那么CAS要怎样分辨获取的锁是没有问题的呢?
·解决方法
如果是基础类型的变量,那么应该保证线程拿到这个变量做的事情只和其值有关系,是否变过、变过几次多结果并不影响。
如果不是基础类型的变量,则需要使用版本号来标识(这不就和git一样吗!)。
其实我们熟悉的atomic包下就有这样的锁:
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the updated value
*/
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
这里我们可以看到AtomicInteger类调用了unsafe这个成员变量的getAndAddInt方法。那么这个方法做了什么呢?
// 我也不知道var3哪里去了
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
这里unsafe就是用了native的compareAndSwapInt方法来进行更新。
这个unsafe提供了硬件级别的原子操作,cas只是其中一个。
这个方法就已经是在Hotspot里面的代码了,如果要深究的话,Hotspot在底层实现是这个方法
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
开始看不懂了吧,这块你只要知道is_MP()是用来确定是不是多个cpu在处理(is_mutiple_processor),LOCK_IF_MP(%4)是说如果是多个处理器在处理,则锁资源。
这边其实可以看到最终使用了cmpxchgl命令(应该是汇编语言),硬件层面不会锁总线。
lock cmpxchg指令
偏向锁与匿名偏向锁
什么是偏向锁?为什么会出现偏向锁?
HotSpot的作者发现大多数情况下,锁不存在多线程竞争,并且总是由同一个线程多次获得,所以为了让线程获得锁的代价更低,引入了所谓“偏向锁”——记录了上锁线程信息并且当上锁线程在进入和退出同步块的时候不需要进行cas操作加锁解锁。
啥是匿名偏向锁?
如果jvm的偏向锁设置是打开状态的话,那么一个实例被new出来的时候默认是开启偏向锁的(markword的倒数三位为101),只不过这时候还没有填上线程Id,所以叫匿名偏向锁。
——如果没有打开偏向锁设置的话那么实例被new出来的时候是无锁状态(001)。
偏向锁什么时候升级?
为什么这么设计呢?我们可以想象一下,如果线程之间不存在竞争,那么线程每次进入执行状态的时候其实没有必要做多次校验,也省下了加锁解锁的开销。
那么如果有竞争了怎么办呢?
这里就引入了偏向锁的撤销机制——如果有竞争,则撤销偏向锁,转为自旋锁。
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,
持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正
在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,
如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈
会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他
线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。图2-1中的线
程1演示了偏向锁初始化的流程,线程2演示了偏向锁撤销的流程。
——摘自《Java并发变成的艺术》
偏向锁的撤销机制可以再另一篇笔记中详细研究。
大概的描述就是:
线程在自己的线程栈生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁。
关于偏向锁的应用场景
1.偏向锁一定比自旋锁性能好吗?
不一定,如果明知道肯定会有线程竞争,那可以一开始就是用自旋锁(反正竞争了也要变成自旋锁的)。
java6、java7和java11中都是默认启用偏向锁的,线程在获取锁时优先获取偏向锁,但是前4秒并不是这样的,这是因为jvm在启动的时候明确知道会有线程竞争资源,所以延迟开启了偏向锁。
jdk8默认对象头是无锁的。←尚待研究
*附测试链接:https://blog.csdn.net/weixin_41263382/article/details/106677235
2.待续。