1. 公平锁 vs 非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。类似排队打饭,先来后到。
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
比较
公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照 FIFO 的规则从队列中取到自己。
非公平锁,比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁的方式。
公平锁的优点是等待锁的线程不会饿死。
非公平锁的优点在于吞吐量比公平锁大。但在高并发的情况下,有可能会造成优先级反转或饥饿现象。
内窥
并发包中 ReentrantLock
的创建可以指定构造函数的 boolean
类型来得到公平锁或非公平锁,默认为非公平锁。
查看 ReentrantLock
,可以看到有一个继承自 AbstractQueuedSynchronizer
的内部类 Sync
,添加锁和释放锁的大部分操作实际上都是在 Sync
中实现的。它有公平锁 FairSync
和非公平锁 NonfairSync
两个子类。
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
private final Sync sync;
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
//......
}
static final class NonfairSync extends Sync {
//......
}
static final class FairSync extends Sync {
//......
}
}
两个构造方法对比,可以看出公平锁和非公平锁的区别
- 非公平锁在调用 lock() 后,首先就会通过 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了,否则按公平锁的方式去排队,进入到阻塞队列等待唤醒
- 公平锁在获取同步状态(获取锁)时 tryAcquire() 多了一个限制条件:
!hasQueuedPredecessors()
,用来判断当前线程是否位于同步队列中的第一个
Synchronized关键字,也是一种非公平锁。
2. 乐观锁 VS 悲观锁
乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在 Java 和数据库中都有此概念对应的实际应用。
-
悲观锁是一种悲观思想,它总认为自己在使用数据的时候一定有别的线程来修改,所以悲观锁在持有数据的时候总会把资源或数据锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。传统的关系型数据库里边就用到了很多这种锁机制,**比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。**悲观锁的实现往往依靠数据库本身的锁功能实现。
Java 中,synchronized 关键字和 Lock 的实现类都是悲观锁。
-
而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)
乐观锁的实现方案一般来说有两种:
版本号机制
和CAS实现
。Java 中
java.util.concurrent.atomic
包下面的原子变量类的递增操作就是通过 CAS 实现了乐观锁。
比较
悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
悲观锁比较适合强一致性的场景,但效率比较低,特别是读的并发低。乐观锁则适用于读多写少,并发冲突少的场景。
乐观锁常见问题:
- ABA 问题
- 循环时间长开销大
- 只能保证一个共享变量的原子操作
3. 可重入锁(递归锁)
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。
也就是说,线程可以进入任何一个它已经拥有的锁同步着的代码块。
可重入锁的最大作用是可一定程度避免死锁,ReentrackLock
、Synchronized
就是典型的可重入锁。
public class Widget {
public synchronized void doSomething() {
System.out.println("方法1执行...");
doOthers();
}
public synchronized void doOthers() {
System.out.println("方法2执行...");
}
}
在上面的代码中,类中的两个方法都是被内置锁 synchronized 修饰的,doSomething() 方法中调用 doOthers() 方法。因为内置锁是可重入的,所以同一个线程在调用 doOthers() 时可以直接获得当前对象的锁,进入doOthers() 进行操作。
如果是一个不可重入锁,那么当前线程在调用 doOthers() 之前需要将执行 doSomething() 时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。
自旋锁
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU
public class SpinLockDemo {
AtomicReference<Thread> lock = new AtomicReference<>();
public void myLock(){
Thread thread = Thread.currentThread();
//如果不为空,自旋
while (!lock.compareAndSet(null,thread)){
}
}
public void myUnlock(){
Thread thread = Thread.currentThread();
//解锁后,将锁置为 null
lock.compareAndSet(thread,null);
}
}
优缺点)优缺点
缺点:
- 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU 使用率极高。
- 上面 Java 实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
优点:
- 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是 active 的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
- 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核&#