多线程操作共享资源时,会出现三个问题:可见性、有序性以及原子性。
乐观锁
乐观锁: 假设不会发生并发冲突,只有在最后更新共享资源的时候会判断一下在此期间有没有别的线程修改了这个共享资源。如果发生冲突就重试,直到没有冲突,更新成功。CAS就是一种乐观锁实现方式。
悲观锁会阻塞其他线程。乐观锁不会阻塞其他线程,如果发生冲突,采用死循环的方式一直重试,直到更新成功。
CAS
就是一种乐观锁的实现
compare and swap
如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,返回true。否则处理器不做任何操作,返回false。
比如当前线程比较成功后,准备更新共享变量值的时候,这个共享变量值被其他线程更改了,那么CAS函数必须返回false。
Unsafe类
java中提供了Unsafe类,它提供了三个函数,分别用来操作基本类型int和long,以及引用类型Object
public final native boolean compareAndSwapObject
(Object obj, long valueOffset, Object expect, Object update);
public final native boolean compareAndSwapInt
(Object obj, long valueOffset, int expect, int update);
public final native boolean compareAndSwapLong
(Object obj, long valueOffset, long expect, long update);
参数的意义:
- obj 和 valueOffset:表示这个共享变量的内存地址。这个共享变量是obj对象的一个成员属性,valueOffset表示这个共享变量在obj类中的内存偏移量。所以通过这两个参数就可以直接在内存中修改和读取共享变量值。
- expect: 表示预期原来的值。
- update: 表示期待更新的值。
getAndAddInt
public final int getAndAddInt(Object obj, long valueOffset, int var) {
int expect;
// 利用循环,直到更新成功才跳出循环。
do {
// 获取value的最新值
expect = this.getIntVolatile(obj, valueOffset);
// expect + var表示需要更新的值,如果compareAndSwapInt返回false,说明value值被其他线程更改了。
// 那么就循环重试,再次获取value最新值expect,然后再计算需要更新的值expect + var。直到更新成功
} while(!this.compareAndSwapInt(obj, valueOffset, expect, expect + var));
// 返回当前线程在更改value成功后的,value变量原先值。并不是更改后的值
return expect;
}
i++,自增的过程包含了读改写,非原子操作。在改的过程时, 其他cpu也许会更改共享变量的值,这时需要CAS操作来试探是否更改,如果发现更改,那么当前i+1 并非我们希望set的值,需要重新计算...
AtomicInteger类
- 成员变量和静态代码块
private 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; // 使用volatile修饰,解决了可见性和有序性问题。
- value为AtomicInteger对象的值
- valueOffset为value在对象内存空间的偏移
- unsafe 一些native方法,通过它实现CAS操作,因为共享变量是int类型,所以调用compareAndSwapInt方法。
value是一个volatile变量,在内存中可见,任何线程都不允许对其进行拷贝,因此JVM可以保证任何时刻任何线程总能拿到该变量的最新值。
static中valueOffset=unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
通过unsafe 获取valueOffset(对象内存偏移量)
- get/set
// 直接读取。因为是volatile关键子修饰的,总是能看到(任意线程)对这个volatile变量最新的写入
public final int get() {
return value;
}
// 直接写入。因为是volatile关键子修饰的,所以它修改value变量也会立即被别的线程读取到。
public final void set(int newValue) {
value = newValue;
}
- compareAndSet
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
- 如果value变量的当前值(内存值)等于期望值(expect),那么就把update赋值给value变量,返回true。
- 如果value变量的当前值(内存值)不等于期望值(expect),就什么都不做,返回false。
- 这个就是CAS操作,使用unsafe.compareAndSwapInt方法,保证整个操作过程的原子性
ABA问题
CAS通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。
内存地址(c++)
线程 1 从内存位置V中取出A,A指向内存位置W。
线程 2 从位置V中取出A。
线程 2 进行了一些操作,释放了A指向的内存。
线程 2 重新申请内存,并恰好申请了内存位置W,将位置W存入C的内容。
线程 2 将内存位置W写入位置V。
线程 1 进行CAS操作,发现位置V中仍然是A指向的即内存位置W,操作成功
内存管理机制中广泛使用的内存重用机制,发现内存地址还是那个旧的指针变量,跟期待的值一样,则CAS操作成功,但实际指针变量的所指向内存空间里的值发生了改变普通数据
进程P1读取了一个数值A
P1被挂起(时间片耗尽、中断等),进程P2开始执行
P2修改数值A为数值B,然后又修改回A
P1被唤醒,比较后发现数值A没有变化,程序继续执行。
解决
对于内存重用机制导致ABA问题,java的垃圾回收机制帮我们解决了;
而第二个问题,加入版本号即可解决