3、Java并发面试题(二)- 挑战高薪必备

85 阅读5分钟

4、ReentrantLock底层是怎么实现的?

ReentrantLock继承体系

首先来看一下继承体系:

image.png

从上图我们可以看到,ReetrantLock本身只是实现了Lock接口,但是如何去实现一把完整的锁相应的功能呢,其实底层是借助于Sync这个静态内部类的对象来实现的。

image.png

这个Sync是一个同步器,有两种不同的实现

  • FairSync 公平锁的同步器
  • NonfairSync 非公平锁的同步器

ReentrantLock可以是一把公平锁,实现先来后到的抢锁机制。也可以非公平锁,允许锁释放的时候,刚来的线程去抢一抢锁,这样就可以避免阻塞和唤醒的开销,性能更高,但是容易出现饥饿现象,之前排队的线程可能由于新来的线程抢锁而一直拿不到锁。

image.png

AQS原理

那么同步器是如何实现的呢?它的底层基于一种叫AQS队列同步器来完成的。

image.png

在Java并发包也就是JUC的包中,AQS实现了很多种线程安全的容器以及锁,具体如下:

image.png

AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任

意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 ASQ 队列中去;当获取锁的线程释放锁以

后,会从队列中唤醒一个阻塞的节点(线程)。

static final class Node {
    /** Marker to indicate a node is waiting in shared mode */
    static final Node SHARED = new Node();
    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;

    /** waitStatus value to indicate thread has cancelled. */
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking. */
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition. */
    static final int CONDITION = -2;
    /**
     * waitStatus value to indicate the next acquireShared should
     * unconditionally propagate.
     */
    static final int PROPAGATE = -3;

   
    volatile int waitStatus;
    
    //上一个节点
    volatile Node prev;

    //下一个节点
    volatile Node next;

    /**
     * The thread that enqueued this node.  Initialized on
     * construction and nulled out after use.
     */
    volatile Thread thread;

    Node nextWaiter;

    /**
     * Returns true if node is waiting in shared mode.
     */
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    /**
     * Returns previous node, or throws NullPointerException if null.
     * Use when predecessor cannot be null.  The null check could
     * be elided, but is present to help the VM.
     *
     * @return the predecessor of this node
     */
    final Node predecessor() {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

这些节点中保存的就是目前AQS中并发去抢锁的线程,有抢到的,也有没有抢到阻塞的,他们最终抢锁的结果会决定state(图中是status)状态的值,在reentrantlock中,> 0 代表有线程持有锁并记录重入的次数,0代表无人持有锁。 image.png

每一个子类都必须重写如下方法:

//独占式获取同步状态,实现该方法须查询并判断当前状态是否符合预期,然后再进行CAS设置状态。
protected boolean tryAcquire (int arg) 
 
//独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态。
protected boolean tryRelease (int arg) 
 
//共享式获取同步状态,返回大于0的值表示获取成功,反之获取失败。
protected int tryAcquireShared (int arg)
 
//共享式释放同步状态。
protected boolean tryReleaseShared (int arg)
 
//当前同步器是否再独占模式下被线程占用,一般用来表示是否被当前线程独占。
protected boolean isHeldExclusively ()

非公平锁的实现

我们以非公平锁为例看下具体的实现:

image.png

有两个小细节:

1、如果当前持有锁的线程就是本线程,会产生重入,state值会加1,所以state记录了重入的次数。

2、使用CAS的方式更新持有锁的线程引用,保证原子性,即便是在多线程情况下也能保证线程安全。

公平锁的实现

那么公平锁的实现有什么区别呢?

公平锁如果state == 0 情况下,会先判断队列中有没有线程,优先保证队列中的线程去处理,保证先来后到的公平性。 image.png

5、CAS是如何实现的?

什么是CAS?

CAS:Compare and Swap,即比较再交换。

CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

CAS比较与交换的伪代码可以表示为:

do{ 备份旧数据; 基于旧数据构造新数据; }
while(!CAS( 内存地址,备份的旧数据,新数据 ))

image.png

CAS在CPU中有对应的指令来进行处理,JVM对CPU的指令进行了封装。如图中,CPU1想要将内存中的100值修改为101,此时CPU2也执行指令将100修改为102,同一时间只会有一个线程成功。图上为cpu1执行成功,内存中的值被修改为了101,而cpu2判断内存中的值已经不是100了,所以就造成了失败。

这种方式可以使用无锁的方式进行数据的修改,在JVM中大量使用了CAS,而并发包中所有Atomic为前缀的类,都是使用了CAS机制来保证原子性操作的类。

image.png