扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
今天就跟大家聊聊有关java中如何实现悲观锁与乐观锁,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。
创新互联是一家专注于网站建设、成都网站设计和西信服务器托管的网络公司,有着丰富的建站经验和案例。
1、 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。在Java语言中synchronized关键字的实现就悲观锁。
2、乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS( Conmpare And Swap 比较并交换)实现的。
从上面的描述我们可以看出2种锁其实各有优劣,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
悲观锁其实没什么好讲,这里主要讲解写乐观锁。
JAVA的乐观锁主要采用CAS算法即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数
需要读写的内存值 V
进行比较的值 A
拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。正因为不断重试所以如果长时间不成功,会给CPU带来非常大的执行开销。
这样说或许有些抽象,我们来看一个例子:
1.在内存地址V当中,存储着值为10的变量。
2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。
3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。
7.线程1进行SWAP,把地址V的值替换为B,也就是12。
在JAVA中是通过Unsafe类提供的一系列compareAndSawp*方法来实现
首先我们先研究下Unsafe,初始化Unsafe用到Unsafe.getUnsafe() ;
通过查看源码我们发现这个类我们不能直接使用
@CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); //判断调用类是否BootstrapClassLoader加载,Unsafe是系统Jar包按JAVA双亲委派模式,这个类是由BootstrapClassLoader加载的。而普通项目的类是由CustomClassLoader加载 if (!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } }
我们继续看源码发现有3个CAS操作方法
var1: 要修改的对象
var2: 对象中field的偏移量
var4: 期望值(预期的原值)
var5: 更新值
返回值 true/false
这里可以看到这几个方法都是native方法,低层都是调操作系统的方法,这里不深入研究
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
因为Unsafe不方便调用(当然我们可以通过反射勉强也可以用),所以我们只能拿AtomicInteger来研究下。我们new AtomicInteger()时,会获取AtomicInteger对像Value字段的偏移量
public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { // 通过Unsafe方法获取value字段的偏移量(可以理解为C++的指针) valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; ... }
我们再来看下atomicInteger.getAndIncrement()这个方法的实现
/** * Atomically increments by one the current value. * * @return the previous value */ public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
它最终调用的是unsafe的getAndAddInt方法,我们继续往下跟踪
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; }
由上图可知,var1是AtomicInteger对象,var2是AtomicInteger对象value字段的偏移量,var5是期望值expected,var5+var4=var5+1。这么说可能比较清楚点,例如:
AtomicInteger atomicInteger = new AtomicInteger(2); int a = atomicInteger.getAndIncrement() ;
则var5为2,var5+var4=2+1=3。我们从上上图可以看出,这里用了个循环也就是说当期望值不是2时会一直循环尝试。相等就把x值赋值给offset位置的值,不相等,就取消赋值,方法返回false。这也是CAS的思想,及比较并交换。用于保证并发时的无锁并发的安全性。
这里有同学可能会担心死循环问题,其实不会的大家可以看下AtomicInteger那个类是设置成volatile类型,也就是内存可见所有线程获取到的值都是最新的。但是用循环却会产生ABA的问题。 即如果在此之间,V被修改了两次,但是最终值还是修改成了旧值V,这个时候,就不好判断这个共享变量是否已经被修改过。
为了防止这种不当写入导致的不确定问题,原子操作类提供了一个带有时间戳的原子操作类。带有时间戳的原子操作类AtomicStampedReference CAS(V,E,N)当带有时间戳的原子操作类AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。
以下是AtomicStampedReference类的compareAndSet方法
/** * Params: expectedReference – 当前值 newReference – 修改后的值 expectedStamp – 当前时间戳 newStamp – 修改后的时间戳 return true/false * */ public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)
以下是我的测试例子
import java.util.concurrent.atomic.AtomicStampedReference; public class CASTest { public static void main(String[] args) { int initialStamp = 1; AtomicStampedReferenceatomicStringReference = new AtomicStampedReference ( "value1", initialStamp); boolean exchanged1 = atomicStringReference.compareAndSet("value1", "value2", initialStamp, initialStamp+1); System.out.println("exchanged: ">
看完上述内容,你们对java中如何实现悲观锁与乐观锁有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注创新互联行业资讯频道,感谢大家的支持。
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流