欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java多线程——JUC ReentrantLock使用详解一篇就够,以及使用ReentrantLock避免死锁情况的产生

程序员文章站 2022-03-07 11:27:56
什么是ReentrantLock简单的来讲ReentrantLock是JUC提供的一种与synchronized关键字加锁作用类似的类。与synchronized关键字相比,ReentrantLock有如下特点:可中断(一个线程可以通过interrupt方法取消另一个线程的锁等待)可以设置竞争锁资源的超时时间可以设置公平锁(synchronized关键字释放锁资源之后,其他在关联该对象Monitor的阻塞线程将会再次竞争锁资源,并没有先来先得,后来后得的公平性)支持多个条件变量(sy...

什么是ReentrantLock

简单的来讲ReentrantLock是JUC提供的一种与synchronized关键字加锁作用类似的类

与synchronized关键字相比,ReentrantLock有如下特点:

  • 可中断(一个线程可以通过interrupt方法取消另一个线程的锁等待)
  • 可以设置竞争锁资源的超时时间
  • 可以设置公平锁(synchronized关键字释放锁资源之后,其他在关联该对象Monitor的阻塞线程将会再次竞争锁资源,并没有先来先得,后来后得的公平性)
  • 支持多个条件变量(synchronized关键字竞争失败的线程都只会进入该对象锁的Monitor的阻塞队列中,也就是没有条件可以选择)
  • 与synchronized关键字一样的是都支持重入锁

可重入

package com.leolee.multithreadProgramming.juc.reentrantLock;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @ClassName Test
 * @Description: JUC ReentrantLock测试
 * 相对于 synchronized,ReentrantLock具有如下特点:
 *      1.可中断(一个线程可以取消另一个线程的锁)
 *      2.可以设置超时时间
 *      3.可以设置为公平锁
 *      4.支持多个条件变量(synchronized竞争失败的线程都会进入Monitor的waitList,ReentrantLock可以根据不同的条件变量进入不同的集合)
 *      5.与synchronized一样,都支持锁重入
 *
 *
 * @Author LeoLee
 * @Date 2020/12/7
 * @Version V1.0
 **/
@Slf4j
public class Test {

    private static final ReentrantLock reentrantLock = new ReentrantLock();

    //============================基本使用方法以及可重入测试============================

    public void normalTest() {
        reentrantLock.lock();

        try {
            //临界区,需要被保护的代码块
            log.info("main method 获得锁,开始执行");
            this.m1();
        } catch (Exception e) {
            e.printStackTrace();

        } finally {
            reentrantLock.unlock();
        }
    }

    public void m1() {
        reentrantLock.lock();

        try {
            //临界区,需要被保护的代码块
            log.info("m1 获得锁,开始执行");
            this.m2();
        } catch (Exception e) {
            e.printStackTrace();

        } finally {
            reentrantLock.unlock();
        }
    }

    public void m2() {
        reentrantLock.lock();

        try {
            //临界区,需要被保护的代码块
            log.info("m2 获得锁,开始执行");
        } catch (Exception e) {
            e.printStackTrace();

        } finally {
            reentrantLock.unlock();
        }
    }


    public static void main(String[] args) {

        Test test = new Test();
        test.normalTest();
    }
}


执行结果:
20:33:20.368 [main] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - main method 获得锁,开始执行
20:33:20.388 [main] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - m1 获得锁,开始执行
20:33:20.388 [main] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - m2 获得锁,开始执行

可中断

可中断的特性就是为了避免线程“死等”的问题,让线程的等待受开发者控制。

因为这个中断线程等待的操作是需要其他线程来调用interrput方法,所以这种特性是一种被动的防止死锁的方式,开发者要考虑在什么情况下,何时来打断等待的线程,所以这种方式在使用上会有一些额外的考虑因素影响实际的代码编写。

package com.leolee.multithreadProgramming.juc.reentrantLock;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @ClassName Test
 * @Description: JUC ReentrantLock测试
 * 相对于 synchronized,ReentrantLock具有如下特点:
 *      1.可中断(一个线程可以取消另一个线程的锁)
 *      2.可以设置超时时间
 *      3.可以设置为公平锁
 *      4.支持多个条件变量(synchronized竞争失败的线程都会进入Monitor的waitList,ReentrantLock可以根据不同的条件变量进入不同的集合)
 *      5.与synchronized一样,都支持锁重入
 *
 *
 * @Author LeoLee
 * @Date 2020/12/7
 * @Version V1.0
 **/
@Slf4j
public class Test {

    private static final ReentrantLock reentrantLock = new ReentrantLock();


    //============================可中断特性测试============================

    public void testInterruptibly() {

        Thread t1 = new Thread(() -> {
            try {
                //如果竞争到锁就继续执行,失败就进入阻塞队列,其他线程可以使用 interrupt 打断,即:你不要继续等待了、
                log.info("{} 尝试获取锁", Thread.currentThread().getName());
                reentrantLock.lockInterruptibly();
            } catch (InterruptedException e) {
                //被interrupt打断之后,抛出InterruptedException,就代表没有获取到锁,直接return
                e.printStackTrace();
                log.info("{} 未获取到锁,直接return", Thread.currentThread().getName());
                return;
            }

            //获取到锁
            try {
                log.info("{} 获取到锁,并执行代码", Thread.currentThread().getName());
            } finally {
                reentrantLock.unlock();
            }
        }, "t1");

        //主线程先获取锁,让t1线程进入阻塞队列
        reentrantLock.lock();
        t1.start();

        //主线程打断t1线程的等待
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("{} 打断 {}", Thread.currentThread().getName(), t1.getName());
        t1.interrupt();
    }


    public static void main(String[] args) {

        Test test = new Test();
        test.testInterruptibly();
    }
}

执行结果:
20:34:26.728 [t1] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - t1 尝试获取锁
20:34:27.739 [main] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - main 打断 t1
20:34:27.740 [t1] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - t1 未获取到锁,直接return
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at com.leolee.multithreadProgramming.juc.reentrantLock.Test.lambda$testInterruptibly$0(Test.java:82)
	at java.lang.Thread.run(Thread.java:745)

锁超时

上面的可中断是一种被动的防止线程死等(死锁)的特性,那么锁超时就是一种主动的方式。

package com.leolee.multithreadProgramming.juc.reentrantLock;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @ClassName Test
 * @Description: JUC ReentrantLock测试
 * 相对于 synchronized,ReentrantLock具有如下特点:
 *      1.可中断(一个线程可以取消另一个线程的锁)
 *      2.可以设置超时时间
 *      3.可以设置为公平锁
 *      4.支持多个条件变量(synchronized竞争失败的线程都会进入Monitor的waitList,ReentrantLock可以根据不同的条件变量进入不同的集合)
 *      5.与synchronized一样,都支持锁重入
 *
 *
 * @Author LeoLee
 * @Date 2020/12/7
 * @Version V1.0
 **/
@Slf4j
public class Test {

    private static final ReentrantLock reentrantLock = new ReentrantLock();

    //============================锁超时============================

    public void testLockTimeout() {

        Thread t1 = new Thread(() -> {
            log.info("{} 尝试获得锁", Thread.currentThread().getName());
            //不带参数方法是立刻返回加锁是否成功的结果,带参的是等待一定时间后返回结果,如果在超时时间内获取到锁,则直接返回true
            if (!reentrantLock.tryLock()) {
                log.info("{} 尝试获取锁失败,直接返回", Thread.currentThread().getName());
                return;
            }
            //临界区代码
            try {
                //获得到了锁
                log.info("{} 获取锁成功,开始执行临界区代码", Thread.currentThread().getName());
            } finally {
                reentrantLock.unlock();
            }
        }, "t1");

        //主线程获取到锁
        reentrantLock.lock();
        t1.start();
    }

    public static void main(String[] args) {

        Test test = new Test();
        test.testLockTimeout();
    }
}

执行结果:
21:17:11.498 [t1] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - t1 尝试获得锁
21:17:11.517 [t1] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - t1 尝试获取锁失败,直接返回

从执行结果看出,不带参的tryLock()获取锁的时候,几乎与同时,就返回了获取锁的结果,不会造成线程的无线等待。

带参的tryLock()如下

package com.leolee.multithreadProgramming.juc.reentrantLock;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @ClassName Test
 * @Description: JUC ReentrantLock测试
 * 相对于 synchronized,ReentrantLock具有如下特点:
 *      1.可中断(一个线程可以取消另一个线程的锁)
 *      2.可以设置超时时间
 *      3.可以设置为公平锁
 *      4.支持多个条件变量(synchronized竞争失败的线程都会进入Monitor的waitList,ReentrantLock可以根据不同的条件变量进入不同的集合)
 *      5.与synchronized一样,都支持锁重入
 *
 *
 * @Author LeoLee
 * @Date 2020/12/7
 * @Version V1.0
 **/
@Slf4j
public class Test {

    private static final ReentrantLock reentrantLock = new ReentrantLock();

    //============================锁超时============================

    public void testLockTimeout2() {

        Thread t1 = new Thread(() -> {
            log.info("{} 尝试获得锁", Thread.currentThread().getName());
            //不带参数方法是立刻返回加锁是否成功的结果,带参的是等待一定时间后返回结果,如果在超时时间内获取到锁,则直接返回true
            try {
                if (!reentrantLock.tryLock(2, TimeUnit.SECONDS)) {
                    log.info("{} 尝试获取锁失败,直接返回", Thread.currentThread().getName());
                    return;
                }
            } catch (InterruptedException e) {
                log.info("{} 尝试获取锁被打断,直接返回", Thread.currentThread().getName());
                e.printStackTrace();
                return;
            }
            //临界区代码
            try {
                //获得到了锁
                log.info("{} 获取锁成功,开始执行临界区代码", Thread.currentThread().getName());
            } finally {
                reentrantLock.unlock();
            }
        }, "t1");

        //主线程获取到锁
        reentrantLock.lock();
        t1.start();
    }

    public static void main(String[] args) {

        Test test = new Test();
        test.testLockTimeout2();
    }
}

执行结果:
21:22:31.956 [t1] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - t1 尝试获得锁
21:22:33.974 [t1] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - t1 尝试获取锁失败,直接返回

tryLock阻塞了两秒钟来尝试获取锁资源,如果在这两秒钟之内主线程释放了锁,那么t1线程将会获得锁对象。

ReentrantLock支持公平锁

ReentrantLock默认是非公平锁,也就是当一个线程释放了锁资源之后,其他等待的线程要再同时竞争锁资源,而不是先来后到的顺序。

ReentrantLock的带参构造方法如下:

Java多线程——JUC ReentrantLock使用详解一篇就够,以及使用ReentrantLock避免死锁情况的产生

这里可以看出如果fair为true则创建的是FairSync,如果是false则是NonFairSync,默认是NonFairSync。字面意思很明显,FairSync公平锁,NonFairSync非公平锁

看一下FairSync的类图

Java多线程——JUC ReentrantLock使用详解一篇就够,以及使用ReentrantLock避免死锁情况的产生

FairSync继承了同为ReentrantLock内部类的Sync,Sync继承了AbstractQueuedSynchronizer。

详细的源码解释暂且不谈之后会有专门的文章来详解,直接看一下AbstractQueuedSynchronizer类。

package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import sun.misc.Unsafe;


public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    private static final long serialVersionUID = 7373984972572414691L;

    /**
     * Creates a new {@code AbstractQueuedSynchronizer} instance
     * with initial synchronization state of zero.
     */
    protected AbstractQueuedSynchronizer() { }

    
    static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

        /**
         * Status field, taking on only the values:
         *   SIGNAL:     The successor of this node is (or will soon be)
         *               blocked (via park), so the current node must
         *               unpark its successor when it releases or
         *               cancels. To avoid races, acquire methods must
         *               first indicate they need a signal,
         *               then retry the atomic acquire, and then,
         *               on failure, block.
         *   CANCELLED:  This node is cancelled due to timeout or interrupt.
         *               Nodes never leave this state. In particular,
         *               a thread with cancelled node never again blocks.
         *   CONDITION:  This node is currently on a condition queue.
         *               It will not be used as a sync queue node
         *               until transferred, at which time the status
         *               will be set to 0. (Use of this value here has
         *               nothing to do with the other uses of the
         *               field, but simplifies mechanics.)
         *   PROPAGATE:  A releaseShared should be propagated to other
         *               nodes. This is set (for head node only) in
         *               doReleaseShared to ensure propagation
         *               continues, even if other operations have
         *               since intervened.
         *   0:          None of the above
         *
         * The values are arranged numerically to simplify use.
         * Non-negative values mean that a node doesn't need to
         * signal. So, most code doesn't need to check for particular
         * values, just for sign.
         *
         * The field is initialized to 0 for normal sync nodes, and
         * CONDITION for condition nodes.  It is modified using CAS
         * (or when possible, unconditional volatile writes).
         */
        volatile int waitStatus;

        /**
         * Link to predecessor node that current node/thread relies on
         * for checking waitStatus. Assigned during enqueuing, and nulled
         * out (for sake of GC) only upon dequeuing.  Also, upon
         * cancellation of a predecessor, we short-circuit while
         * finding a non-cancelled one, which will always exist
         * because the head node is never cancelled: A node becomes
         * head only as a result of successful acquire. A
         * cancelled thread never succeeds in acquiring, and a thread only
         * cancels itself, not any other node.
         */
        volatile Node prev;

        /**
         * Link to the successor node that the current node/thread
         * unparks upon release. Assigned during enqueuing, adjusted
         * when bypassing cancelled predecessors, and nulled out (for
         * sake of GC) when dequeued.  The enq operation does not
         * assign next field of a predecessor until after attachment,
         * so seeing a null next field does not necessarily mean that
         * node is at end of queue. However, if a next field appears
         * to be null, we can scan prev's from the tail to
         * double-check.  The next field of cancelled nodes is set to
         * point to the node itself instead of null, to make life
         * easier for isOnSyncQueue.
         */
        volatile Node next;

        /**
         * The thread that enqueued this node.  Initialized on
         * construction and nulled out after use.
         */
        volatile Thread thread;

        /**
         * Link to next node waiting on condition, or the special
         * value SHARED.  Because condition queues are accessed only
         * when holding in exclusive mode, we just need a simple
         * linked queue to hold nodes while they are waiting on
         * conditions. They are then transferred to the queue to
         * re-acquire. And because conditions can only be exclusive,
         * we save a field by using special value to indicate shared
         * mode.
         */
        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

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

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     */
    private volatile int state;

    /**
     * Returns the current value of synchronization state.
     * This operation has memory semantics of a {@code volatile} read.
     * @return current state value
     */
    protected final int getState() {
        return state;
    }

    /**
     * Sets the value of synchronization state.
     * This operation has memory semantics of a {@code volatile} write.
     * @param newState the new state value
     */
    protected final void setState(int newState) {
        state = newState;
    }

    //此处省略了很多方法
}

