文档章节

ReentrantLock(重入锁)详解

猿来狮码农
 猿来狮码农
发布于 11/19 23:39
字数 2846
阅读 75
收藏 1

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

转载链接: https://www.cnblogs.com/takumicx/p/9338983.html

ReentrantLock简介

jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。

ReentrantLock和synchronized的相同点

public class ReentrantLockTest {

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();

        for (int i = 0; i < 3; i++) {
            lock.lock();
            System.out.println("获取锁:" + i);
        }

        for (int i = 0; i < 3; i++) {
            try {

            } finally {
                lock.unlock();
                System.out.println("释放锁:" + i);
            }
        }
        System.out.println("main继续");
    }
}

上面的代码通过lock()方法先获取锁三次,然后通过unlock()方法释放锁3次,程序可以正常退出。说明ReentrantLock是可以重入的锁,当一个线程获取锁时,还可以接着重复获取多次。再加上ReentrantLock的独占性,我们可以得出以下ReentrantLock和synchronized的相同点。

  • 1.ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。但是实现上两者不同: synchronized是在JVM层面上实现的,代码执行出现异常会自动释放锁,ReentrantLock是Java代码实现的类,要释放锁,必须在finally{}中执行unlock方法。
  • 2.ReentrantLock和synchronized都是可重入的。synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。

ReentrantLock相比synchronized的额外功能

ReentrantLock可以实现公平锁

公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。而非公平锁则随机分配这种使用权。和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。在创建ReentrantLock的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁。

一个公平锁的例子:

public class ReentrantLockTest {

    private static Lock lock = new ReentrantLock(true);

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int j = 0; j < 2; j++) {
                    lock.lock();
                    System.out.println("获得锁的线程:" + Thread.currentThread().getName());
                    lock.unlock();
                }
            }).start();
        }
    }
}

输出结果:

获得锁的线程:Thread-1
获得锁的线程:Thread-2
获得锁的线程:Thread-0
获得锁的线程:Thread-3
获得锁的线程:Thread-4
获得锁的线程:Thread-1
获得锁的线程:Thread-2
获得锁的线程:Thread-0
获得锁的线程:Thread-3
获得锁的线程:Thread-4

我们开启5个线程,让每个线程都获取释放锁2次。为了能更好的观察到结果,在每次获取锁前让线程休眠10毫秒。可以看到线程几乎是轮流的获取到了锁(第2次获取锁的顺序与第1次获取锁的顺序一致)。

如果我们改成非公平锁,再看下结果:

获得锁的线程:Thread-0
获得锁的线程:Thread-0
获得锁的线程:Thread-2
获得锁的线程:Thread-2
获得锁的线程:Thread-4
获得锁的线程:Thread-4
获得锁的线程:Thread-1
获得锁的线程:Thread-1
获得锁的线程:Thread-3
获得锁的线程:Thread-3

线程会重复获取锁。如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁。这就是非公平锁的“饥饿”问题。

大部分情况下我们使用非公平锁,因为其性能比公平锁好很多。但是公平锁能够避免线程饥饿,某些情况下也很有用。

ReentrantLock可响应中断

当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()。该方法可以用来解决死锁问题。

public class ReentrantLockTest {

    private static Lock lockFirst = new ReentrantLock();
    private static Lock lockSecond = new ReentrantLock();

    public static void main(String[] args) {
        //该线程(Thread-0)先获取锁1,再获取锁2
        Thread threadFirst = new Thread(new ThreadTest(lockFirst, lockSecond));
        //该线程(Thread-1)先获取锁2,再获取锁1
        Thread threadSecond = new Thread(new ThreadTest(lockSecond, lockFirst));
        threadFirst.start();
        threadSecond.start();
        try {
            TimeUnit.MILLISECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //Thread-0 中断,释放锁
        threadFirst.interrupt();
    }

    private static class ThreadTest implements Runnable {

        private Lock lockFirst;
        private Lock lockSecond;

        public ThreadTest(Lock lockFirst, Lock lockSecond) {
            this.lockFirst = lockFirst;
            this.lockSecond = lockSecond;
        }

        @Override
        public void run() {
            try {
                lockFirst.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + "拿到第1个锁!");
                TimeUnit.MILLISECONDS.sleep(10);
                lockSecond.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + "拿到第2个锁!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lockFirst.unlock();
                lockSecond.unlock();
                System.out.println(Thread.currentThread().getName() + "锁正常释放!");
            }
            System.out.println(Thread.currentThread().getName() + "正常结束!");
        }
    }
}

构造死锁场景:创建两个子线程,子线程在运行时会分别尝试获取两把锁。其中一个线程先获取锁1在获取锁2,另一个线程正好相反。如果没有外界中断,该程序将处于死锁状态永远无法停止。我们通过使其中一个线程中断,来结束线程间毫无意义的等待。被中断的线程将抛出异常,而另一个线程将能获取锁后正常结束。

运行结果:

