1、Lock接口的实现——并发包锁
(1)ReentrantLock
重入锁,重入锁指线程在获得锁之后,再次获得该锁不需要阻塞,而是直接关联一次计数器增加重入次数。
(2)ReentrantReadWriteLock
重入读写锁,实现了ReadWriteLock接口,它维护两个锁,一个ReadLock,一个WriteLock,这两个都实现了Lock。基本原则是:读和读不互斥,读和写互斥,写和写互斥。适合读多写少的场景。
(3)StampedLock
jdk1.8引入的新的锁机制,可以认为是对ReentrantReadWriteLock的改进版本。解决大量的读线程存在,可能会引起写线程的饥饿的问题。stampedLock是一种乐观的读策略,使得乐观锁完全不会阻塞写线程。
2、ReentrantLock的实现原理
重入锁的设计目的:比如调用demo方法获得了当前的对象锁,然后在这个方法中再去调用 demo2,demo2中的存在同一个实例锁,这个时候当前线程会因为无法获得 demo2的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死锁。
关系图:
(1)AQS
全称 AbstractQueuedSynchronizer,是实现Lock的核心组件。从功能层面分为两种:独占和共享。
* 独占锁,ReentrantLock就是以独占方式实现的互斥锁。
* 共享锁,如ReentrantReadWriteLock。
AQS内部实现:AQS队列内部维护了一个FIFO的双向链表,双向链表可以从任意一个节点访问很方便地访问前驱和后继节点(非公平锁很好地利用了这一点特性),每个节点(Node)由线程和等待状态封装而成,当线程争抢锁是被,会被封装成Node添加到链表末端。
Node节点:
static final class Node { static final Node SHARED = new Node(); static final Node EXCLUSIVE = null; static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3; volatile int waitStatus; volatile Node prev; //前驱节点 volatile Node next; //后继节点 volatile Thread thread;//当前线程 Node nextWaiter; //存储在condition队列中的后继节点 //是否为共享锁 final boolean isShared() { return nextWaiter == SHARED; } final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { // Used to establish initial head or SHARED marker } //将线程构造成一个Node,添加到等待队列 Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; } //这个方法会在Condition队列使用,后续单独写一篇文章分析condition Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; } }
(2)结合AQS看ReentrantLock实现原理
假设ThreadA、B、C三个线程同时访问一个同步块:
如果ThreadA通过CAS获得了锁,此时state=1,exclusiveOwnerThread=ThreadA。ThreadB和ThreadC会被封装成Node节点,形成双向链表。
(3)公平锁和非公平锁(默认)
FairSync在尝试获取锁时,多了一个hasQueuePredecessors判断,也就是如果当前线程(Node)有前置节点,则不会进行CAS去竞争锁,NonFairSync则不会做这个检查,直接通过CAS去竞争锁。
(4)ReentrantLock偏向锁的巧妙实现
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
nonfairTryAcquire方法是lock方法间接调用的第一个方法,每次请求锁时都会首先调用该方法。该方法会首先判断当前状态,如果c==0说明没有线程正在竞争该锁,如果c !=0 说明有线程正拥有了该锁,但如果发现是自己拥有该锁的话,只是简单地++acquires,并修改status值,但因为没有竞争,所以通过setStatus修改,而非CAS,也就是说这段代码实现了偏向锁的功能。
(5)ReentrantLock和synchronized区别
* AbstractQueuedSynchronizer通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过CAS操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。
* synchronized的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程的出列速度;当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优化,而Lock则完全依靠系统阻塞挂起等待线程。
* Lock比synchronized更适合在应用层扩展,可以继承AbstractQueuedSynchronizer定义各种实现,比如实现读写锁(ReadWriteLock),公平或不公平锁;同时,Lock对应的Condition也比wait/notify要方便的多、灵活的多。
3、Synchronized与ReentrantLock实现原理有何不同?
锁的实现原理基本是为了达到一个目的:让所有的线程都能看到某种标记。Synchronized通过在对象头中设置标记实现了这一目的,是一种JVM原生的锁实现方式,而ReentrantLock以及所有的基于Lock接口的实现类,都是通过用一个volitile修饰的int型变量,以此来保证每个线程都能拥有对该int变量的可见性和原子修改,其本质是基于AQS框架。
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
static final class Node {
private volatile int state;
}
推荐文章:
1、深入剖析ReentrantLock:https://mp.weixin.qq.com/s/XMsFNCB0m7eTlH56ipZL7A
2、AQS源码分析:https://mp.weixin.qq.com/s/6Znq_SDZwzwifIIUSBRcMg