理解J.U.C中的ReentrantLock

JUC是啥?

其实很简单,大家都喜欢缩写!J.U.C= java.util.concurrent就是这个东西

来自哪里?出现的理由

在Lock接口出现之前,java中的应用程序对于多线程的并发安全处理只能基于synchronized关键字来解决。但是synchronized在有些场景中会存在一些短板,也就是它并不适合所有的并发场景。但是在java5以后,Lock的出现可以解决synchronized在某些场景中的短板,它比synchronized更加灵活

下面我们来简单介绍几种锁:
  • 1、ReentrantLock(重入锁)
  • 2、ReentrantReadWriteLock(重入读写锁)

看下面的案例: ReentrantLock的Demo

public class ReentrantLockTest1 {


    static int value = 0;


    Lock lock = new ReentrantLock();


    public static void incr() {


        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        value++;
    }


    public static void main(String[] args) {




        Thread[] threads = new Thread[1000];


        for (int i = 0; i < 1000; i++) {


            threads[i] = new Thread(() -> {
                incr();
            });
        }
        // 启动线程
        for (int i = 0; i < 1000; i++) {


            threads[i].start();
        }


        System.out.println("value的值为:" + value);
    }
}

结果: value的值为:960

很明显这个结果不是我们想要的!我们想要的是: 1000

继续往下看:

public class ReentrantLockTest1 {


    static int value = 0;


    static Lock lock = new ReentrantLock();


    public static void incr() {


        try {
            lock.lock();
            value ++;
            try {


                Thread.sleep(1);
                value++;
            } catch (InterruptedException e) {


                e.printStackTrace();


            }
        } finally {


            lock.unlock();
        }
    }




    public static void main(String[] args) {




        Thread[] threads = new Thread[1000];


        for (int i = 0; i < 1000; i++) {


            threads[i] = new Thread(() -> {
                incr();
            });
        }
        // 启动线程
        for (int i = 0; i < 1000; i++) {


            threads[i].start();
        }


        System.out.println("value的值为:" + value);
    }
}

结果: value的值为:89
说明什么?完整获取锁的执行只有89次,我们在改变一下

接着看下面的案例:

public class ReentrantLockTest1 {


    static int value = 0;


    static Lock lock = new ReentrantLock();


    public static void incr() {


        try {
            lock.lock();
            value++;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread[] threads = new Thread[1000];


        for (int i = 0; i < 1000; i++) {


            threads[i] = new Thread(() -> {
                incr();
            });
        }
        // 启动线程
        for (int i = 0; i < 1000; i++) {


            threads[i].start();
        }


        Thread.sleep(3000);
        System.out.println("value的值为:" + value);
    }
}

结果: value的值为:1000

以上得出的结论是: ReentrantLock.lock() 确实可以保证多线程情况下的线程安全,前提是你得让他执行完!

在上面执行的工程中我们发现一个问题我们尝试过用ReentrantLock.tryLock() 去尝试获得锁,但是存在一个问题:

public class ReentrantLockTest1 {


    static int value = 0;


    static Lock lock = new ReentrantLock();


    public static void incr() {


        if (lock.tryLock()) {
            value++;
            lock.unlock();
        }
    }


    public static void main(String[] args) throws InterruptedException {




        Thread[] threads = new Thread[1000];


        for (int i = 0; i < 1000; i++) {


            threads[i] = new Thread(() -> {
                incr();
            });
        }
        // 启动线程
        for (int i = 0; i < 1000; i++) {


            threads[i].start();
        }


        Thread.sleep(10000);
        System.out.println("value的值为:" + value);
    }
}

前提: 我试过把睡眠时间调整为 37710秒,但是得到的结果都是不足1000
     这样子说来
     ReentrantLock.lock()
     ReentrantLock.tryLock()
     存在很大区别了
    
    从结果上看:ReentrantLock.lock()最起码能保证结果的正确性
              ReentrantLock.tryLock()不能保证结果的正确性
    我们先去看下ReentrantLock.tryLock()因为Lock()的底层原理我已经比较熟悉了
              代码如下:

/**
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */

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


ReentrantLock.lock()最起码能保证结果的正确性的原因是:

/**
 * Acquires in exclusive mode, ignoring interrupts.  Implemented
 * by invoking at least once {@link #tryAcquire},
 * returning on success.  Otherwise the thread is queued, possibly
 * repeatedly blocking and unblocking, invoking {@link
 * #tryAcquire} until success.  This method can be used
 * to implement method {@link Lock#lock}.
 *
 * @param arg the acquire argument.  This value is conveyed to
 *        {@link #tryAcquire} but is otherwise uninterpreted and
 *        can represent anything you like.
 */
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

它将为获取到锁的线程放置到了一个等待队列(双向链表)中

所以lock() tryLock() 从本质上讲还是存在很大区别的!!!

下面我们再说下: ReentrantReadWriteLock(重入读写锁)
看下面的案例:

public class Demo {


