理解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()的场景(先把总体的图给你们,再去看源码分析)

理解J.U.C中的ReentrantLock_第1张图片


/**
 * Acquires the lock.
 *
 * 

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. * *

Implementation Considerations * *

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. * *

Acquires the lock if it is not held by another thread and returns * immediately, setting the lock hold count to one. * *

If the current thread already holds the lock then the hold * count is incremented by one and the method returns immediately. * *

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. * *

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()}. * *

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

你可能感兴趣的:(并发编程,juc,多线程,ReentrantLock)