关于ReentrantLock

Posted zjoe80

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于ReentrantLock相关的知识,希望对你有一定的参考价值。

一.ReentrantLock是什么

ReentrantLock是一个可重入的互斥锁(Reentrant就是再次进入的意思),又被称为“独占锁”。它添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。

ReentrantLock在同一个时间点只能被一个线程获取(当某线程获取到“锁”时,其它线程就必须等待)。但是它可以被单个线程多次获取,每获取一次AQSstate就加1,每释放一次state就减1。

ReentrantLock分为“公平锁”和“非公平锁”。在公平锁上,线程按照他们发出请求的顺序获取锁,但在非公平锁上则允许“插队”。

ReentraantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的。在“公平锁”的机制下,线程依次排队获取锁;而“非公平锁”在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。

相关术语:

ReentrantLock:可重入锁;

AQS:AbstractQueuedSynchronized 抽象类,队列式同步器;

CAS:Compare and Swap, 比较并交换值;

CLH队列:The wait queue is a variant of a "CLH" (Craig, Landin, and* Hagersten) lock queue。

 

二.ReentrantLock能做什么

1、可中断锁的同步执行

synchronized关键字只能支持单条件(condition)、比如10个线程都在等待synchronized块锁定的资源、如果一直锁定、则其他线程都得不到释放从而引起死锁;

而同样的情况使用ReentrantLock、则允许其他线程中断放弃尝试。reentrantlock本身支持多wait/notify队列、它可以指定notify某个线程。

ReentrantLock lock = new ReentrantLock(true); //公平锁  
lock.lockInterruptibly();  
try {  
    //操作  
} catch (InterruptedException e) {  
    e.printStackTrace();  
} finally {  
    lock.unlock();  
}  

2、防止重复执行(忽略重复触发)

ReentrantLock lock = new ReentrantLock();  
if (lock.tryLock()) {  //如果已经被lock,则立即返回false不会等待,达到忽略操作的效果   
    try {  
        //操作  
    } finally {  
        lock.unlock();  
    }  
}  

3、同步执行,类似synchronized

ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁  
ReentrantLock lock = new ReentrantLock(true); //公平锁  
  
lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果  
try {  
    //操作  
} finally {  
    lock.unlock();  
}  

4、尝试等待执行

通过tryLock方法来实现,可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。可以将这种方法用来解决死锁问题。

ReentrantLock lock = new ReentrantLock(true); //公平锁  
try {  
    if (lock.tryLock(5, TimeUnit.SECONDS)) {      
        //如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行  
        try {  
            //操作  
        } finally {  
            lock.unlock();  
        }  
    }  
} catch (InterruptedException e) {  
    e.printStackTrace(); //当前线程被中断时(interrupt),会抛InterruptedException                   
}  

5、可轮询

比如:一个转账的操作,要么在规定的时间内完成,要么在规定的时间内告诉调用者,操作没有完成。

这个例子就是要了ReentrantLock的可轮询特性,就是在规定的时间内,反复去试图获得一个锁,如果获得成功,就能完成转账操作,如果在规定的时间内,没有获得这个锁,那么就是转账失败。

如果使用synchronized的话,肯定是无法做到的。

 

三.ReentrantLock原理

1、在Java中通常实现锁有两种方式,一种是synchronized关键字,另一种是Lock。二者其实并没有什么必然联系,但是各有各的特点。

synchronized是基于JVM层面实现的,而Lock是基于JDK层面实现的,通过阅读JDK的源码来理解Lock的实现。

Lock是比较复杂的,需要lock和realse,如果忘记释放锁就会产生死锁的问题,所以,通常需要在finally中进行锁的释放。

但是synchronized的使用十分简单,只需要对自己的方法或者关注的同步对象或类使用synchronized关键字即可。

但是对于锁的粒度控制比较粗,同时对于实现一些锁的状态的转移比较困难。

在JDK1.5之后synchronized引入了偏向锁,轻量级锁和重量级锁,从而大大的提高了synchronized的性能。

Lock的实现主要有ReentrantLock、ReadLock和WriteLock,后两者用的不多。

ReentrantLock类在java.util.concurrent.locks包中,它的上一级的包java.util.concurrent主要是常用的并发控制类.

 

