【并发编程】并发容器ReentrantLock

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zpx31613/article/details/80808798

 

ReentrantLock  可重入锁 ,实现了Lock 接口 ,是AQS框架的具体实现 。支持重入,等待超时,响应中断,公平和非公平锁。

 

ReentrantLock 使用  

例1


public class ReentrantLockTest {


     //两个线程同时去跑同一个方法 ,模拟竞争场景
    public static void main(String[] args) {

        ReentrantLockTest r = new ReentrantLockTest();
        Lock lock =  new ReentrantLock();
        new Thread(()->{
          r.test(lock);

        }).start();

        new Thread(()->{
            r.test(lock);

        }).start();
    }

    public void test (Lock lock){
        try {

            lock.lock();
            System.out.println("线程"+Thread.currentThread().getName()+"获取到锁");
            Thread.sleep(2000);

        }catch (Exception e){

        }finally {
            lock.unlock();
            System.out.println("线程"+Thread.currentThread().getName()+"释放锁");
        }

    }
}

控制台打印 

线程Thread-0获取到锁
线程Thread-0释放锁
线程Thread-1获取到锁
线程Thread-1释放锁

 为了避免死锁,一般将释放锁动作放在finally块中 ,当try 块中发生异常时也能最终释放锁

例2

public class ReentrantLockTest2 {

    public static void main(String[] args) {

        Lock lock = new ReentrantLock();
        Mythread threada = new Mythread(lock);
        Mythread threadb = new Mythread(lock);
        threada.start();
        threadb.start();
    }


    public static void test(Lock lock) throws InterruptedException {
        if (lock.tryLock(2000, TimeUnit.MILLISECONDS)) {
            try {
                System.out.println("线程" + Thread.currentThread().getName() + "获取到锁");
                Thread.sleep(4000);

            } finally {
                lock.unlock();
                System.out.println("线程" + Thread.currentThread().getName() + "释放掉了锁");
            }
        } else {
            System.out.println("线程" + Thread.currentThread().getName() + "放弃了获取锁");
        }
    }
}

class Mythread extends Thread {
    private Lock lock;

    public Mythread(Lock lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            ReentrantLockTest2.test(lock);
        } catch (InterruptedException e) {

        }

    }
}

控制台打印:

线程Thread-0获取到锁
线程Thread-1放弃了获取锁
线程Thread-0释放掉了锁

说明没获取到锁的线程等待两秒后会放弃等待。tryLock方法 返回一个boolean值 ,获取锁写法 必须写在if条件中。

例3

public class ReentrantLockTest2 {

    public static void main(String[] args) {

        Lock lock = new ReentrantLock();
        Mythread threada = new Mythread(lock);
        Mythread threadb = new Mythread(lock);
        threada.start();
        threadb.start();


        try {
       Thread.sleep(2000);
        }catch (Exception e){

        }
        threadb.interrupt();
    }


    public static void test(Lock lock) throws InterruptedException {
        if (lock.tryLock(4000, TimeUnit.MILLISECONDS)) {
            try {
                System.out.println("线程" + Thread.currentThread().getName() + "获取到锁");
                Thread.sleep(5000);

            } finally {
                lock.unlock();
                System.out.println("线程" + Thread.currentThread().getName() + "释放掉了锁");
            }
        } else {
            System.out.println("线程" + Thread.currentThread().getName() + "放弃了获取锁");
        }
    }
}

class Mythread extends Thread {
    private Lock lock;

    public Mythread(Lock lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            ReentrantLockTest2.test(lock);
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"被中断");
        }

    }
}

 

控制台打印

线程Thread-0获取到锁
Thread-1被中断
线程Thread-0释放掉了锁

当线程获取锁失败,等待获取锁时,tryLock(long timeout, TimeUnit unit)可以响应中断。需要注意的事tryLock()不传参的不支持响应中断。

 

ReentrantLock源码分析

这里重点说一下lock()方法  ,获取锁默认的是非公平锁。非公平锁即不用先到先得,举个例子,当线程A 释放锁后,本来先来的线程B不一定能获取到锁,反而有可能刚刚来的线程C获取到了锁。如果是公平锁,那么如果之前B线程是阻塞状态(?)那么线程被唤醒的过程是需要耗费时间的 ,这时候已经来的线程C就需要白白等待。所以在一些耗时比较短且竞争激烈场景中非公平锁的性能可能更好。

追踪下ReentrantLock 中lock 方法的实现  

public void lock() {
        sync.lock();
    }

lock 方法调用了抽象内部类Sync 的lock 方法  


 abstract void lock();
 

Sync 中并没有直接实现 ,而是由Sync的两个实现类去实现。这里重点看下非公平的实现


 static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

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

