java多线程---ReentrantLock源码分析

ReentrantLock源码分析

基础知识复习

synchronized和lock的区别

  1. synchronized是非公平锁,无法保证线程按照申请锁的顺序获得锁,而Lock锁提供了可选参数,可以配置成公平锁,也可以配置成非公平锁。通常来说,非公平锁的效率比公平锁要高。
  2. 一个线程使用syn获取锁,除非该线程成功获取到锁,否则将一直阻塞住。而Lock锁提供了lockInterruptibly()接口,提供了可中断的操作
  3. 带超时时间的锁。Lock锁提供了tryLock(long time, TimeUnit unit)带超时时间的获取锁的接口,在等待指定时间后,如果获取不到锁,则放弃获取锁
  4. 自动释放锁。如果用syn加锁,当发生异常时(比方运行时异常),那么jvm会自动释放掉线程持有的锁,而lock锁则不会主动释放,除非调用了unlock接口,因此使用lock锁时有可能导致死锁
  5. 在ReentrantLock上可以绑定多个Condition条件,也就是可以拥有多个等待队列,比如在实现生产者消费者的时候,使用一个队列(锁的队列)存放等待 队列(生产者消费者的队列)有元素的消费者,使用另一个队列(锁的队列)存放等待 队列not full的生产者,相比较synchronized和wait notify而言,避免了错误的唤醒生产者或者消费者的开销

ReentrantLock 基础分析

ReentrantLock是lock的一个实现类,独占锁。首先看一下ReentrantLock的内部属性

private final Sync sync;

发现在只有一个Sync类型的属性,这个Sync是AQS的一个抽象类如下:

abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        abstract void lock();

        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;
        }

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
        ....

同时在ReentrantLock 内部,实现了Sync2个不同的类,一个是NonfairSync(非公平锁),一个是FairSync(公平锁)。也就是在一开头,synchronized和lock的区别的第一点,Lock可以创建2种不同的锁,根据传入的参数。
如下源码

//默认的ReentrantLock无参构造函数,是非公平锁
 public ReentrantLock() {
        sync = new NonfairSync();
    }

    //当传入fasle时,就创建了公平锁。
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

AbstractQueuedSynchronizer

那么Sync的父类是AQS,并发包中的锁底层就是使用了AQS:AbstractQueuedSynchronizer。所以来看一下AQS,那么首先来看下属性:

//这个是父类AbstractOwnableSynchronizer中的属性,标识了独占模式下获取锁的是哪一个线程
 private transient Thread exclusiveOwnerThread;
 
 
 //AQS的数据结构是FIFO的双向队列
 //头部节点
 private transient volatile Node head;
 
 //尾部节点
 private transient volatile Node tail;
 //同步状态值
 private volatile int state;
 
 //设置这里使用CAS来更新 主要用于对state的更新
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long stateOffset;
    private static final long headOffset;
    private static final long tailOffset;
    private static final long waitStatusOffset;
    private static final long nextOffset;
 

可以看到有头部节点和尾部节点外,还有一个同步状态值state,该属性在不同的锁中,代表了不同的含义。

  • 在ReentrantLock中,state可以用来表示当前线程获取锁的可重入次数
  • 对与ReentrantReadWriteLock来说,高16位代表了读锁,也就是读锁的次数,低16位代表了写锁的可重入次数
  • 在Semaphore中,state代表了可用信号的个数
  • 在countdownlatch中,state用来表示计数器当前的值

所以在ReentrantLock 中 state用来表示当前线程获取锁的可冲入次数。当没有线程持有锁的时候,state为0,当有一个线程获取锁时,state为1。然后当前线程需要再次获取锁时,发现自己已经是锁的持有者,state+1

看下AQS的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;

在已知队列中的节点是这样的结构情况下,来看下何如进行获取资源

//根据源码中的英文来翻译,就是获取独占资源时调用该方法,
//也就是使用tryAcquire来改变state的值,如果失败就会把线程封装成Node.EXCLUSIVE放入队列尾部并挂起
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

//其中 tryAcquire是在实现类中具体实现的

对应的释放资源

//tryRelease是在实现类中具体实现的方法,如果成功释放资源,则激活队列中的一个线程,也就是头部节点的线程。
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

