• 关于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等方法,没有其他的挂起操作)。

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

  • 相关阅读:
    JavaScript面向对象之类的创建
    VSCode Debug模式下各图标 含义
    Visual Studio icon 含义
    Unity坐标系 左手坐标系 图
    Unity类继承关系 图
    Unity的SendMessage方法
    ONGUI->NGUI->UGUI (Unity UI史)
    Console.WriteLine 不会输出到unity控制台
    Chrome添加Unity本地文档引擎
    Unity "Build failed : Asset is marked as don't save " 解决方案
  • 原文地址:https://www.cnblogs.com/ZJOE80/p/12881149.html
Copyright © 2020-2023  润新知