要不要来点 "AQS"

2020-05-30  本文已影响0人  JackDaddy
之前讲了多线程,线程池,各种锁,以及 Java 中的 synchronized 关键字,但是在 Java 中,锁屎是如何实现的,synchronized 这个关键字是如何实现线程同步的呢
今天我们要来探讨一下 synchronized 以及 锁 是如何实现线程同步的。

今天我决定采用倒叙的写法来阐述这篇文章。synchronized 以及 Java 中的锁是通过一个AQS 的类来实现同步的,那么问题就是转变成AQS是怎么去实现线程同步的。

AQS是什么

AQS 的全称是 AbstractQueuedSynchronizer,他是用来构建的Java中锁和其他同步组件一个基本框架。

那么 AQS 是怎么发挥他的作用的呢?

因为上面说到 Java 中的锁机制都是通过 AQS 来实现的,所以接下来经常用到的 ReentrantLock ( 可重入锁 )来进行讲解。
首先进入的到 ReentrantLock 的源码,我们都知道 ReentrantLock 是实现了 Lock 接口,因此会实现 Lock 接口中的方法:

//ReentrantLock  实现了Lock接口
public class ReentrantLock implements Lock, java.io.Serializable

那么我们来看下在实现方法中,ReentrantLock 做了什么,进入到 ReentrantLock 的 lock 方法中:

//这里的 lock 方法为实现了 Lock 接口的方法
public void lock() {
        sync.lock();
    }

在这里我们可以看到在 lock 方法里面调用了 sync 的 lock 方法,所以接下来看下这个 sync 是什么:

abstract static class Sync extends AbstractQueuedSynchronizer{
...
      abstract void lock();
      //以下是 AQS 中的方法,重写里面的某些方法完成我们的需求
      protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
...
}

从上面可以看到,在 ReentrantLock 内部定义了 一个静态抽象内部类,这个内部类继承了 AbstractQueuedSynchronizer,然后在这个类里面去实现 AQS(AbstractQueuedSynchronizer) 的方法,接着通过让其他类继承 Sync 从而实现线程同步。下面通过非公平锁来举例说明:

static final class NonfairSync extends Sync {
        //这个UID是为了序列化的版本号使用的
        private static final long serialVersionUID = 7316153563782823691L;
        //实现 sync 中的抽象方法
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        //重写 AQS 中的 tryAcquire 方法
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

通过 NonfairSync 这个类继承 Sync 从而来实现 AQS 的里面的锁机制。我们可以看到 ReentrantLock 的构造方法里根据传入的布尔值来实例不同继承了 Sync 的子类

//父类Sync —依赖倒置原则
private final Sync sync;

//默认是非公平锁
public ReentrantLock() {
        sync = new NonfairSync();
 }
//根据传入的布尔值来判断实例哪个对象
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
  }

**讲完 AQS 在 Java 中锁是怎么是发挥作用的,接下来来看看 AQS 中的实现原理:

  private volatile int state;

可以发现这是一个 int 型带有 volatile 关键字的变量。因为带有 volatile 关键字,因此当state 一发生改变就会刷新到主存里面,每次需要使用 state 时都从主存里面去拿数据。

这个变量表示的是锁的状态(当 state = 0 时表示当前没有线程持有锁,当state >= 1时表示有线程正持有锁)

每次加锁,释放锁都会通过这个 state 来进行判断,因此就需要以下几个重要方法:**

  // 获取 锁 状态
  protected final int getState() {
        return state;
  }

  //这里使用到了一个类似CAS 的操作(对比和设置)使用这个方法能保证原子性,用于每次加锁,解锁的时候对 state 设置状态
  protected final boolean compareAndSetState(int expect, int update) {
        return U.compareAndSwapInt(this, STATE, expect, update);
 }

  //独占式获取同步状态,先判断状态是否符合预期,然后再进行CAS设置同步状态
  protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

  //独占式释放同步状态,等待获取同步状态的线程有机会获得同步状态
  protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

从前面源码可以知道 ReentrantLock 中可以根据传入的布尔值设置为公平锁与非公平锁,那具体是怎么实现的呢:

  //公平锁实现方式
  protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //重点关注 hasQueuedPredecessors 这一条件,这里是和非公平锁唯一不同
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //这里判断是否是当前线程
            else if (current == getExclusiveOwnerThread()) {
                //如果是当前线程则 +1 
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
      }

  //非公平锁实现方式
  final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //可以看出加锁操作都是根据 state 这个关键字来判断的
            int c = getState();
            //当 state == 0 时表示没有其线程获取到锁
            if (c == 0) {
                //可以发现这里相对于公平锁少了 hasQueuedPredecessors  这一条件
                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;
        }

=========================================
  //这个方法是在公平锁与非公平锁的区别。在 ReentrantLock 维护有一条链表,把新加入的线程放在链表的尾端,在每次加锁前会判断
//该线程前面是否还有其他线程,没有则执行,有则阻塞在队列中等待;
  public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

从上面可以看到 AQS 里 state 关键字是用来判断能否加锁的依据,以及通过 compareAndSetState ,setState 等方法来设置新的 state 值。

CLH队列锁

CLH 队列锁是一个基于链表的高可用的,高性能的,公平的自旋锁,当申请线程时,仅在本地变量上,不断轮询前驱节点的锁状态,当轮询到前驱锁释放,当前节点就停止自旋,获取节点:其中,一个节点包含前驱节点的引用,以及是否需要获取锁的状态,需要获取锁时则将 locked 置为 true。

节点 当线程 A 加入队列时,对 tail 域调用 getAndSet() 方法,使线程A 成为队列尾部,同时获得上一节点的引用:(其中不断对前驱节点的引用自旋轮询myPred = true 表示轮询时前驱获取到锁,locked = true 表示线程A 需要获得锁) 线程A 当轮询到发现前驱节点释放锁时,线程A 就获取到了锁: 线程A获取到锁

而在 Java 中 CLH 是 AQS 的一个变体实现。

上一篇下一篇

猜你喜欢

热点阅读