以上2个release和acquire,的最常见的实现例子就是Lock中的unlock和lock。但是在AQS源码中还存在着一个类似的方法,acquireInterruptibly ,这个方法和acquire功能一样,但是带Interruptibly 的方法,就是可以对中断进行响应。如果该线程在等待过程中被中断了,那么带Interruptibly 的方法就会抛出异常,也会终端。否则不会。对应了一开始的第二条,lockInterruptibly()可以对中断进行响应。

再来看一下在AQS中,是怎么进行加入队列操作的。

//有2种方式,如果尾部节点不为空,就直接加入封装好的node,为空则调用enq()加入队列
private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
            //加入一个空的node为哨兵节点
                if (compareAndSetHead(new Node()))
                
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
    

也就是说,当队列为空时,在加入第一个节点的时候,会先加入一个哨兵节点。

ReentrantLock 中的lock

这里分为2中情况分析lock()方法,之前说过ReentrantLock中的Sync有非公平和公平2中模式。又结合AQS的源码分析,所以我们知道区别在于FairSyncNonfairSync 2个类中的 tryAcquire 实现不同。

  • FairSync 公平情况如下:
protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //当state为0时,就标识空闲,可以被获取
            if (c == 0) {
            //hasQueuedPredecessors是公平策略,
                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;
        }

可以看到对state的修改都是运行了CAS来更新,同时,在修改state的基础上,使用了公平策略hasQueuedPredecessors是在AQS中的方法

//判断当前节点是否是队列的第一个节点,如果当前节点有前驱结点返回true,当前队列为空或者当前节点是第一个节点则返回false
public final boolean hasQueuedPredecessors() {
    
    Node t = tail; 
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

这里最后的判断可以分开来看,h==s则是当前队列为空,直接返回false,如果h!=s且s==null则是,说明有一个元素将要作为AQS的第一个节点入队列,那么返回true,如果,s.thread != Thread.currentThread()) 就代表,第一个元素就不是当前的线程,返回true。

  • NonfairSync 非公平情况如下

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

//非公平获取
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //缺少了校验公平的策略,直接对state进行修改
        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;
}

所以按照以上Lock的逻辑,lockInterruptibly便是调用了acquireInterruptibly的方法来获取资源

public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        //判断线程的状态
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
}

ReentrantLock 中的unlock

ReentrantLock 中的unlock,是没有策略之分,在Snyc中就实现了方法

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

unlock的代码就比较好读了,先判断lock的持有线程与当前线程是否一致,然后是的话,就把state和持有线程清空。最后在AQS中,会移除该节点

当然在AQS中,还存在一个条件队列,在后续文章再谈。

下面是ReentrantLock 的一个例子,模拟领取优惠卷的情况

public class ReenTrantLockDemo extends Thread{

    //模拟优惠卷
    private static List<Integer> array = new ArrayList<>();

    private static Lock lock = new ReentrantLock();

    public Integer get(){
        lock.lock();
        try {
            Integer o=  array.get(0);
            array.remove(o);
            return o;
        }catch (Exception e){
            System.out.println("获取出错");
        }finally {
            lock.unlock();
        }
        return -1;
    }


    @Override
    public void run() {
       Integer a =  get();
        System.out.println("获取到的优惠卷编号为"+a);
    }

    public static void main(String[] args) {

        for(int i=0;i<10;i++){
            array.add(i);
        }
        ReenTrantLockDemo reenTrantLockDemo = new ReenTrantLockDemo();
        ExecutorService service = Executors.newCachedThreadPool();
        ReenTrantLockDemo demo = new ReenTrantLockDemo();
        for (int i = 0; i < 10; i++) {
            service.submit(demo);
        }
        service.shutdown();

    }
}

结果为

获取到的优惠卷编号为3
获取到的优惠卷编号为2
获取到的优惠卷编号为6
获取到的优惠卷编号为5
获取到的优惠卷编号为1
获取到的优惠卷编号为0
获取到的优惠卷编号为4
获取到的优惠卷编号为8
获取到的优惠卷编号为7
获取到的优惠卷编号为9

去掉lock的结果为:

获取到的优惠卷编号为0
获取到的优惠卷编号为5
获取到的优惠卷编号为0
获取到的优惠卷编号为0
获取到的优惠卷编号为0
获取到的优惠卷编号为7
获取到的优惠卷编号为4
获取到的优惠卷编号为8
获取到的优惠卷编号为0
获取到的优惠卷编号为6

名词解释

公平锁与非公平锁 :是否按照线程进入阻塞队列的顺序来执行

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值