Java中各类锁的概述
1、悲观锁、乐观锁
乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度;
悲观锁:对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
Integer a = 1;
synchronized (Test.class) {
a++;
}
Integer b = 1;
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
b++;
} finally {
lock.unlock();
}
乐观锁:在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作。java中,乐观锁是通过无锁的形式去实现的,比较常用的就是CAS算法;CAS算法的实现,主要是在java.util.concurrent.atomic
包下的Atomic
系列的原子类。
AtomicInteger atomicInteger = new AtomicInteger();
int i = atomicInteger.getAndIncrement();
CAS
:CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。
CAS算法涉及到三个操作数:
- 需要读写的内存值 V。
- 进行比较的值 A。
- 要写入的新值 B。
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。
CAS虽然很高效,但是它也存在三大问题:
- ABA问题:线程把数据 A–>B,又从B–>A,另外的线程拿到数据的时候,发现还是A数据,以为数据没有改变,其实数据已经改变了;解决方式是,数据增加一个版本号。
- 循环时间长开销大:CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销;
- 只能保证一个共享变量的原子操作:多个共享变量的原子操作,可以使用
AtomicReference
类,对引用类型的对象,做原子操作;
2、自旋锁
自旋锁:顾名思义,就是在while循环中,不断重试获取锁;线程的阻塞或者唤醒,都是需要操作系统切换CPU的来完成的;这种状态转换需要耗费处理器时间,如果同步代码块的执行内容比较简单,让线程去挂起和恢复,可能得不偿失,所以需要系统自旋
一下,获取到锁;
自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。
如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。
所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
CAS
的操作,其实就是一个自旋锁的过程。
3、公平锁、非公平锁
公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
优点:等待锁的线程不会饿死。
缺点:整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大;
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
非公平锁:非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
优点:是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。
缺点:是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况;详细可以查看ReentrantLock
源码实现。默认为非公平锁,有参构造,传入true
为公平锁
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
4、可重入锁、不可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。
Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
static Object lock = new Object();
public static void main(String[] args) {
synchronized (lock) {
System.out.println("第一层锁");
synchronized (lock) {
System.out.println("第二层锁");
}
}
ReentrantLock reentrantLock = new ReentrantLock();
try {
reentrantLock.lock();
reentrantLock.lock();
reentrantLock.unlock();
} finally {
reentrantLock.unlock();
}
}
需要注意的是,如果使用ReentrantLock
加锁,加锁的次数,跟解锁的次数要一致,不然会导致资源阻塞,得不到释放;