ReentrantLock的使用以及底层源码

一、使用ReentrangLock实现多线程同步

例子:模拟妻子和丈夫去取钱,银行卡余额为100元,两个人同时取100元,那么最终余额应该是0元,有一个人取不出钱。

package com.cy.test.lock;


public class Account implements Runnable {
    private int money = 100;    //账户上默认有100元

    //取钱
    public void draw() {
        int balance = money; //当前余额
        if (balance > 0) {
            for(int i=0; i<10000; i++){

            }

            money = money - 100;   //取走100元
            System.out.println("当前余额为" + balance +"元," + Thread.currentThread().getName() + "取走100元,还剩" +  money + "元");
        }
    }

    @Override
    public void run(){
        draw();
    }
}

妻子和丈夫分别来取钱,那么如果其中一人取走100元,另一个应该就没法取钱了,因为余额为0了。

package com.cy.test.lock;

public class TestReentrantLock {

    public static void main(String[] args) {
        Account account = new Account();
        Thread wife = new Thread(account, "wife");
        Thread husband = new Thread(account, "husband");
        //妻子取钱
        wife.start();
        //丈夫取钱
        husband.start();
    }
}

console打印:

当前余额为100元,wife取走100元,还剩-100元
当前余额为100元,husband取走100元,还剩-100元

运行上面main方法,查看结果,很明显,出问题了,余额变成-100了。

这就是多线程造成的问题。

下面使用ReentrantLock来解决

package com.cy.test.lock;


import java.util.concurrent.locks.ReentrantLock;

public class Account implements Runnable {
    private int money = 100;    //账户上默认有100元
    private ReentrantLock lock = new ReentrantLock();

    //取钱
    public void draw() {
        try{
            lock.lock();
            int balance = money; //当前余额
            if (balance > 0) {
                for(int i=0; i<10000; i++){

                }

                money = money - 100;   //取走100元
                System.out.println("当前余额为" + balance +"元," + Thread.currentThread().getName() + "取走100元,还剩" +  money + "元");
            }
        }finally {
            lock.unlock();
        }
    }

    @Override
    public void run(){
        draw();
    }
}

再次运行main方法结果正确。  (当前余额为100元,wife取走100元,还剩0元)

 

二、ReentrantLock底层源码的实现

当调用new ReentrantLock().lock()时,调用的是sync.lock(),再是非公平NonfairSync里面的lock()方法,再是acquire(1),调用的是父类AbstractQueuedSynchronizer里面的tryAcquire(arg)方法,再是NonfairSync的tryAcquire(int acquires)方法,最终调用的是:父类Sync的nonfairTryAcquire(int acquires)

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

这个是核心方法。其实AbstractQueuedSynchronizer父类中有个private volatile int state;变量,是volatile修饰的,线程之间可见,state=0时,表示线程未获取到锁。state=1时,表示线程获取到锁了。

如果state=0,执行cas方法:compareAndSetState(0, acquires),如果成功,则设置获取到锁的线程为当前线程setExclusiveOwnerThread(current),返回true。

如果state !=0,锁已被获取到,且来获取锁的线程是当前占有锁的线程,current == getExclusiveOwnerThread(),则可以继续获取到锁,表示是可重入的,设置state的值 + 1,返回true。

如果NonfairSync的tryAcquire没有获取到锁,则执行父类AbstractQueuedSynchronizer的acquireQueued(addWaiter(Node.EXCLUSIVE), arg),进入等待队列,等待锁的释放。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

父类AbstractQueuedSynchronizer中维护着一个链表,Node节点,有Node prev和Node next,因此它是双向链表,Node节点中保存着thread。就是用这个链表来实现等待队列的。aqs最核心的点在于怎么等待队列中的线程怎么入队,怎么出队的问题,原来入队出队需要加锁的,这里的入队出队用的都是cas,用cas方法往tail上加。比如多个线程都没有获取到锁,都要入队,如果队列不加锁的话,可能造成一个tail上有多个线程,那就乱了。想到的方法是给这个队列加锁,几个线程入队时拿到锁再加到tail,挨个的加进去。现在AQS实现的都是cas的方法入队出队,没有锁。

image

 

 

 

 

 

--

posted on 2019-05-05 20:22  有点懒惰的大青年  阅读(555)  评论(0)    收藏  举报