分销商城网站建设,汉中seo培训,动漫制作专业用什么笔记本电脑,jsp网站开发好书锁可以从不同的角都分类。其中乐观锁和悲观锁是一种分类方式
一、悲观锁、乐观锁定义 悲观锁就是我们常说到的锁。对于悲观锁来说#xff0c;他总是认为每次访问共享资源时会发生冲突#xff0c;所以必须每次数据操作加上锁#xff0c;以保证临界区的程序同一时间只能有一个…锁可以从不同的角都分类。其中乐观锁和悲观锁是一种分类方式
一、悲观锁、乐观锁定义 悲观锁就是我们常说到的锁。对于悲观锁来说他总是认为每次访问共享资源时会发生冲突所以必须每次数据操作加上锁以保证临界区的程序同一时间只能有一个线程在执行。 乐观锁又称为“无锁”,顾名思义它是乐观派。乐观锁总是假设对共享资源的访问没有冲突线程可以不停地执行无需加锁也无需等待。而一旦多个线程发生冲突 乐观锁通常是使用一种称为CAS的技术来保证线程执行的安全性。 由于无锁操作中没有锁的存在因此不肯能出现死锁的情况也就是说乐观锁天生免疫死锁。 乐观锁多用于“读多写少”的环境避免频繁加锁影响性能而悲观锁锁用于“写多读少”的环境。避免频繁失败和重试影响性能。 二、实现方式
悲观锁的实现方式是加锁加锁既可以是对代码块加锁如Java的synchronized关键字也可以是对数据加锁。synchronized关键字和Lock的实现类都是悲观锁。
乐观锁的实现方式主要有两种CAS机制和版本号机制。乐观锁在Java中是通过使用无锁编程来实现最常采用的是CAS算法Java原子类中的递增操作就通过CAS自旋实现的。
1、CASCompare And Swap CAS操作包括了3个操作数
需要读写的内存位置(V)进行比较的预期值(A)拟写入的新值(B)
CAS操作逻辑如下如果内存位置V的值等于预期的A值则将该位置更新为新值B否则不进行任何操作。许多CAS的操作是自旋的如果操作不成功会一直重试直到操作成功为止。 这里引出一个新的问题既然CAS包含了Compare和Swap两个操作它又如何保证原子性呢答案是CAS是由CPU支持的原子操作其原子性是在硬件层面进行保证的。
下面以Java中的自增操作(i)为例看一下悲观锁和CAS分别是如何保证线程安全的。我们知道在Java中自增操作不是原子操作它实际上包含三个独立的操作1读取i值2加13将新值写回i。
因此如果并发执行自增操作可能导致计算结果的不准确。在下面的代码示例中value1没有进行任何线程安全方面的保护value2使用了乐观锁(CAS)value3使用了悲观锁(synchronized)。运行程序使用1000个线程同时对value1、value2和value3进行自增操作可以发现value2和value3的值总是等于1000而value1的值常常小于1000。
public class suo {//value1线程不安全private static int value1 0;//value2使用乐观锁private static AtomicInteger value2 new AtomicInteger(0);//value3使用悲观锁private static int value3 0;private static synchronized void increaseValue3(){value3;}public static void main(String[] args) throws Exception {//开启1000个线程并执行自增操作for(int i 0; i 1000; i){new Thread(new Runnable() {Overridepublic void run() {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}value1;value2.getAndIncrement();increaseValue3();}}).start();}//打印结果Thread.sleep(1000);System.out.println(线程不安全 value1);System.out.println(乐观锁(AtomicInteger) value2);System.out.println(悲观锁(synchronized) value3);}
}输出 线程不安全991 乐观锁(AtomicInteger)1000 悲观锁(synchronized)1000 首先来介绍AtomicInteger。AtomicInteger是java.util.concurrent.atomic包提供的原子类利用CPU提供的CAS操作来保证原子性除了AtomicInteger外还有AtomicBoolean、AtomicLong、AtomicReference等众多原子类。 java是无法实现对底层内存的操作的C可以java使用Unsafe类实现。
public class AtomicInteger extends Number implements java.io.Serializable {private static final long serialVersionUID 6214790243416807050L;// setup to use Unsafe.compareAndSwapInt for updatesprivate static final Unsafe unsafe Unsafe.getUnsafe();private static final long valueOffset;static {try {valueOffset unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField(value));} catch (Exception ex) { throw new Error(ex); }}private volatile int value;unsafe 获取并操作内存的数据。valueOffset 存储value在AtomicInteger中的偏移量。value 存储AtomicInteger的int值该属性需要借助volatile关键字保证其在线程间是可见的。
我们查看AtomicInteger的自增函数incrementAndGet()的源码时发现自增函数底层调用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class只通过class文件中的参数名并不能很好的了解方法的作用所以我们通过OpenJDK 8 来查看Unsafe的源码
// ------------------------- JDK 8 -------------------------
// AtomicInteger 自增方法
public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) 1;
}// Unsafe.class
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;
}// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {int v;do {v getIntVolatile(o, offset);} while (!compareAndSwapInt(o, offset, v, v delta));return v;
}根据OpenJDK 8的源码我们可以看出getAndAddInt()循环获取给定对象o中的偏移量处的值v然后判断内存值是否等于v。如果相等则将内存值设置为 v delta否则返回false继续循环进行重试直到设置成功才能退出循环并且将旧值返回。整个“比较更新”操作封装在compareAndSwapInt()中在JNI里是借助于一个CPU指令完成的属于原子操作可以保证多个线程都能够看到同一个变量的修改值。
其他源码 public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}
public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1);
}getAndIncrement()实现的自增操作是自旋CAS操作在循环中进行compareAndSet如果执行成功则退出否则一直执行。其中compareAndSet是CAS操作的核心它是利用Unsafe对象实现的。Unsafe又是何许人也呢Unsafe是用来帮助Java访问操作系统底层资源的类如可以分配内存、释放内存通过UnsafeJava具有了底层操作能力可以提升运行效率强大的底层资源操作能力也带来了安全隐患(类的名字Unsafe也在提醒我们这一点)因此正常情况下用户无法使用。AtomicInteger在这里使用了Unsafe提供的CAS功能。valueOffset可以理解为value在内存中的偏移量对应了CAS三个操作数(V/A/B)中的V偏移量的获得也是通过Unsafe实现的。value域的volatile修饰符Java并发编程要保证线程安全需要保证原子性、可视性和有序性CAS操作可以保证原子性而volatile可以保证可视性和一定程度的有序性在AtomicInteger中volatile和CAS一起保证了线程安全性。关于volatile作用原理的说明涉及到Java内存模型(JMM)这里不详细展开。 2、版本号机制
除了CAS版本号机制也可以用来实现乐观锁。版本号机制的基本思路是在数据中增加一个字段version表示该数据的版本号每当数据被修改版本号加1。当某个线程查询数据时将该数据的版本号一起查出来当该线程更新数据时判断当前版本号与之前读取的版本号是否一致如果一致才进行操作。
需要注意的是这里使用了版本号作为判断数据变化的标记实际上可以根据实际情况选用其他能够标记数据版本的字段如时间戳等。
三、优缺点和适用场景
1、功能限制
与悲观锁相比乐观锁适用的场景受到了更多的限制无论是CAS还是版本号机制。
例如CAS只能保证单个变量操作的原子性当涉及到多个变量时CAS是无能为力的而synchronized则可以通过对整个代码块加锁来处理。再比如版本号机制如果query的时候是针对表1而update的时候是针对表2也很难通过简单的版本号来实现乐观锁。
2、竞争激烈程度
如果悲观锁和乐观锁都可以使用那么选择就要考虑竞争的激烈程度
当竞争不激烈 (出现并发冲突的概率小)时乐观锁更有优势因为悲观锁会锁住代码块或数据其他线程无法同时访问影响并发而且加锁和释放锁都需要消耗额外的资源。 当竞争激烈(出现并发冲突的概率大)时悲观锁更有优势因为乐观锁在执行更新时频繁失败需要不断重试浪费CPU资源。
悲观锁适合写操作多的场景先加锁可以保证写操作时数据正确。乐观锁适合读操作多的场景不加锁的特点能够使其读操作的性能大幅提升。
四、乐观锁加锁吗
1乐观锁本身是不加锁的只是在更新时判断一下数据是否被其他线程更新了AtomicInteger便是一个例子。
2有时乐观锁可能与加锁操作合作。
五、CAS有哪些缺点
1、ABA问题
假设有两个线程——线程1和线程2两个线程按照顺序进行以下操作
(1)线程1读取内存中数据为A
(2)线程2将该数据修改为B
(3)线程2将该数据修改为A
(4)线程1对数据进行CAS操作
在第(4)步中由于内存中数据仍然为A因此CAS操作成功但实际上该数据已经被线程2修改过了。这就是ABA问题。
在AtomicInteger的例子中ABA似乎没有什么危害。但是在某些场景下ABA却会带来隐患例如栈顶问题一个栈的栈顶经过两次(或多次)变化又恢复了原值但是栈可能已发生了变化。
对于ABA问题比较有效的方案是引入版本号内存中的值每发生一次变化版本号都1在进行CAS操作时不仅比较内存中的值也会比较版本号只有当二者都没有变化时CAS才能执行成功。Java中的AtomicStampedReference类便是使用版本号来解决ABA问题的。这样变化过程就从“ABA”变成了“1A2B3A”。
2、循环时间长开销大
CAS操作如果长时间不成功会导致其一直自旋给CPU带来非常大的开销。
3、功能限制
只能保证一个共享变量的原子操作。对一个共享变量执行操作时CAS能够保证原子操作但是对多个共享变量操作时CAS是无法保证操作的原子性的。