• 并发包(JUC)之Lock和AQS


    1、Lock接口的实现——并发包锁

    (1)ReentrantLock

      重入锁,重入锁指线程在获得锁之后,再次获得该锁不需要阻塞,而是直接关联一次计数器增加重入次数。

    (2)ReentrantReadWriteLock

      重入读写锁,实现了ReadWriteLock接口,它维护两个锁,一个ReadLock,一个WriteLock,这两个都实现了Lock。基本原则是:读和读不互斥,读和写互斥,写和写互斥。适合读多写少的场景。

    (3)StampedLock

      jdk1.8引入的新的锁机制,可以认为是对ReentrantReadWriteLock的改进版本。解决大量的读线程存在,可能会引起写线程的饥饿的问题。stampedLock是一种乐观的读策略,使得乐观锁完全不会阻塞写线程。

    2、ReentrantLock的实现原理

      重入锁的设计目的:比如调用demo方法获得了当前的对象锁,然后在这个方法中再去调用 demo2,demo2中的存在同一个实例锁,这个时候当前线程会因为无法获得 demo2的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死锁。

      关系图:

            

    (1)AQS 

      全称 AbstractQueuedSynchronizer,是实现Lock的核心组件。从功能层面分为两种:独占和共享。

        *  独占锁,ReentrantLock就是以独占方式实现的互斥锁。

        *  共享锁,如ReentrantReadWriteLock。

      AQS内部实现:AQS队列内部维护了一个FIFO的双向链表,双向链表可以从任意一个节点访问很方便地访问前驱和后继节点(非公平锁很好地利用了这一点特性),每个节点(Node)由线程和等待状态封装而成,当线程争抢锁是被,会被封装成Node添加到链表末端。

          

       Node节点

    static final class Node {
            static final Node SHARED = new Node();
            static final Node EXCLUSIVE = null;
            static final int CANCELLED =  1;
            static final int SIGNAL    = -1;
            static final int CONDITION = -2;
            static final int PROPAGATE = -3;
            volatile int waitStatus;
            volatile Node prev; //前驱节点
            volatile Node next; //后继节点
            volatile Thread thread;//当前线程
            Node nextWaiter; //存储在condition队列中的后继节点
            //是否为共享锁
            final boolean isShared() { 
                return nextWaiter == SHARED;
            }
    
            final Node predecessor() throws NullPointerException {
                Node p = prev;
                if (p == null)
                    throw new NullPointerException();
                else
                    return p;
            }
    
            Node() {    // Used to establish initial head or SHARED marker
            }
            //将线程构造成一个Node,添加到等待队列
            Node(Thread thread, Node mode) {     // Used by addWaiter
                this.nextWaiter = mode;
                this.thread = thread;
            }
            //这个方法会在Condition队列使用,后续单独写一篇文章分析condition
            Node(Thread thread, int waitStatus) { // Used by Condition
                this.waitStatus = waitStatus;
                this.thread = thread;
            }
        }
    

    (2)结合AQS看ReentrantLock实现原理  

      假设ThreadA、B、C三个线程同时访问一个同步块:

        

       如果ThreadA通过CAS获得了锁,此时state=1,exclusiveOwnerThread=ThreadA。ThreadB和ThreadC会被封装成Node节点,形成双向链表。

        

    (3)公平锁和非公平锁(默认)

      FairSync在尝试获取锁时,多了一个hasQueuePredecessors判断,也就是如果当前线程(Node)有前置节点,则不会进行CAS去竞争锁,NonFairSync则不会做这个检查,直接通过CAS去竞争锁。

    (4)ReentrantLock偏向锁的巧妙实现

    final boolean nonfairTryAcquire(int acquires) {  
        final Thread current = Thread.currentThread();  
        int c = getState();  
        if (c == 0) {  
            if (compareAndSetState(0, acquires)) {  
                setExclusiveOwnerThread(current);  
                return true;  
            }  
        }  
        else if (current == getExclusiveOwnerThread()) {  
            int nextc = c + acquires;  
            if (nextc < 0) // overflow  
                throw new Error("Maximum lock count exceeded");  
            setState(nextc);  
            return true;  
        }  
        return false;  
    }  
    

      nonfairTryAcquire方法是lock方法间接调用的第一个方法,每次请求锁时都会首先调用该方法。该方法会首先判断当前状态,如果c==0说明没有线程正在竞争该锁,如果c !=0 说明有线程正拥有了该锁,但如果发现是自己拥有该锁的话,只是简单地++acquires,并修改status值,但因为没有竞争,所以通过setStatus修改,而非CAS,也就是说这段代码实现了偏向锁的功能。

    (5)ReentrantLock和synchronized区别

      *  AbstractQueuedSynchronizer通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过CAS操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。

      *  synchronized的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程的出列速度;当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优化,而Lock则完全依靠系统阻塞挂起等待线程。

      *  Lock比synchronized更适合在应用层扩展,可以继承AbstractQueuedSynchronizer定义各种实现,比如实现读写锁(ReadWriteLock),公平或不公平锁;同时,Lock对应的Condition也比wait/notify要方便的多、灵活的多。

     3、Synchronized与ReentrantLock实现原理有何不同?

      锁的实现原理基本是为了达到一个目的:让所有的线程都能看到某种标记。Synchronized通过在对象头中设置标记实现了这一目的,是一种JVM原生的锁实现方式,而ReentrantLock以及所有的基于Lock接口的实现类,都是通过用一个volitile修饰的int型变量,以此来保证每个线程都能拥有对该int变量的可见性和原子修改,其本质是基于AQS框架。

    public class ReentrantLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = 7373984872572414699L;
        /** Synchronizer providing all implementation mechanics */
        private final Sync sync;
    
        abstract static class Sync extends AbstractQueuedSynchronizer
    public abstract class AbstractQueuedSynchronizer
        extends AbstractOwnableSynchronizer
        implements java.io.Serializable {
    
        static final class Node {
      
            private volatile int state;
    }
    

     推荐文章:

    1、深入剖析ReentrantLock:https://mp.weixin.qq.com/s/XMsFNCB0m7eTlH56ipZL7A

    2、AQS源码分析:https://mp.weixin.qq.com/s/6Znq_SDZwzwifIIUSBRcMg

      

  • 相关阅读:
    php.ini中设置session过期时间
    IP(Internet Protocal) 地址 说明
    html年月日下拉联动菜单 年月日三下拉框联动
    使用数组的键值,做为变量名的方法
    html中js只允许输入数字
    阿里云服务器问题攻略
    小帆远行
    Android图片转换类 1. Bitmap去色,转换为黑白的灰度图, 2. Bitmap图片加圆角效果
    EditText禁止输入回车
    Android之系统自带的文字外观设置
  • 原文地址:https://www.cnblogs.com/jing-yi/p/12768678.html
Copyright © 2020-2023  润新知