zoukankan      html  css  js  c++  java
  • ReentrantLock(重入锁)功能详解和应用演示

    目录


    一.ReentrantLock简介

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

    接口:

    Modifier and Type Method Description
    void lock() 获取锁
    void lockInterruptibly() 除非当前线程被中断,否则获取锁定
    Condition newCondition() 返回绑定到此Lock实例的新Condition实例
    boolean tryLock() 只有在调用时它是空闲的才能获取锁
    boolean tryLock(long time, TimeUnit unit) 如果在给定的等待时间内空闲并且当前线程未被中断,则获取锁
    void unlock() 释放锁

    二.ReentrantLock和synchronized的比较 ↑top

    1.相同点 ↑top

    • a.它们都是加锁方式同步;
    • b.都是重入锁;
    • c.阻塞式的同步;也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的

    可重入性

    public class ReentrantLockTest {
    
        public static void main(String[] args) throws InterruptedException {
    
            ReentrantLock lock = new ReentrantLock();
    
            for (int i = 1; i <= 3; i++) {
                lock.lock();
            }
    
            for(int i=1;i<=3;i++){
                try {
    
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    

    上面的代码通过lock()方法先获取锁三次,然后通过unlock()方法释放锁3次,程序可以正常退出。

    从上面的例子可以看出,ReentrantLock是可以重入的锁,当一个线程获取锁时,还可以接着重复获取多次。在加上ReentrantLock的的独占性,我们可以得出以下ReentrantLock和synchronized的相同点。

    • 1.ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。
    • 2.ReentrantLock和synchronized都是可重入的。synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。

    2.不同点 ↑top

    比较方面 SynChronized ReentrantLock(实现了 Lock接口)
    原始构成 它是java语言的关键字,是原生语法层面的互斥,需要jvm实现 它是JDK 1.5之后提供的API层面的互斥锁类
    代码编写 采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用,更安全, 而ReentrantLock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。需要lock()和unlock()方法配合try/finally语句块来完成,
    灵活性 锁的范围是整个方法或synchronized块部分 Lock因为是方法调用,可以跨方法,灵活性更大
    等待可中断 不可中断,除非抛出异常(释放锁方式:
    1.代码执行完,正常释放锁;
    2.抛出异常,由JVM退出等待
    持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,(方法:

    1.设置超时方法 tryLock(long timeout, TimeUnit unit),时间过了就放弃等待;

    2.lockInterruptibly()放代码块中,调用interrupt()方法可中断,而synchronized不行)
    是否公平锁 非公平锁 两者都可以,默认公平锁,构造器可以传入boolean值,true为公平锁,false为非公平锁,
    条件Condition 通过多次newCondition可以获得多个Condition对象,可以简单的实现比较复杂的线程同步的功能.
    提供的高级功能 提供很多方法用来监听当前锁的信息,如:
    getHoldCount() getQueueLength() isFair() isHeldByCurrentThread() isLocked()

    三. ReentrantLock相比synchronized的额外功能 ↑top

    3.1 ReentrantLock可以实现公平锁。 ↑top

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

    ReentrantLock lock = new ReentrantLock(true);
    

    继续跟进看下源码

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }	
    

    可以看到公平锁和非公平锁的实现关键在于成员变量sync的实现不同,这是锁实现互斥同步的核心。

    公平锁例子:

    public class ReentrantLockTest {
    
        static Lock lock = new ReentrantLock(true);
    
        public static void main(String[] args) throws InterruptedException {
    
            for(int i=0;i<5;i++){
                new Thread(new ThreadDemo(i)).start();
            }
    
        }
    
        static class ThreadDemo implements Runnable {
            Integer id;
    
            public ThreadDemo(Integer id) {
                this.id = id;
            }
    
            @Override
    
          public void run() {
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for(int i=0;i<2;i++){
                    lock.lock();
                    System.out.println("获得锁的线程:"+id);
                    lock.unlock();
                }
            }
        }
    }
    

    公平锁结果:

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

    我们开启5个线程,让每个线程都获取释放锁两次。为了能更好的观察到结果,在每次获取锁前让线程休眠100毫秒。可以看到线程几乎是轮流的获取到了锁。如果我们改成非公平锁,再看下结果:

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

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

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

    3.2ReentrantLock可响应中断 ↑top

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

    public class ReentrantLockDemo02 implements Runnable {
    
        private Lock lock = new ReentrantLock();
    
        @Override
        public void run() {
            try {
                lock.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + " running");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + " finished");
                lock.unlock();
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " interrupted");
            }
        }
    
        public static void main(String[] args) {
            ReentrantLockDemo02 reentrantLockDemo = new ReentrantLockDemo02();
            Thread thread01 = new Thread(reentrantLockDemo, "thread01");
            Thread thread02 = new Thread(reentrantLockDemo, "thread02");
            thread01.start();
            thread02.start();
            thread02.interrupt();
        }
    }
    

    结果:

    thread01 running
    thread02 interrupted
    thread01 finished
    

    3.3ReentrantLock获取锁限时等待 ↑top

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

    public class ReentrantLockDemo03 implements Runnable {
    
        private ReentrantLock lock = new ReentrantLock();
    
        @Override
        public void run() {
            try {
                if (lock.tryLock(2, TimeUnit.SECONDS)) {
                    System.out.println(Thread.currentThread().getName() + " 获取当前lock锁");
                    TimeUnit.SECONDS.sleep(4);
                } else {
                    System.out.println(Thread.currentThread().getName()+ " 获取锁失败");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        }
    
    
        public static void main(String[] args) {
            ReentrantLockDemo03 reentrantLockDemo = new ReentrantLockDemo03();
            Thread thread01 = new Thread(reentrantLockDemo, "thread01");
            Thread thread02 = new Thread(reentrantLockDemo, "thread02");
            thread01.start();
            thread02.start();
        }
    }
    

    运行结果:

    thread02 获取当前lock锁
    thread01 获取锁失败
    

    线程2运行,首先获得锁,休眠4秒钟后释放所,线程1如果在2秒内如果没有获取到锁,则会输出获取锁失败

    如果将线程2的休眠时间小于1秒或者线程1的等待时间大于4秒此时两者都会成功

    thread02 获取当前lock锁
    thread01 获取当前lock锁
    

    四. 结合Condition实现等待通知机制 ↑top

    4.1 Condition使用简介 ↑top

    Condition由ReentrantLock对象创建,并且可以同时创建多个

    static Condition notEmpty = lock.newCondition();
    
    static Condition notFull = lock.newCondition();		
    

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

    ublic class ConditionTest {
    
        static ReentrantLock lock = new ReentrantLock();
        static Condition condition = lock.newCondition();
        public static void main(String[] args) throws InterruptedException {
    
            lock.lock();
            new Thread(new SignalThread()).start();
            System.out.println("主线程等待通知");
            try {
                condition.await();
            } finally {
                lock.unlock();
            }
            System.out.println("主线程恢复运行");
        }
        static class SignalThread implements Runnable {
    
            @Override
            public void run() {
                lock.lock();
                try {
                    condition.signal();
                    System.out.println("子线程通知");
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    

    运行结果:

    主线程等待通知
    子线程通知
    主线程恢复运行
    

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

    阻塞队列是一种特殊的先进先出队列,它有以下几个特点
    1.入队和出队线程安全
    2.当队列满时,入队线程会被阻塞;当队列为空时,出队线程会被阻塞。

    public class MyBlockingQueue<E> {
    
        int size;//阻塞队列最大容量
    
        ReentrantLock 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) throws InterruptedException {
            lock.lock();
            try {
                while (list.size() ==size)//队列已满,在notFull条件上等待
                    notFull.await();
                list.add(e);//入队:加入链表末尾
                System.out.println("入队:" +e);
                notEmpty.signal(); //通知在notEmpty条件上等待的线程
            } finally {
                lock.unlock();
            }
        }
    
        public E dequeue() throws InterruptedException {
            E e;
            lock.lock();
            try {
                while (list.size() == 0)//队列为空,在notEmpty条件上等待
                    notEmpty.await();
                e = list.removeFirst();//出队:移除链表首元素
                System.out.println("出队:"+e);
                notFull.signal();//通知在notFull条件上等待的线程
                return e;
            } finally {
                lock.unlock();
            }
        }
    }
    

    测试代码:

    public static void main(String[] args) throws InterruptedException {
    
        MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(2);
        for (int i = 0; i < 10; i++) {
            int data = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        queue.enqueue(data);
                    } catch (InterruptedException e) {
    
                    }
                }
            }).start();
    
        }
        for(int i=0;i<10;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Integer data = queue.dequeue();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    
    }
    

    运行结果:

    入队:3
    出队:3
    入队:4
    入队:8
    出队:4
    入队:1
    出队:8
    入队:0
    出队:1
    出队:0
    入队:9
    出队:9
    入队:2
    入队:5
    出队:2
    出队:5
    入队:7
    入队:6
    出队:7
    出队:6
    

    五.总结 ↑top

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

    什么情况下使用ReenTrantLock:

    答案是,如果你需要实现ReenTrantLock的三个独有功能时。

    参考:

    主参考: https://www.cnblogs.com/takumicx/p/9338983.html

    辅助参考:

    详细解释:

  • 相关阅读:
    Vue3源码系列之触发更新的实现
    Vue3源码系列之依赖收集的实现
    Vue3源码系列之reactiveApi实现
    删除链表的倒数第n个节点
    Shared_ptr 参考实现
    linux 目录结构 比较老
    C++11 bind function
    状态机DP
    尾递归
    秒杀系统的构建(2)
  • 原文地址:https://www.cnblogs.com/eternityz/p/12238808.html
Copyright © 2011-2022 走看看