java主流锁大致有以下几种:
1. 乐观锁VS悲观锁
从概念上讲
乐观锁:在使用数据的时候默认其他线程不会同时修改数据,所以不加锁。只有在修改数据之前判断该数据之前有无更新,没有就继续修改数据。
乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
悲观锁:在获取数据时加锁,确保线程安全。
Java中,synchronized关键字和Lock的实现类都是悲观锁。
应用场景
- 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
代码实现
CAS介绍
Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。
CAS算法涉及到三个操作数:需要读写的内存值 V ||进行比较的值 A ||要写入的新值 B。
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作。
CAS的弊端:
(1)ABA问题:当需要进行比较的值A,由A->B->A时,CAS算法检查时,无法发现其改变过的。补救措施,跟个时间戳。
(2)循环时间长开销大
(3)只能保证一个共享变量的原子操作
2.公平锁VS非公平锁
从概念上讲
公平锁:多个线程按照申请锁的顺序,排队获取锁。先申请先获取。优点是所有线程都能得到锁,不会饿死。缺点是相对于非公平锁,整体吞吐效率低,CPU唤醒阻塞线程的开销大。
非公平锁:多个线程加锁时,会尝试获取锁。获取不到会去排队等待,若此时锁正好可用,便无阻塞获取锁。优点是减少了唤醒线程的开销,整体吞吐效率变高。缺点是线程有可能会饿死或很久之后才获得锁。
从代码实现上看
以ReentrantLock
的源码为例:
内部类
Sync
继承AQS(AbstractQueuedSynchronizer
),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync
和非公平锁NonfairSync
两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。
接下来看一下公平锁与非公平锁的加锁方法的源码:
公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:
hasQueuedPredecessors()
。深入了解:该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。
综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。
3.可重入锁VS非可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。优点一定程度上避免了死锁。举例:
在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。
如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。
可重入锁的实现原理
ReentrantLock和synchronized都是重入锁,那么我们通过重入锁ReentrantLock以及非可重入锁NonReentrantLock的源码来对比分析一下为什么非可重入锁在重复调用同步资源时会出现死锁。
ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。
当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。
释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。
4.其他
自旋锁 VS 适应性自旋锁
无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
独享锁 VS 共享锁
5.结语
限于时间精力以及个人水平,还有一些锁不再做深入探究。java中的锁已经做了很好的封装,熟悉各种锁的实现原理,在不同场景下选择不同的锁。