看了一个final修饰的静态内部类 Node,Node中有被volatile修饰的Thread属性,以及如下两个外部类属性声明:

    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

这是一个太明显的提示了,说明公平锁的等待线程会以链表的方式进行排序,每次锁资源的释放,都会从链表中按照顺序取出一个最先被加入的线程来获取锁资源。

多条件变量

synchronized其实也有条件变量:当获得锁的线程在临界区中由于不满足某些条件判断,调用了锁对象的wait方法,该线程就会进入锁对象对应的Monitor中的waitSet中等待被唤醒。

而ReentrantLock相对于synchronized来说支持多个条件变量,如果说synchronized对应的waitSet是一个休息区,那么ReentrantLock就支持多个休息区。所以唤醒的时候ReentrantLock可以根据不同的休息区来分别唤醒。这样就可以一定程度的避免虚假唤醒所有等待的线程后,发现很多线程还是不满足条件再次进入wairSet。

下面是主要的使用方法

    public void testCondition() {

        //创建一个新的条件变量(即:一个休息区)
        Condition condition1 = reentrantLock.newCondition();
        Condition condition2 = reentrantLock.newCondition();

        reentrantLock.lock();
        //当某些条件不满足进入休息室等待,此处省略条件判断
        try {
            condition1.await();
            condition1.await(1, TimeUnit.SECONDS);//也是可以设置等待超时时间
            condition1.awaitUninterruptibly();//不能被打断的等待
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //唤醒condition1中某一个等待的线程
        condition1.signal();
        //唤醒condition1中所有等待的线程
        condition1.signalAll();

        //最后要释放锁
    }

使用规范:

  • await前要获得锁
  • await执行后,当前线程释放锁,进入指定的条件变量中等待
  • await的唤醒(超时、被打断),线程将重新竞争锁资源
  • await之后唤醒的线程,将继续执行后续的代码

本文地址:https://blog.csdn.net/qq_25805331/article/details/110828654