01 乐观锁和悲观锁介绍?
乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。
因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。
因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
锁是针对多线程状态下保护数据的手段.
乐观锁的实现方式主要有两种:CAS机制和版本号机制
CAS操作包括了3个操作数:
1) 需要读写的内存位置(V)
2) 进行比较的预期值(A)
3) 拟写入的新值(B)
悲观锁的实现方式主要有synchronized
悲观锁进入synchronized修饰的方法后会给对象加锁,通过管程保证方法完成后才会有新的线程进入该方法
public class Test {
//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() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
value1++;
value2.getAndIncrement();
increaseValue3();
}
}).start();
}
//查看活跃线程 ,因守护线程的原因[基于工具问题windows:idea run 启动用 >2,debug 用>1]
while (Thread.activeCount() > 2) {
//Thread.currentThread().getThreadGroup().list();
Thread.yield();//让出cpu
}
//打印结果
Thread.sleep(1000);
System.out.println("线程不安全:" + value1);
System.out.println("乐观锁(AtomicInteger):" + value2);
System.out.println("悲观锁(synchronized):" + value3);
}
}
运行程序,使用1000个线程同时对value1、value2和value3进行自增操作,可以发现:value2和value3的值总是等于1000,而value1的值常常小于1000。
上面的代码中:
首先来介绍AtomicInteger。
AtomicInteger是java.util.concurrent.atomic包提供的原子类,利用CPU提供的CAS操作来保证原子性;
除了AtomicInteger外,还有AtomicBoolean、AtomicLong、AtomicReference等众多原子类。
下面看一下AtomicInteger的源码,了解下它的自增操作getAndIncrement()是如何实现的(源码以Java7为例,Java8有所不同,但思想类似)。
public class AtomicInteger extends Number implements java.io.Serializable {
//存储整数值,volatile保证可视性
private volatile int value;
//Unsafe用于实现对底层资源的访问
private static final Unsafe unsafe = Unsafe.getUnsafe();
//valueOffset是value在内存中的偏移量
private static final long valueOffset;
//通过Unsafe获得valueOffset
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
}
下面是unsafe.compareAndSwapInt方法在C++上面的实现
UNSAFE\_ENTRY(jboolean, Unsafe\_CompareAndSwapObject(JNIEnv \*env, jobject unsafe, jobject obj, jlong offset, jobject e\_h, jobject x\_h))
UnsafeWrapper("Unsafe\_CompareAndSwapObject");
oop x = JNIHandles::resolve(x\_h); //待更新的新值,也就是UpdateValue
oop e = JNIHandles::resolve(e\_h); //期望值,也就是ExpectValue
oop p = JNIHandles::resolve(obj); //待操作对象
HeapWord\* addr = (HeapWord \*)index\_oop\_from\_field\_offset\_long(p, offset);//根据操作的对象和其在内存中的offset,计算出内存中具体位置
oop res = oopDesc::atomic\_compare\_exchange\_oop(x, addr, e, true);// 如果操作对象中的值和e期望值一致,则更新存储值为x,反之不更新
jboolean success = (res == e);
if (success) //满足更新条件
update\_barrier\_set((void\*)addr, x); // 更新存储值为x
return success;
UNSAFE\_END
版本号机制 使用版本号机制实现乐观锁
版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。
- 当某个线程查询数据时,将该数据的版本号一起查出来;
- 当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。
02 乐观锁加锁吗?
下面是我对这个问题的理解:
- 乐观锁本身是不加锁的,只是在更新时判断一下数据是否被其他线程更新了;AtomicInteger便是一个例子。
- 有时乐观锁可能与加锁操作合作,例如,在前述updateCoins()的例子中,MySQL在执行update时会加排它锁。
但这只是乐观锁与加锁操作合作的例子,不能改变“乐观锁本身不加锁”这一事实。
03 CAS有哪些缺点?
下面是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问题的。
2.高竞争下的开销问题 在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。
针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。
当然,更重要的是避免在高竞争环境下使用乐观锁。
3.功能限制 CAS的功能是比较受限的,例如CAS只能保证单个变量(或者说单个内存值)操作的原子性,这意味着:
(1)原子性不一定能保证线程安全,例如在Java中需要与volatile配合来保证线程安全;
(2)当涉及到多个变量(内存值)时,CAS也无能为力。
除此之外,CAS的实现需要硬件层面处理器的支持,在Java中普通用户无法直接使用,只能借助atomic包下的原子类使用,灵活性受到限制。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。