    static Map<String, Object> cacheMap = new HashMap<>();


    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();


    static Lock read = rwl.readLock();


    static Lock write = rwl.writeLock();


    static Lock fromLock = new ReentrantLock();


    public static Object get(String key) {

        if (fromLock.tryLock())
            // 读锁  阻塞
            read.lock();


        try {

            return cacheMap.get(key);

        } finally {

            read.unlock();
        }
    }


    public static Object write(String key, Object value) {


        // other thread  获得了写锁
        write.lock();


        try {
            return cacheMap.put(key, value);
        } finally {
            write.unlock();
        }
    }
}

说明: 当多个线程访问get()/write()方法的时候,当多个线程读一个变量的时候是不互斥的,但是当一个线程获取了写锁,那么此时
      读锁会阻塞,防止拿到当数据

Ps: ReentrantReadWriteLock适用于读多写少的场景

但是!究竟尼玛为啥,当获取写锁的时候读锁会阻塞?我们去看看

/**
 * A {@code ReadWriteLock} maintains a pair of associated {@link
 * Lock locks}, one for read-only operations and one for writing.
 * The {@link #readLock read lock} may be held simultaneously by
 * multiple reader threads, so long as there are no writers.  The
 * {@link #writeLock write lock} is exclusive.
*/

我感觉已经说的很明显了。。实际上是因为位置,没有看到具体的实现

上面的问题呢?先放着吧,暂时超出我的能力,需要指引!!!

思考锁的实现(设计思维)

1、锁的互斥
2、没有抢占到锁的线程?
3、等待的线程怎么存储?
4、公平和非公平(能否插队)
5、重入的特性(识别是否同一个人?ThreadID)


解决方案:

1、锁的互斥,说的在简单点就是就共享资源的竞争,巧的是以前抢夺的是共享资源!现在抢占的是一个标志位!state,如果state=0那么代表当前线程没有抢占到锁,如果state=1则代表抢占到了锁,可以继续向下执行

23 没有抢占到锁的线程我们该如何处理?等待的线程怎么存储?我们可以举例下面的一个场景,好比去医院看病,这个例子不好!换一个~假如我们去洗脚城洗脚吧,我们中意7号!但是奈何喜欢她的人比较多,老板只能让你等着等7号空闲出来了,你才能上!用词错误,你才能洗~ 但是,不可能说我先来的我最后一个上是吧,所以老板需要给我发一个号码牌,假定是9527号,按照正常来讲一定是顺序排队的,谁先来,谁上!

4、这个公平不公平我们沿用上面的例子!正常来说一定是谁先来的谁先上,但是存在一个问题,一个新来的大哥,看队伍比较长,他想先洗,不洗就挂了!拿500块买我的位置~ 我可能也不会卖,除非给我550!如果我卖他了,那就是不公平的(大哥插队了),如果我大喝一声: 这世道竟然还有插队的!?他可能就得老老实实排队去了,那么就是公平的,因为得排队

5、重入性这个就比较有意思了~ 7号给大爷,再加个钟!!,懂的都懂。。不能再说了

技术方案:

1volatile state = 0;(无锁)1代表是持有锁, > 1代表重入
2、wait/notify马上到!condition 需要唤醒指定线程。【LockSupport.park(); -> unpark(thread)】 unsafe类中提供的一个方法
3、双向链表
4、逻辑层面实现
5、在某一个地方存储当前获得锁的线程的ID,判断下次抢占锁的线程是否为同一个


下面我们来模拟一个场景: 模拟三个线程争夺lock()的场景(先把总体的图给你们,再去看源码分析)

在这里插入图片描述


/**
 * Acquires the lock.
 *
 * <p>If the lock is not available then the current thread becomes
 * disabled for thread scheduling purposes and lies dormant until the
 * lock has been acquired.
 *
 * <p><b>Implementation Considerations</b>
 *
 * <p>A {@code Lock} implementation may be able to detect erroneous use
 * of the lock, such as an invocation that would cause deadlock, and
 * may throw an (unchecked) exception in such circumstances.  The
 * circumstances and the exception type must be documented by that
 * {@code Lock} implementation.
 */

void lock();

说的什么意思呢?

1、尝试获取锁
2、在获取锁的过程中如果发现当前锁没抢到那么,当前线程会变为阻塞状态进入休眠状态
3、当持有锁的线程释放掉锁,那么休眠的线程就可以去竞争锁


/**
 * Acquires the lock.
 *
 * <p>Acquires the lock if it is not held by another thread and returns
 * immediately, setting the lock hold count to one.
 *
 * <p>If the current thread already holds the lock then the hold
 * count is incremented by one and the method returns immediately.
 *
 * <p>If the lock is held by another thread then the
 * current thread becomes disabled for thread scheduling
 * purposes and lies dormant until the lock has been acquired,
 * at which time the lock hold count is set to one.
 */
public void lock() {
    sync.lock();
}

这个是ReentrantLock里面的lock()说的什么意思呢?

1、如果当前锁没有被持有那么当前线程持有锁,并且将持有次数设置为1
2、如果当前线程已经持有了锁,那么持有次数 + 1,并且立即返回表示持有锁
3、同上

这个sync是啥?瞅一瞅

提供所有实现机制的同步器,基于AQS去表示当前锁的状态,成吧(我是没理解)
说下我的理解吧

保证锁状态"state"的实时性,这东西就是干这个的!

我们接着看非公平锁的实现


/**
 * Sync object for non-fair locks
 */
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);
    }
}

