乐观锁与悲观锁
锁可以从不同的角度分类。其中,乐观锁和悲观锁是一种分类方式。
乐观锁:
乐观锁又称为“无锁”。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。而一旦多个线程发生冲突,乐观锁通常是使用一种称为 CAS 的技术来保证线程执行的安全性。
由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说乐观锁免疫死锁。
乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;而悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。
悲观锁:
悲观锁就是我们常说的锁。对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。
CAS
在Java中可以通过锁和循环 CAS 的方式来实现原子操作。
CAS 的全称是:比较并交换(Compare And Swap)。在CAS中,有这样三个值:
- 内存地址V
- 旧的预期值A
- 即将要更新的目标值B
比较并交换的过程如下:
CAS 指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
我们以一个简单的例子来解释这个过程:
- 如果有一个多个线程共享的变量
i
原本等于5,我现在在线程A中,想把它设置为新的值6; - 我们使用CAS来做这个事情;
- 首先我们用
i
去与5对比,发现它等于5,说明没有被其它线程改过,那我就把它设置为新的值6,此次CAS成功,i
的值被设置成了6; - 如果不等于5,说明
i
被其它线程改过了(比如现在i
的值为2),那么我就什么也不做,此次CAS失败,i
的值仍然为2。
在这个例子中,i就是V,5就是A,6就是B。
那有没有可能我在判断了i
为5之后,正准备更新它的新值的时候,被其它线程更改了i
的值呢?
不会的。因为CAS是一种原子操作,它是一种系统原语,是一条CPU的原子指令,从CPU层面保证它的原子性
当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
CAS 的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环 CAS 就是在一个循环里不断的做 cas 操作,直到成功为止。
CAS 是怎么实现线程的安全呢?
我们将其交给硬件 — CPU 和内存,利用 CPU 的多处理能力,实现硬件层面的阻塞,再加上 volatile 变量的特性即可实现基于原子操作的线程安全。
CAS的原理
- 利用了现代处理器都支持的CAS的指令, 循环这个指令,直到成功为止
CAS算法的好处
CAS 是一种无锁算法,通过硬件层面上对先后操作内存的线程进行排队处理,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS(比较并交换)是CPU指令级的操作,只有一步原子操作,所以非常快。而且避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了
CAS 实现原子操作的三大问题
1、ABA 问题
CAS 在操作的时候会检查变量的值是否被更改过,如果没有则更新值,但是带来一个问题,最开始的值是A,接着变成B,最后又变成了A。经过检查这个值确实没有修改过,因为最后的值还是A,但是实际上这个值确实已经被修改过了。为了解决这个问题,在每次进行操作的时候加上一个版本号,每次操作的就是两个值,一个版本号和某个值,A——>B——>A问题就变成了1A——>2B——>3A。在 jdk 中提供了 AtomicStampedReference
类解决ABA问题,用Pair这个内部类实现,包含两个属性,分别代表版本号和引用。
这个类的 compareAndSet
方法的作用是首先检查当前引用是否等于预期引用,并且检查当前版本号标志是否等于预期版本号标志,如果二者都相等,才使用CAS设置为新的值和标志。
2、循环时间长开销大
自旋 CAS 如果长时间不成功,会占用大量的 CPU 资源,给 CPU 带来非常大的执行开销。
解决思路是让 JVM 支持处理器提供的 pause 指令。
pause 指令能让自旋失败时 cpu 睡眠一小段时间再继续自旋,从而使得读操作的频率低很多,为解决内存顺序冲突而导致的CPU流水线重排的代价也会小很多。
3、只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比 如,有两个共享变量 i=2,j=a,合并一下 ij=2a,然后用 CAS 来操作 ij。从 Java 1.5 开始,JDK 提供了 AtomicReference
类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行 CAS 操作。
解决方案:
- 使用JDK 1.5开始就提供的
AtomicReference
类保证对象之间的原子性,把多个变量放到一个对象里面进行CAS操作; - 使用锁。锁内的临界区代码可以保证只有当前线程能操作。
jdk 中相关原子操作类的使用
- 更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong
- 更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
- 更新引用类型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
- 原子更新字段类: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
AtomicInteger
- int addAndGet(int delta):以原子方式将输入的数值与实例中的值 (AtomicInteger 里的 value)相加,并返回结果。
- boolean compareAndSet(int expect, int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
- int getAndIncrement():以原子方式将当前值加 1,注意,这里返回的是自增前的值。
- int getAndSet(int newValue):以原子方式设置为 newValue 的值,并返回旧值。
实例:
/**
* 类说明:演示基本类型的原子操作类
*/
public class UseAtomicInt {
static AtomicInteger ai = new AtomicInteger(10);
public static void main(String[] args) {
// ai.getAndIncrement();
// ai.incrementAndGet();
ai.compareAndSet(10, 12);
// ai.addAndGet(24);
System.out.println(ai.get());
// System.out.println(value[0]);//原数组不会变化
}
}
打印结果:12。
ai.compareAndSet(10, 12); 改为 ai.compareAndSet(11, 12);时,打印结果:10
AtomicIntegerArray
主要是提供原子的方式更新数组里的整型,其常用方法如下。
- int addAndGet(int i,int delta):以原子方式将输入值与数组中索引 i 的元素相加。
- boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置 i 的元素设置成 update 值。
需要注意的是,数组 value 通过构造方法传递进去,然后 AtomicIntegerArray 会将当前数组复制一份,所以当 AtomicIntegerArray 对内部的数组元素进行修改时,不会影响传入的数组。
实例
/**
* 类说明:演示数组的原子操作类
*/
public class AtomicArray {
static int[] value = new int[]{1, 2};
static AtomicIntegerArray ai = new AtomicIntegerArray(value);
public static void main(String[] args) {
ai.getAndSet(0, 3);
System.out.println(ai.get(0));
System.out.println(value[0]);//原数组不会变化
}
}
打印结果:
3
1
更新引用类型
原子更新基本类型的 AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic 包提供了以下 3 个类。
**AtomicReference **
原子更新引用类型。
实例:
/**
*类说明:演示引用类型的原子操作类
*/
public class UseAtomicReference {
static AtomicReference atomicUserRef;
public static void main(String[] args) {
UserInfo user = new UserInfo("Mark", 15);//要修改的实体的实例
atomicUserRef = new AtomicReference(user);
UserInfo updateUser = new UserInfo("Bill",17);
atomicUserRef.compareAndSet(user,updateUser);
System.out.println(atomicUserRef.get());
System.out.println(user);
}
//定义一个实体类
static class UserInfo {
private volatile String name;
private int age;
public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "UserInfo{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
打印结果:
UserInfo{name='Bill', age=17}
UserInfo{name='Mark', age=15}
AtomicStampedReference
利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在 ABA 问题了。这就是 AtomicStampedReference 的解决方案。AtomicMarkableReference 跟 AtomicStampedReference 差不多,AtomicStampedReference 是使用 pair 的 int stamp 作为计数器使用,AtomicMarkableReference 的 pair 使用的是 boolean mark。 AtomicStampedReference 可能关心的是动过几次, AtomicMarkableReference 关心的是有没有被人动过,方法都比较简单。
实例:
/**
* 类说明:演示带版本戳的原子操作类
*/
public class UseAtomicStampedReference {
static AtomicStampedReference asr
= new AtomicStampedReference("mark", 0);
public static void main(String[] args) throws InterruptedException {
//拿到当前的版本号(旧)
final int oldStamp = asr.getStamp();
final String oldReference = asr.getReference();
System.out.println(oldReference + "============" + oldStamp);
Thread rightStampThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":当前变量值:"
+ oldReference + "-当前版本戳:" + oldStamp + "-"
+ asr.compareAndSet(oldReference,
oldReference + "+Java", oldStamp,
oldStamp + 1));
}
});
Thread errorStampThread = new Thread(new Runnable() {
@Override
public void run() {
String reference = asr.getReference();
System.out.println(Thread.currentThread().getName()
+ ":当前变量值:"
+ reference + "-当前版本戳:" + asr.getStamp() + "-"
+ asr.compareAndSet(reference,
reference + "+C", oldStamp,
oldStamp + 1));
}
});
rightStampThread.start();
rightStampThread.join();
errorStampThread.start();
errorStampThread.join();
System.out.println(asr.getReference() + "============" + asr.getStamp());
}
}
打印结果:
mark============0
Thread-0:当前变量值:mark-当前版本戳:0-true
Thread-1:当前变量值:mark+Java-当前版本戳:1-false
mark+Java============1
AtomicMarkableReference
原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是 AtomicMarkableReference(V initialRef,boolean initialMark)。
原子更新字段类
如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类, Atomic 包提供了以下 3 个类进行原子字段更新。 要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类, 每次使用的时候必须使用静态方法 newUpdater() 创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段(属性)必须使用 public volatile 修饰符。
AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
AtomicReferenceFieldUpdater: 原子更新引用类型里的字段。