java并发-乐观锁与悲观锁,独占锁与共享锁,公平锁与非公平锁,重入锁与非重入锁
java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁
从宏观上分类,锁的类型可分为乐观锁和悲观锁
- 乐观锁
乐观锁是一种读多写少,遇到并发写的性能可能会变低,每次去拿数据的时候别人都认为不会修改,所以不会上锁,但是在更新的时候会判断一下在这期间别人是否更新了数据,才去在写之前先读取版本号,然后加锁(比较更上一次版本号,如果一样则更新),如果失败则重复读-比较-写的操作。
java乐观锁基本都通过CAS操作实现的,cas是一种原子操作,比较当前值与传入值是否一样,一样则更新,额否则失败。 - 悲观锁
悲观锁就是悲观思想,任务写多,遇到并发写可能性能比较高。每次去拿数据都认为已经被别人修改,所有每次读取数据都会上锁,这样别人想读取数据就会block直到拿到锁。java中的悲观锁是synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取,获取不到,才会转为悲观锁,如RetreenLock。
基础知识二:java线程阻塞的代价
java的线程是映射到操作系统上原生线程之上的,如果阻塞或者唤醒一个线程需要操作系统的介入,需要在用户态与核心态之间切换,这种切换回消耗大量的系统资源。因为用户态与核心态都有各自专用的内存空间,专用的寄存器等,用户态切换到核心态需要传递许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态结束后切换到用户态继续工作。
- 如果线程转态切换是一个高频操作时,这将会消耗很多cpu的处理时间。
- 如果对于需要同步简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略是非常糟糕的。
synchronized会导致争用不到锁的进入阻塞状态,所有说是java中一个重量级的同步操作,别成为重量级锁,为了缓解上述性能问题,jvm从1.5开始,引入了轻量级锁和偏量锁,默认启用自旋锁,他们都属于乐观锁。
明确理解java中线程切换的代价,是理解java中各种锁优缺点的基础之一。
基础知识三:markword
在介绍java之前,先介绍一个markword,markword是java对象数据结构的一部分,这里只做markword的详细介绍,因为对象markword对java的各种类型的锁密切相关;
markword数据的长度在32位和64位的虚拟机(未开启压缩指正)分别为32bit和64bit,它的最后2位是锁的状态标志位
,用来标记当前对象所处的状态,觉得了markword存储的内容,如下表所示:
状态 | 标志位 | 存储内容 |
---|---|---|
未锁定 | 01 | 对象哈希码、对象分代年龄 |
轻量级锁定 | 00 | 指向锁记录的指针 |
膨胀(重量级锁定) | 10 | 执行重量级锁定的指针 |
GC标记 | 11 | 空(不需要记录信息) |
可偏向 | 01 | 偏向线程ID、偏向时间戳、对象分代年龄 |
32位的虚拟机在不同状态下的markword结构如下图
image.png
了解markword结构,有助于后面了解java锁的加解锁过程。
小结
前面 提到了java的四种锁,重量级锁、自旋锁、轻量级锁、偏向锁
不同的锁有不同的特点,每种锁只有在特定的场合下,才能发挥最大的作用,java中没有那种锁能在不同环境中都能有出色的效率,引入这么多锁的原因就是为了应付不同的场景。
前面提到了重量级锁是悲观锁,自旋锁、轻量级锁、偏向锁都是乐观锁,我们都有了一些了解,但是具体的如何使用这几种锁呢,这个就是后面分析他们的特性了;
java中的锁
synchronized 和ReentrantLock是可重入锁
ReentrantLock中可重入锁实现
这里看非公平锁的锁获取方法:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//就是这里
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
在AQS中维护了一个private volatile int state来计数重入次数
,避免了频繁的持有释放操作,这样既提升了效率,又避免了死锁。
与可重入锁synchronized和Lock不同的就是自旋锁。
public class SpinLock {
private AtomicReference<Thread> owner =new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while(!owner.compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
对于自旋锁来说,没有 int c = getState(),private volatile int state维护
- 若有同一线程两调用lock() ,会导致第二次调用lock位置进行自旋,产生了死锁说明这个锁并不是可重入的。(在lock函数内,应验证线程是否为已经获得锁的线程)
- 若1问题已经解决,当unlock()第一次调用时,就已经将锁释放了。实际上不应释放锁。(采用计数次进行统计)
转自:https://blog.csdn.net/zqz_zqz/article/details/70233767
https://www.zhihu.com/question/23284564
https://blog.csdn.net/rickiyeat/article/details/78314451
网友评论