分析: (我们多分析点,多个线程抢夺锁的情况,分析如图的情况吧ThreadA、ThreadB、ThreadC)


第一次,刚刚进入,此时state = 0, 那么我们进入if分支

setExclusiveOwnerThread(Thread.currentThread());

注释如下:
/**
 * Sets the thread that currently owns exclusive access.
 * A {@code null} argument indicates that no thread owns access.
 * This method does not otherwise impose any synchronization or
 * {@code volatile} field accesses.
 * @param thread the owner thread
 */
protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

/**
 * The current owner of exclusive mode synchronization.
 */
private transient Thread exclusiveOwnerThread;

解释:

它说了一堆没有的没用的,总结就是一句话: 表示当前这个线程拥有了锁,可以去访问了!没了。

总结: 第一次进入做了什么事呢?

1、设置state 0 ---> 1
2、设置exclusiveOwnerThread 为当前线程
(我画的图还是蛮好的!!!)

那么当一个线程持有锁,其他线程进入是什么样子的一个情况呢?我们继续分析


它会进入else分支,那么如下:

/**
 * Acquires in exclusive mode, ignoring interrupts.  Implemented
 * by invoking at least once {@link #tryAcquire},
 * returning on success.  Otherwise the thread is queued, possibly
 * repeatedly blocking and unblocking, invoking {@link
 * #tryAcquire} until success.  This method can be used
 * to implement method {@link Lock#lock}.
 *
 * @param arg the acquire argument.  This value is conveyed to
 *        {@link #tryAcquire} but is otherwise uninterpreted and
 *        can represent anything you like.
 */

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

注解解释:

忽略打断,至少调用一次tryAcquire(),尝试去获取锁
换句话说,线程在一个队列中可能被再次阻塞和释放,不断调用tryAcquire()
方法直到成功,该方法被调用一般在实现了Lock接口(听不出什么东西),不过可以知晓下面两点:

1、阻塞的线程在队列中
2、阻塞的线程会调用tryAcquire()方法


我们再来仔细分析下acquire(int arg),这里面调用了什么方法,呵~好家伙,可不少

1tryAcquire(arg)
2addWaiter(Node.EXCLUSIVE)
3acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

三个方法,我们一个一个来分析

1tryAcquire(arg),其实在分析它前我们可以猜一下这个方法干了什么?

    A、查看当前的state是否变为了0,如果为零了,那么就返回

养成好习惯,看源码前要先读注释,要先在总体上有一个把握,再去看具体的实现,不然,你看个什么玩意,听话养成好习惯,别看一大串子,别急,源码急不来的
差距就是在一点一滴中养成的

/**
 * Attempts to acquire in exclusive mode. This method should query
 * if the state of the object permits it to be acquired in the
 * exclusive mode, and if so to acquire it.
 *
 * <p>This method is always invoked by the thread performing
 * acquire.  If this method reports failure, the acquire method
 * may queue the thread, if it is not already queued, until it is
 * signalled by a release from some other thread. This can be used
 * to implement method {@link Lock#tryLock()}.
 *
 * <p>The default
 * implementation throws {@link UnsupportedOperationException}.
 *
 * @param arg the acquire argument. This value is always the one
 *        passed to an acquire method, or is the value saved on entry
 *        to a condition wait.  The value is otherwise uninterpreted
 *        and can represent anything you like.
 * @return {@code true} if successful. Upon success, this object has
 *         been acquired.
 * @throws IllegalMonitorStateException if acquiring would place this
 *         synchronizer in an illegal state. This exception must be
 *         thrown in a consistent fashion for synchronization to work
 *         correctly.
 * @throws UnsupportedOperationException if exclusive mode is not supported
 */