2、ReentrantLock是JDK1.5引入的,ReentrantLock的实现基于AQS(AbstractQueuedSynchronizer)和LockSupport。

AQS主要利用硬件原语指令(CAS compare-and-swap),来实现轻量级多线程同步机制,并且不会引起CPU上文切换和调度,

同时提供内存可见性和原子化更新保证(线程安全的三要素:原子性、可见性、顺序性)。

AQS的本质上是一个同步器/阻塞锁的基础框架,其作用主要是提供加锁、释放锁,并在内部维护一个FIFO等待队列,用于存储由于锁竞争而阻塞的线程。

 

3、ReentrantLock具有公平和非公平两种模式

关于公平性:

在new ReentrantLock的时候,有一个构造函数是带boolean类型的。这个参数告诉ReentrantLock是构造一个公平的锁还是不公平的锁。

其实这里的公平性是指获取锁的时候,是否允许插队。允许插队,就是创建了不公平的锁。并且,ReentrantLock默认采用的是不公平的锁。

为啥采用不公平的锁呢?应该先到先得嘛。

原因在于线程挂起。当多个线程同时请求一个锁时,未获得锁的线程B会被挂起,当锁被线程A释放时,刚好来了一个线程C,那么操作系统就需要选择,

第一,从挂起的队列中选择一个线程B,按照先到先得的原则,将锁交给它。

但是这需要很大的开销,因为那个线程B很可能正睡觉呢,或者还在做美梦呢,叫醒它还得让它热热身,等他来接锁的时候,可能黄花菜都凉了。

第二种选择,就是将锁交给刚好到来的这个线程C,刚到的线C程拿到锁就能使用。为了提高性能,操作系统选择第二个选择。

说到公平性,JDK的synchronized锁也是采用的非公平锁。

(1)公平锁

公平锁的优点是等待锁的线程不会夯死。缺点是吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

公平锁是严格的以FIFO的方式进行锁的竞争,但是非公平锁是无序的锁竞争,刚释放锁的线程很大程度上能比较快的获取到锁,队列中的线程只能等待,

(1)非公平锁

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,

所以非公平锁有可能出现后申请锁的线程先获取锁的场景。

非公平锁可能会有“饥饿”的问题。但是重复的锁获取能减小线程之间的切换,而公平锁则是严格的线程切换,这样对操作系统的影响是比较大的,

所以非公平锁的吞吐量是大于公平锁的,这也是为什么JDK将非公平锁作为默认的实现。

 

四.ReentrantLock使用

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

(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();
    }

}

运行结果:

技术图片

五.ReentrantLock总结

ReentrantLock在采用非公平锁构造时,首先检查锁状态,如果锁可用,直接通过CAS设置成持有状态,且把当前线程设置为锁的拥有者。

如果当前锁已经被持有,那么接下来进行可重入检查,如果可重入,需要为锁状态加上请求数。如果不属于上面两种情况,那么说明锁是被其他线程持有,当前线程应该放入等待队列。

在放入等待队列的过程中,首先要检查队列是否为空队列,如果为空队列,需要创建虚拟的头节点,然后把对当前线程封装的节点加入到队列尾部。

由于设置尾部节点采用了CAS,为了保证尾节点能够设置成功,这里采用了无限循环的方式,直到设置成功为止。

在完成放入等待队列任务后,则需要维护节点的状态,以及及时清除处于Cancel状态的节点,以帮助垃圾收集器及时回收。

如果当前节点之前的节点的等待状态小于1,说明当前节点之前的线程处于等待状态(挂起),那么当前节点的线程也应处于等待状态(挂起)。

挂起的工作是由LockSupport类支持的,LockSupport通过JNI调用本地操作系统来完成挂起的任务(java中除了废弃的suspend等方法,没有其他的挂起操作)。

在当前等待的线程,被唤起后,检查中断状态,如果处于中断状态,那么需要中断当前线程。

 

以上是关于关于ReentrantLock的主要内容,如果未能解决你的问题,请参考以下文章

关于ReentrantLock锁的一些理解

关于ReentrantLock锁的一些理解

关于代码片段的时间复杂度

ReentrantLock tryLock(timeout,timeUnit)无法按预期工作

ReentrantLock以及 Condition深度解析

互联网JAVA面试常问问题