亲宝软件园·资讯

展开

悲观锁与乐观锁 详解Java中的悲观锁与乐观锁

resumebb 人气:0
想了解详解Java中的悲观锁与乐观锁的相关内容吗,resumebb在本文为您仔细讲解悲观锁与乐观锁的相关知识和一些Code实例,欢迎阅读和指正,我们先划重点:悲观锁与乐观锁,Java锁,下面大家一起来学习吧。

一、悲观锁

悲观锁顾名思义是从悲观的角度去思考问题,解决问题。它总是会假设当前情况是最坏的情况,在每次去拿数据的时候,都会认为数据会被别人改变,因此在每次进行拿数据操作的时候都会加锁,如此一来,如果此时有别人也来拿这个数据的时候就会阻塞知道它拿到锁。在Java中,Synchronized和ReentrantLock等独占锁的实现机制就是基于悲观锁思想。在数据库中也经常用到这种锁机制,如行锁,表锁,读写锁等,都是在操作之前先上锁,保证共享资源只能给一个操作(一个线程)使用。

由于悲观锁的频繁加锁,因此导致了一些问题的出现:比如在多线程竞争下,频繁加锁、释放锁导致频繁的上下文切换和调度延时,一个线程持有锁会导致其他线程进入阻塞状态,从而引起性能问题。

二、乐观锁

乐观锁从字面上看是从积极,乐观的角度去看待问题,因此它认为数据一般不会产生冲突,因此一般不加锁,当数据进行提交更新时,才会真正对数据是否产生冲突进行监测。如果发生冲突,就返回给用户错误信息,由用户来决定如何去做,主要有两个步骤:冲突检测和数据更新。

三、CAS

CAS(compare and set),比较和更新。CAS是乐观锁的技术实现,当多个线程尝试使用CAS同时来更新同一个变量,只有一个线程能够更新变量值,而其他的线程都会失败,失败的线程并不会被挂起,告知这次竞争失败,可以再次尝试。

CAS操作包含三个操作数:

如果内存位置V的值与原预期值A相匹配,那么处理器就会自动将该位置更新为新值B,否则处理器不做任何处理。乐观锁是一种思想,CAS是这种思想的一种实现方法。Java中对CAS支持,在jdk1.5之后新增java.util.concurrent(J.U.C)就是建立CAS基础上,CAS是一种非阻塞的实现,例如:Atomic

四、AtomicXXX

在Java中,提供了一些原子化的操作类型,如下操作

 private volatile int value;
 
public final int get() {
        return value;
    }

读取的值,value是声明为volatile的,就可以保证在没有锁的情况下,线程可见性

在涉及到数据变更,以incrementAndGet实例:++i操作

public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

采用的CAS的操作,每次读取内存中的数据,让后将数据+1的结果进行CAS操作,如果成功就返回结果,负责重试指导成功为止,这里调用compareAndSet是CAS所依赖的JNI的实现的乐观锁 。

public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

Atomic就是volatile的使用场景,也是CAS的使用场景。

五、CAS中的ABA问题

CAS使用起来能够提高性能,但会引起ABA的问题

假如如下事件序列:

1、线程1从内次位置V来获取值A

2、线程2从内存位置V获取A

3、线程2进行一些操作,将B写入到V

4、线程2将A写入位置V

5、线程1进行CAS操作,发现位置V的值任然为A,操作成功了

6、线程1尽管CAS操作成功了,该过程有可能出现问题,对于线程1,线程2做的处理就可能丢失了

举例说明:一个链表ABA的例子

1、现有一个用单向链表实现的堆栈,栈顶为A。这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:

1head.compareAndSet(A,B);

2、在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再依次入栈D、C、A,而对象B此时处于游离状态。

3、此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B。但实际上B.next为null,此时堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,C、D被丢掉了。

image.png

六、ABA问题解决方案

ABA问题解决思路就是使用版本号,在变量前面追加版本号,每次对变量你进行更新的时候对版本进行加1,对于A->B->A 就会变成1A ->2B->3A

七、使用CAS会引起的问题

1.ABA问题

ABA问题可以使用版本号解决