Thread-0拿到第1个锁!
Thread-1拿到第1个锁!
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1220)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
Thread-1拿到第2个锁!
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
Thread-1锁正常释放!
	at java.lang.Thread.run(Thread.java:748)
Thread-1正常结束!

获取锁时限时等待

ReentrantLock还给我们提供了获取锁限时等待的方法tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题。

public class ReentrantLockTest {

    private static Lock lockFirst = new ReentrantLock();
    private static Lock lockSecond = new ReentrantLock();

    public static void main(String[] args) {
        //该线程(Thread-0)先获取锁1,再获取锁2
        Thread threadFirst = new Thread(new ThreadTest(lockFirst, lockSecond));
        //该线程(Thread-1)先获取锁2,再获取锁1
        Thread threadSecond = new Thread(new ThreadTest(lockSecond, lockFirst));
        threadFirst.start();
        threadSecond.start();
    }

    private static class ThreadTest implements Runnable {

        private Lock lockFirst;
        private Lock lockSecond;

        public ThreadTest(Lock lockFirst, Lock lockSecond) {
            this.lockFirst = lockFirst;
            this.lockSecond = lockSecond;
        }

        @Override
        public void run() {
            try {
                while (!lockFirst.tryLock()) {
                    TimeUnit.MILLISECONDS.sleep(10);
                }
                System.out.println(Thread.currentThread().getName() + "拿到第1个锁!");
                while (!lockSecond.tryLock()) {
                    lockFirst.unlock();
                    System.out.println(Thread.currentThread().getName() + "没能获取第2个锁,释放第1个锁!");
                    TimeUnit.MILLISECONDS.sleep(10);
                }
                System.out.println(Thread.currentThread().getName() + "拿到第2个锁!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                try {
                    lockFirst.unlock();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                try {
                    lockSecond.unlock();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "锁正常释放!");
            }
            System.out.println(Thread.currentThread().getName() + "正常结束!");
        }
    }
}

线程通过调用tryLock()方法获取锁,获取第一把锁失败时会休眠10毫秒,然后重新获取,直到获取成功。获取第二把失败时,首先会释放第一把锁,再休眠10毫秒,然后重试直到成功为止。线程获取第二把锁失败时将会释放第一把锁,这是解决死锁问题的关键,避免了两个线程分别持有一把锁然后相互请求另一把锁。

运行结果:

Thread-0拿到第1个锁!
Thread-1拿到第1个锁!
Thread-0没能获取第2个锁,释放第1个锁!
Thread-1拿到第2个锁!
Thread-1锁正常释放!
Thread-1正常结束!
Thread-0拿到第2个锁!
java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at java.lang.Thread.run(Thread.java:748)
Thread-0锁正常释放!
Thread-0正常结束!

结合Condition实现等待通知机制

使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制。ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。

Condition使用简介

Condition由ReentrantLock对象创建,并且可以同时创建多个。Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁。之后调用Condition接口的await()将释放锁,并且在该Condition上等待,直到有其他线程调用Condition的signal()方法唤醒线程。使用方式和wait,notify类似。

public class ConditionTest {

    private static Lock lock = new ReentrantLock();

    private static Condition condition = lock.newCondition();

    public static void main(String[] args) {

        lock.lock();
        System.out.println(Thread.currentThread().getName() + "获取锁");
        new Thread(() -> {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "获取锁");
            try {
                TimeUnit.SECONDS.sleep(3);
                condition.signal();
                System.out.println(Thread.currentThread().getName() + "通知");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放锁");
            }
        }).start();

        try {
            System.out.println(Thread.currentThread().getName() + "等待通知");
            condition.await();
            System.out.println(Thread.currentThread().getName() + "恢复运行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放锁");
        }

    }
}

运行结果 :

main获取锁
main等待通知
Thread-0获取锁
Thread-0通知
Thread-0释放锁
main恢复运行
main释放锁

使用Condition实现简单的阻塞队列

阻塞队列是一种特殊的先进先出队列,它有以下几个特点:

  • 入队和出队线程安全
  • 当队列满时,入队线程会被阻塞;当队列为空时,出队线程会被阻塞。

阻塞队列的简单实现:

public class MyBlockingQueue<E> {

    //阻塞队列最大容量
    private int size;

    private Lock lock = new ReentrantLock();

    //队列底层实现
    LinkedList<E> list = new LinkedList<>();

    //队列满时的等待条件
    Condition notFull = lock.newCondition();
    //队列空时的等待条件
    Condition notEmpty = lock.newCondition();

    //队列空时的等待条件
    public MyBlockingQueue(int size) {
        this.size = size;
    }