protected boolean tryAcquire(int arg) {

    throw new UnsupportedOperationException();
}

我们来一起读,其实我也没看过这里,也是新的知识,这是我的学习方法,我感觉还不错吧


1、尝试去获取独占模式(也就是去获取这个锁)
2、当state 准许被访问的时候,访问这个方法的线程应该是有序的排队访问
3、如果说线程没有获取到state那么它可能会进等待队列中,如果它没有在等待队列中话(这里面是有说法的 a、等待队列中的线程去顺序获取state b、未在队列中的也可以竞争)
4、以上的所有前提是: signalled by a release(state)

Ps: 其实说的已经很明显了!你看我们上面的图,没有获取到锁的线程,它会进入到一个双向的等待队列中

继续往下看:


/**
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */
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;
}

这个方法比较简单:
其实,这个方法要明确一个前提就是,我们可以尝试着去获取锁了!(此时锁可能还未释放)

1、如果抢占到了则获取state,并设置线程为自己
2、如果获取state的线程为当前持有state的线程,那么重入次数 + 1


下面我们来分析第二个方法: addWaiter(Node.EXCLUSIVE), arg)


这个中规中矩,其实还可以吧

/**
 * Creates and enqueues node for current thread and given mode.
 *
 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
 * @return the new node
 */
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;
}

解释:

添加节点至双向队列,节点以给定的模式进行存储,如果当前队列存在节点,那么进入if分支,如果不存在节点那么走非if分支,我们接着看这两个分支

我们这个先进入enq(node);这个方法

/**
 * Inserts node into queue, initializing if necessary. See picture above.
 * @param node the node to insert
 * @return node's predecessor
 */
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;
            }
        }
    }
}

解释: 可以看出以下知识点:

1、尾插法  Node t = tail
2、当队列中不存在元素的时候那么tail = head = new Node
3else 分支node.prev = t其实执行的操作就是新插入元素的前一个元素为原队列的尾节点,那么可以判断
   新插入的元素必定为队列的尾节点
4、我们看下compareAndSetTail(t, node),应该指的就是我们上面的操作,点进去之后发现是一个native方法,但是可以推测和我们猜测差不多的
5compareAndSetHead(new Node()) 这个方法点进去也是native的至于功能我们也阐述过了

Ps: 再来看下我们的图: 没有获得锁的线程,是不是很神奇


我们接着往下看,第三个方法: 是以第二个方法返回的Node作为参数

/**
 * Acquires in exclusive uninterruptible mode for thread already in
 * queue. Used by condition wait methods as well as acquire.
 *
 * @param node the node
 * @param arg the acquire argument
 * @return {@code true} if interrupted while waiting
 */
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);
    }
}


解释下:
通俗点解释就是,将这个未获取到锁的Node丢到等待队列中,当锁可以被竞争了"state"那么他就活了

/**
 * Returns previous node, or throws NullPointerException if null.
 * Use when predecessor cannot be null.  The null check could
 * be elided, but is present to help the VM.
 *
 * @return the predecessor of this node
 */
final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
        throw new NullPointerException();
    else
        return p;
}

返回当前节点的前一个节点

继续研究: final boolean acquireQueued(final Node node, int arg)

那么如下: 说了啥呢

1、如果当前节点的前一个节点为头结点并且尝试获取锁成功!那么将node设置为当前等待队列的head节点
2、如果不成立的话,说明当前锁还是不可获取的状态这时判断是否可以挂起当前线程、
3、如果判断结果为真则挂起当前线程, 否则继续循环, 
4、在这期间线程不响应中断

5、在最后确保如果获取失败就取消获取

       if (failed) {
           cancelAcquire(node);
       }

我目前的水平值准许我分析到这种程度了。。以后找到对象我再继续分析,哈哈! 再见。

有问题,大家一起讨论,不开心你骂我也成,但是你得说出所以然,不然我可能会去打你。。

图感觉有点花,可以看这个: https://app.yinxiang.com/fx/279855bd-bcda-462e-be8f-e69ab987df95
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值