2.循环时间长开销大

 自旋CAS如果长时间不成功,CPU带来非常大的执行开销,需要考虑长时间循环问题,给每个线程循环给定循环次数阈值,让当前线程释放CPU的使用权,进入阻塞中

3.只能保证一个共享变量的原子操作

八、Synchronized锁优化

JDK1.5之前, Synchronized称之为“重量级锁”,对该做了各种所有,分别为偏向锁、轻量级锁、重量级锁

Java对象内存布局:

说到 synchronized 加锁原理与Java对象在内存中的布局有很大关系, Java 对象内存布局如下:

图片.png

如上图所示,在创建一个对象后,在 JVM 虚拟机( HotSpot )中,对象在 Java 内存中的存储布局 可分为三块:

对象头区域

存放锁信息,对象年龄等信息

实例数据区域

此处存储的是对象真正有效的信息,比如对象中所有字段的内容

对齐填充区域

JVM 的实现 HostSpot 规定对象的起始地址必须是 8 字节的整数倍,换句话来说,现在 64 位的 OS 往外读取数据的时候一次性读取 64bit 整数倍的数据,也就是 8 个字节,所以 HotSpot 为了高效读取对象,就做了"对齐",如果一个对象实际占的内存大小不是 8byte 的整数倍时,就"补位"到 8byte 的整数倍。所以对齐填充区域的大小不是固定的。

synchronized用的锁是存在Java对象头里的,如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit,如下图:

图片.png

Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如下图所示:

图片.png

在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

图片.png

九、偏向锁

偏向锁的操作根本没有去找操作系统, 每个对象都有对象头,看看这个account对象的所谓“对象头”,其中有个叫做Mark Word:里边有几个标识位,还有其他数据。

图片.png

JVM使用CAS操作把线程ID记录到了这个Mark Word当中,修改了标识位,当前线程就拥有这把锁了

图片.png

可以看出:JVM不用和操作系统协商设置Mutex,它只记录下线程ID,就表示当前线程拥有这把锁了,不用操作系统介入

这时线程获得了锁,可以执行synchronized修饰的代码块。

当线程再次执行到这个synchronized的时候,JVM通过锁对象account的Mark Word判断:“当前线程ID还在,还持有着这个对象的锁,就可以继续进入临界区执行

这就是偏向锁,在没有别的线程竞争的时候,一直偏向当前线程,当前线程可以一直执行

十、轻量级锁

继续沿着偏向锁思路研究

另一个线程0x3704也要进入这个代码块执行,但是锁对象account 保存的是当前线程ID,他是没法进入临界区的。

这时也不需要和操作系统交流,JVM可以对偏向锁升级一下,变成一个轻量级的锁。

JVM把锁对象account恢复成无锁状态,在当前两线程的栈帧中各自分配了一个空间,叫做Lock Record,把锁对象account的Mark Word在俩线程的栈帧中各自复制了一份,叫做Displaced Mark Word

然后当前线程的Lock Record的地址使用CAS放到了Mark Word当中,并且把锁标志位改为00, 这意味着当前线程也已经获得了这个轻量级的锁了,可以继续进入临界区执行。

 图片.png

0x3704线程没有获得锁,但不阻塞,JVM让他自旋几次,等待一会儿。等当前退出临界区,释放锁的时候,需要把这个Displaced markd word 使用CAS复制回去。接下来他就可以加锁了。

两线程交替着进入临界区,执行这段代码,相安无事,很少出现真正的竞争。

即使是出现了竞争,想获得锁的线程只要自旋几次,等待一会儿,锁就可能释放了。

很明显,如果没有竞争或者轻度的竞争,轻量级锁仅仅使用CAS操作和Lock record就避免了重量级互斥锁的开销

十一、重量级锁

再次分析:轻量级锁运行时,一线程0x3704 正在持有锁。另一线程自旋了好多次,0x3704还是没释放锁。 这时候JVM考虑自旋次数太多了浪费CPU。接则升级为重量级锁!

重量级锁需要操作系统的介入,依赖操作系统底层的Mutex Lock。

JVM创建了一个monitor 对象,把这个对象的地址更新到了Mark word当中。

图片.png

在持有锁运行,而另一线程则切换进程状态至:阻塞

加载全部内容

相关教程
猜你喜欢
用户评论