先尝试CAS尝试将共享资源状态从0替换到1(共享资源状态是ReentrantLock的一个成员变量,当为0时代表共享资源没有被占据,同一个锁可多次进入被锁住代码块,每进入一次state+1,每释放一次state-1) , 尝试成功将当前线程设置为锁的独占线程,尝试失败调用acquire方法。发现ReentrantLock中并没有此方法的实现,那具体实现必然在其父类AQS中 。

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

尝试获取锁,获取失败尝试加入队列知道成功加入队列为止

分析下tryAcquire方法的最终实现在 NonfairSync 中,AQS只实现了框架尝试获取共享资源的抽象以及等待队列入队出队的具体实现,尝试获取共享资源的具体实现由实现了AQS框架的各并发容器去实现。

// NofairSync 中实现了AQS的tryAcquire方法
 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) {
                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;
        }

获取当前线程

获取当前共享状态,如果当前共享状态的值为0 说明并没有线程获取到锁,CAS操作尝试获取锁,获取锁成功将当前线程设置为独占线程,返回true。

如果共享状态的值不为0,判断当前线程是否是共享资源的独占线程,是的话将共享资源状态加1 ,返回true

以上都不成功返回false

当tryAquire 返回true 时,表示获取锁成功,不再进入后续代码,当tryAquire返回false时也就是说没有获取到锁,尝试加入队列

调用addWaiter方法

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

加入队列的操作由父类AQS实现。

将当前尝试加入队列的线程包装为一个node节点

获取等待队列的尾节点,如果尾节点不为空,先将当前线程的前驱指向尾节点,然后CAS操作保证当前尾节点没有改变并将成员变量tail设置为当前线程的节点,将之前尾节点的后继设置为当前线程的节点,此时线程加入等待队列成功,返回线程节点,方法执行完毕。

如果尾节点为空,或者刚才尝试加入队列失败则进入enq方法,此方法是利用CAS操作和自旋将节点加入队尾,如果不成功就一直自旋。

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

当前线程加入等待队列后调用 acquireQueued方法 ,此方法主逻辑是在队列中排队,以及排队结束尝试去获取锁

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

  1. 获取当前节点的前节点,

  2. 判断前节点是否是头节点 ,如果是前节点调用tryAquire方法尝试获取锁,获取锁成功,设置当前节点为头结点

  3.不是头节点或者获取锁失败,就去尝试将线程的节点放到队列中合适的位置然后让线程挂起

  关于挂起的操作:因为中间有的线程可能已经放弃等待了 ,所以节点应将节点放到离本身最近的未放弃等待的节点之后,然后挂起。shouldParkAfterFailedAcquire 方法作用是将线程节点放到最后一个未放弃的节点之后。parkAndCheckInterrupt 方法作用是将线程的节点挂起。

 4.直到线程获取到锁,且出队之后才能跳出自旋

 

acquire()方法,再总结一下就是,尝试获取锁,若果获取不成功则加入等待队列排队,等轮到自己有机会获取锁再去尝试获取,非公平的情况下可能被加塞,直到获取锁成功为止出列。中间有些情况下可能放弃获取锁,如调用public boolean tryLock(long timeout, TimeUnit unit) 方法时,在等待过程中可以被中断,那么就不必非要获取到锁才能出列了。

关于AQS简单的说下,维护了一个先进先出的等待队列和一个共享资源状态的成员变量,关于进出队列的操作由AQS实现,而释放共享资源和获取共享的操作有实现了AQS框架的类自己去根据需求去个性化实现:比如共享资源是否独占(ReentrantLock就是独占,CountDownLatch就是共享),是否是公平的等···

关于和Synchronized的比较 

ReentrantLock是 lock 的实现类 ,内部实现了AQS框架 ,是纯代码层面的实现的锁,本质上就是一个java类。而Synchronized代码块 是由jvm 帮助我们在进入和退出代码块时加上了monitorenter/monitorexit 指令实现的。

ReentrantLock必须手动的获取锁和释放锁,而Synchronized并不用手动获取锁释放锁,由jvm帮我们完成。

ReentrantLock 可以支持等待超时,也就是等待多久之后就主动放弃获取锁,

ReentrantLock 支持响应中断 ,当线程在等待锁时,可以主动中断等待,中断后会抛出中断异常;而等待Synchronized 关键字实现的内置锁时,中断是不起作用的

另外特别注意,不要讲获取锁的代码放在try块中,不然在获取锁的过程中发生异常,最终在finally 中会错误的释放锁(因为没获取到锁)

 

 

展开阅读全文

没有更多推荐了,返回首页