乐观锁和悲观锁
乐观锁和悲观锁是一种概念,他们的区别主要是在对待线程同步时的态度。
- 悲观锁认为自己在使用数据时一定存在其他线程在修改数据,所以它在使用数据前会先加上锁,待到使用完毕释放锁资源。Java中,
synchronized
关键字和Lock
的实现类都属于悲观锁。 - 反之乐观锁则认为在使用数据时不会有线程修改数据,所以它不会添加锁,只是在更新数据时判断是否有线程修改了数据。如果数据没有被更新,则当前线程成功将数据写入。如果数据被更新了,则会根据实现方式不同执行不同的处理(报错 or 重试)。Java中,最常见的乐观锁实现就是CAS原子类。
正是因为乐观锁和悲观锁的不同,他们所适用的场景自然不一样,
- 乐观锁适合于读多写少的场景,无锁的设计能大幅提高并发效率。
- 悲观锁则适合写多读少场景,使用前先加锁能保证数据安全。
使用
//=============== 悲观锁 ===============
//synchronized
public synchronized void test() {
//需要同步的资源
}
/**
* ReentrantLock
* 需要保证多线程操作的是同一个锁
*/
ReentrantLock lock = new ReentrantLock();
public void test1() {
lock.lock();
try {
//需要同步的资源
} finally {
lock.unlock();
}
}
//=============== 乐观锁 ===============
/**
* 需要保证多线程操作的是同一个AtomicInteger
*/
AtomicInteger atomicInteger = new AtomicInteger(0);
public void test2() {
atomicInteger.getAndIncrement();
}
通过上述使用方式我们可以总结出,悲观锁都是通过显式调用去获取锁从而同步数据,但是为什么乐观锁不需要显示的获取锁也同样能同步数据呢。这里就要谈谈什么是CAS。
CAS (compare and swap)
从字面意思上看,即比较和交换,是一种无锁的算法。即可以在不需要加锁的情况下,实现多线程变量同步。在Java中,atomic
包下的原子类们是CAS的一系列实现
其中,我们就以最常见的AtomicInteger分析,源码如下,
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 {
//反射获取AtomicInteger类中value值的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//通过volatile关键字防止cpu指令重排序
//使value对所有线程可见
private volatile int value;
...
public final int getAndIncrement() {
//实际调用的是Unsafe.getAndAddInt
return unsafe.getAndAddInt(this, valueOffset, 1);
}
}
//Unsafe类
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
//通过循环重试比较新值与旧值,直到两者相等说明此时数据未被其他线程修改,之后更新内存中的变量值
do {
var5 = this.getIntVolatile(var1, var2);
//compareAndSwapInt这个方法是native方法具体分析见底下
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
//native方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
可见实际的CAS操作的实现是在native层的compareAndSwapInt()
中,JNI里是借助于CPU指令cmpxchg
完成的,该指令是一个原子操作。显然,可以保证变量的可见性。
具体CPU的cmpxchg
指令做的事情是,比较寄存器中的A和内存中的值V。
- 如果相等,把要写入的新值 B 存入内存中。
- 如果不相等,将内存值 V 赋值给寄存器中的值 A。
之后通过上述的do-while
循环再次调用cmpxchg
指令进行重试,一直到更新成功为止。
CAS带来的问题
CAS这种算法虽然非常高效,但也存在问题。
- ABA问题,因为CAS在更新变量前需要先检查变量是否可以更新,此时如果将变量A更新成B随后立马又更新成A。那么显然存在一种情况导致CAS认为变量没有变化,但实际是有变化的(线程安全策略变得不可靠)。解决办法可以将变量每次的更新记录一个版本号,即1A-2B-3A,这样CAS做compare的时候就不会出现变量已更新却被误判为未更新的情况了。
- 循环策略导致CPU开销高。
公平锁和非公平锁
- 公平锁,线程按照申请锁的顺序来持有锁。优点是等待的线程不会饥饿,但缺点是吞吐效率比非公平锁下降。除了获取锁的线程,其余线程处于阻塞状态,而且CPU做线程唤醒的开销很大。
- 非公平锁,线程获取锁是无序的,存在线程插队获取到锁的情况。优点是吞吐效率高,因为线程有几率不被阻塞就获取到了锁,但缺点可能会导致线程一直等待,处于饥饿状态。
ReentrantLock中的公平锁与非公平锁
public class ReentrantLock implements Lock, java.io.Serializable {
...
public ReentrantLock() {
//可见ReentrantLock默认使用的是非公平锁
sync = new NonfairSync();
}
...
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//非公平锁的实现
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
...
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//公平锁的实现
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
...
}
protected final boolean tryAcquire(int acquires) {
...
}
}
}
我们观察到实际获取锁的逻辑在tryAcquire
方法中,我们对NonfairSync
和FairSync
中(左为FairSync
)该方法做横向比较来看看他们的区别是什么,
除了增加了hasQueuedPredecessors
以外没有什么不同,
public final boolean hasQueuedPredecessors() {
...
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
该方法主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。
可重入锁和不可重入锁
- 可重入锁,在外层方法获取了锁后,如果内部调用的方法也需要获取锁,那么会自动获取(必须为同一个锁)。不会因为外层方法获取到的锁没有被释放掉而被阻塞。可重入锁的特点就是可以在一定程度上避免产生死锁。
- 同理不可重入锁则不允许出现上述情况,比如不能使用它做递归操作。
独享锁和共享锁
- 独享锁又名互斥锁。该锁一次只能被一个线程所持有,获得锁的线程能同时进行读写操作。Java中
synchronized
和Lock
的实现类都属于互斥锁。 - 共享锁,该锁可以被多个线程持有,如果一个变量A被线程加了共享锁,则之后的线程也只能加共享锁。并且获得共享锁的线程只能读,不能写。
Java中ReentrantReadWriteLock
类实现了互斥锁与共享锁,如下
ReentrantReadWriteLock
有两把锁,ReadLock读锁,是共享锁,WriteLock写锁,是互斥锁。