ReentrantLock(重入锁)功能详解和应用演示

Posted eternityz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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

辅助参考:

  • https://blog.csdn.net/u014730165/article/details/82144848
  • https://blog.csdn.net/weixin_40792878/article/details/81369385

详细解释:

  • https://www.cnblogs.com/zhimingyang/p/5702752.html
  • https://www.jianshu.com/p/c021f144a565
  • https://blog.csdn.net/wuyangyang555/article/details/80984428

以上是关于ReentrantLock(重入锁)功能详解和应用演示的主要内容,如果未能解决你的问题,请参考以下文章

Java 重入锁 ReentrantLock 原理分析

ReentrantLock 重入锁(下)

ReentrantLock重入锁

重入锁 ReentrantLock (转)(学习记录)

JAVA多线程重入锁ReentrantLock应用

ReentrantLock (重入锁) 源码浅析