    public void enqueue(E e) {
        lock.lock();
        try {
            //队列已经满了,在notFull条件上等待,等待队列不满的一刻
            while (list.size() == size) {
                notFull.await();
            }
            //入队,加入链表末尾
            list.add(e);
            System.out.println("入队:" + e);
            //通知在notEmpty条件上等待的线程
            notEmpty.signal();
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public E dequeue() {
        lock.lock();
        E e;
        try {
            //队列为空,在notEmpty条件上等待,等待不为空的一刻
            while (list.size() == 0) {
                notEmpty.await();
            }
            //出队,移除链表首元素
            e = list.removeFirst();
            System.out.println("出队:" + e);
            //通知在notFull条件上等待的线程
            notFull.signal();
            return e;
        } catch (Exception exception) {
            exception.printStackTrace();
        } finally {
            lock.unlock();
            return null;
        }
    }
}

测试:

public class BlockingQueueTest {

    public static void main(String[] args) {

        MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(2);

        //10个线程生产
        for (int i = 0; i < 10; i++) {
            int data = i;
            new Thread(() -> {
                queue.enqueue(data);
                System.out.println(Thread.currentThread().getName() + "成产:" + data);
            }).start();
        }

        //10个线程消费
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Integer data = queue.dequeue();
                System.out.println(Thread.currentThread().getName() + "消费:" + data);
            }).start();
        }
    }
}

运行结果:

入队:0
入队:9
Thread-9成产:9
Thread-0成产:0
出队:0
Thread-10消费:0
入队:3
Thread-3成产:3
出队:9
出队:3
Thread-11消费:9
入队:4
Thread-13消费:3
入队:1
Thread-4成产:4
出队:4
Thread-1成产:1
出队:1
Thread-16消费:4
Thread-17消费:1
入队:2
Thread-2成产:2
出队:2
Thread-18消费:2
入队:6
Thread-6成产:6
出队:6
Thread-14消费:6
入队:5
Thread-5成产:5
出队:5
Thread-12消费:5
入队:7
Thread-7成产:7
出队:7
Thread-15消费:7
入队:8
Thread-8成产:8
出队:8
Thread-19消费:8

总结

ReentrantLock是可重入的独占锁。比起synchronized功能更加丰富,支持公平锁实现,支持中断响应以及限时等待等等。可以配合一个或多个Condition条件方便的实现等待通知机制。

© 著作权归作者所有

猿来狮码农
粉丝 2
博文 53
码字总数 60690
作品 0
杭州
程序员
私信 提问
Java并发编程序列之JUC中的ReentrantLock-ReentrantReadWriteLock

Java并发编程序列之JUC中的ReentrantLock-ReentrantReadWriteLock Hello,大家好,之前两篇关于JUC中AQS的介绍算上大致把AQS是什么,自定义Lock怎么实现讲清楚了,这一篇就来说一说JDK中自带...

2017/12/27
0
0
ReentrantLock(重入锁)功能详解和应用演示

1. ReentrantLock简介 jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而...

takumiCX
2018/07/19
0
0
Java中的公平锁和非公平锁实现详解

前言 ReentrantLock的可重入性分析 synchronized的可重入性 ReentrantLock的可重入性 ReentrantLock锁的实现分析 公平锁和非公平锁 公平锁FairSync 非公平锁NonfairSync ReentrantLock锁的释...

kim_o
2018/06/08
80
0
再一次理解ReentrantLock

1. ReentrantLock的介绍 ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不...

你听___
2017/11/07
0
0
彻底理解ReentrantLock

ReentrantLock的介绍 ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会...

群星纪元
04/03
24
0

没有更多内容

加载失败,请刷新页面

加载更多

采购单品汇总_华南.xlsx

import pandas as pdimport matplotlib.pyplot as pltimport matplotlib as mp1mp1.rcParams["font.family"] = "STFangsong"# 加载《销售》表数据df1 = pd.read_excel(r"C:\Us......

龙玉滕
今天
5
0
OSChina 周五乱弹 —— 一次四千 要4次还能多给一千

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @这次装个文艺青年吧 :#今日歌曲推荐# 分享金志文的单曲《远走高飞》: 版权又回来了现在听歌得好几个软件 《远走高飞》- 金志文 手机党少年们...

小小编辑
今天
5
0
Spring Cloud Alibaba 实战(十) - Spring Cloud GateWay

> 本文主要内容是:为什么要使用网关,整合Gateway,Gateway核心学习:Route,Predicate,Filter,最后使用Gateway聚合微服务请求 先总结至此的架构 1 网关的价值 不使用网关行嘛? 各个请求直接打在...

JavaEdge
今天
4
0
【CKB.DEV 茶话会】第二期:聊聊 CKB 钱包和 Nervos DAO 全流程

CKB.DEV 茶话会第二期:聊聊 CKB 钱包和 Nervos DAO 全流程 为了鼓励更多优秀的开发者和研究人员参与到 CKB 的开发和生态建设中去,我们希望组织一系列 CKB Developer Seminar(CKB.DEV 茶话...

NervosCommunity
今天
4
0
聊聊rocketmq的HAClient

序 本文主要研究一下rocketmq的HAClient HAClient rocketmq-all-4.6.0-source-release/store/src/main/java/org/apache/rocketmq/store/ha/HAService.java class HAClient extends Serv......

go4it
昨天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部