【java多线程】4、AtomicInteger+AQS+ReentrantLock源码

Atomic

高并发的情况下,i++无法保证原子性,往往会出现问题,所以引入AtomicInteger类。

线程不安全示例

我们分别累加普通变量、volatile变量、AtomicInteger变量

public class TestAtomicInteger {
    
    
    private static final int THREADS_COUNT = 2;

    public static int count = 0;
    public static volatile int countVolatile = 0;
    public static AtomicInteger atomicInteger = new AtomicInteger(0);
    public static CountDownLatch countDownLatch = new CountDownLatch(2);

    public static void increase() {
    
    
        count++;//普通变量
        countVolatile++;//volatile变量
        atomicInteger.incrementAndGet();//AtomicInteger变量
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread[] threads = new Thread[THREADS_COUNT];
        for(int i = 0; i< threads.length; i++) {
    
    
            threads[i] = new Thread(() -> {
    
    
                for(int i1 = 0; i1 < 1000; i1++) {
    
    
                    increase();
                }
                countDownLatch.countDown();
            });
            threads[i].start();
        }

        // 等线程全部算完再输出
        countDownLatch.await();

        // 正确值2000
        System.out.println(count);//普通变量 输出1977
        System.out.println(countVolatile);//volatile变量 输出1990
        System.out.println(atomicInteger.get());//AtomicInteger变量 输出2000
    }
}

AtomicInteger源码

  • 类内部维护一个Unsafe对象,改变值通过Unsafe的CAS方法保证
package java.util.concurrent.atomic;
import java.util.function.IntUnaryOperator;
import java.util.function.IntBinaryOperator;
import sun.misc.Unsafe;

public class AtomicInteger extends Number implements java.io.Serializable {
    
    
    private static final long serialVersionUID = 6214790243416807050L;

    // 使用Unsafe类的CAS去更新值:Unsafe.compareAndSwapInt
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    
    // 被volatile修饰 // 保证可见性和不可重排序
    private volatile int value;

    // 类加载时运行
    static {
    
    
        try {
    
    
            // 通过Unsafe计算出AtomicInteger类的value属性在对象中的偏移,该偏移值下边会用到
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {
    
     throw new Error(ex); }
    }


    public AtomicInteger(int initialValue) {
    
    
        value = initialValue;
    }
    public AtomicInteger() {
    
    //初始值为0
    }

    public final int get() {
    
    
        return value;
    }
    public final void set(int newValue) {
    
    
        value = newValue;
    }

    /**
     1.首先set()是对volatile变量的一个写操作, 我们知道volatile的write为了保证对其他线程的可见性会追加以下两个Fence(内存屏障)
1)StoreStore // 在intel cpu中, 不存在[写写]重排序, 这个可以直接省略了
2)StoreLoad // 这个是所有内存屏障里最耗性能的
注: 内存屏障相关参考Doug Lea大大的cookbook (http://g.oswego.edu/dl/jmm/cookbook.html)

2.Doug Lea大大又说了, lazySet()省去了StoreLoad屏障, 只留下StoreStore
     */
    // 调用unsafe.putOrderedInt
    public final void lazySet(int newValue) {
    
    
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }

    // 设置新值,并返回旧值
    public final int getAndSet(int newValue) {
    
    
        // 用unsafe类进行操作
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }

    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
    
    
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * <p><a href="package-summary.html#weakCompareAndSet">May fail
     * spuriously and does not provide ordering guarantees</a>, so is
     * only rarely an appropriate alternative to {@code compareAndSet}.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful
     */
    public final boolean weakCompareAndSet(int expect, int update) {
    
    
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    /*
    采用CAS机制,不断使用compareAndSwapInt尝试修改该值,如果失败,重新获取。如果并发量小,问题不大。
    并发量大的情况下,由于真正更新成功的线程占少数,容易导致循环次数过多,浪费时间。
    由于需要保证变量真正的共享,缓存行失效,缓存一致性开销变大。
    底层开销可能较大,这个我就不追究了。
    该函数做的事较多,不仅增加value,同时还给出返回值,返回值换成void就好了。
    */
    public final int getAndIncrement() {
    
    //返回旧值
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    /*
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;	
        do {//从主内存拿最新的值赋值给v5
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
    */
    public final int getAndDecrement() {
    
    
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }
    public final int getAndAdd(int delta) {
    
    
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

    public final int incrementAndGet() {
    
    //返回新值
        // this是调用的变量,valueOffset主内存的数的地址,1是+1
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    public final int decrementAndGet() {
    
    
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }
    public final int addAndGet(int delta) {
    
    
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

    /**
     * Atomically updates the current value with the results of
     * applying the given function, returning the previous value. The
     * function should be side-effect-free, since it may be re-applied
     * when attempted updates fail due to contention among threads.
     *
     * @param updateFunction a side-effect-free function
     */
    // 该方法需要实现IntUnaryOperator接口,然后会调用applyAsInt方法对当前值进行处理,将当前值替换为applyAsInt方法的返回值。
    public final int getAndUpdate(IntUnaryOperator updateFunction) {
    
    //返回旧值
        int prev, next;
        do {
    
    
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while (!compareAndSet(prev, next));
        return prev;
    }

    /**
     * Atomically updates the current value with the results of
     * applying the given function, returning the updated value. The
     * function should be side-effect-free, since it may be re-applied
     * when attempted updates fail due to contention among threads.
     *
     * @param updateFunction a side-effect-free function
     */
    public final int updateAndGet(IntUnaryOperator updateFunction) {
    
    //返回新值
        int prev, next;
        do {
    
    
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while (!compareAndSet(prev, next));
        return next;
    }

    /**
     * Atomically updates the current value with the results of
     * applying the given function to the current and given values,
     * returning the previous value. The function should be
     * side-effect-free, since it may be re-applied when attempted
     * updates fail due to contention among threads.  The function
     * is applied with the current value as its first argument,
     * and the given update as the second argument.
     *
     * @param x the update value
     * @param accumulatorFunction a side-effect-free function of two arguments
     */
     // 返回旧值
    public final int getAndAccumulate(int x,//更新值
                                      IntBinaryOperator accumulatorFunction) {
    
    
        int prev, next;
        do {
    
    
            prev = get();
            next = accumulatorFunction.applyAsInt(prev, x);
        } while (!compareAndSet(prev, next));
        return prev;
    }

    /**
     * Atomically updates the current value with the results of
     * applying the given function to the current and given values,
     * returning the updated value. The function should be
     * side-effect-free, since it may be re-applied when attempted
     * updates fail due to contention among threads.  The function
     * is applied with the current value as its first argument,
     * and the given update as the second argument.
     *
     * @param x the update value
     * @param accumulatorFunction a side-effect-free function of two arguments
     * @return the updated value
     */
    public final int accumulateAndGet(int x,
                                      IntBinaryOperator accumulatorFunction) {
    
    
        int prev, next;
        do {
    
    
            prev = get();
            next = accumulatorFunction.applyAsInt(prev, x);
        } while (!compareAndSet(prev, next));
        return next;
    }


    public String toString() {
    
    
        return Integer.toString(get());
    }

    public int intValue() {
    
    
        return get();
    }
    public long longValue() {
    
    
        return (long)get();
    }
    public float floatValue() {
    
    
        return (float)get();
    }
    public double doubleValue() {
    
    
        return (double)get();
    }
}
getAndAddInt

先去内存拿最新的地址,然后不断进行CAS直到成功

public final int getAndAddInt(Object var1, long var2, int var4) {
    
    
    int var5;	
    do {
    
    //从主内存拿最新的值赋值给v5
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//compareAndSwapInt是原子性的操作:比较和替换是连着的,不可分割,不会分割成比较好后被别的线程插入更改

    return var5;
}

其他Atomic

AtomicReference

AtomicReference类提供了一个可以原子读写的对象引用变量。 原子意味着尝试更改相同AtomicReference的多个线程(例如,使用比较和交换操作)不会使AtomicReference最终达到不一致的状态。 AtomicReference甚至有一个先进的compareAndSet()方法,它可以将引用与预期值(引用)进行比较,如果它们相等,则在AtomicReference对象内设置一个新的引用。

是整体更改引用,而不是改变对象内部的值

public class UseAtomicReference {
    
    
    // 保证User的原子性
    static AtomicReference<User> userRef = new AtomicReference<User>();

    public static void main(String[] args) {
    
    
        User zhangsan = new User("zhangsan", 15);
        userRef.set(zhangsan);//设置
        User lisi = new User("lisi", 11);
        // 不是更新的zhangsan本身
        userRef.compareAndSet(zhangsan,lisi);//更改
        System.out.println(userRef.get().getName());//lisi
        System.out.println(userRef.get().getAge());
        System.out.println(zhangsan.name);//zhangsan
        System.out.println(zhangsan.age);

    }
}
class User{
    
    
    public User(String name, int age) {
    
    
        this.name = name;
        this.age = age;
    }

    String name;
    int age;

    public int getAge() {
    
    
        return age;
    }

    public String getName() {
    
    
        return name;
    }
}

atomic

处理CAS的ABA问题有两个可用的类

  • AtomicMarkableReference: 用boolean标记有没有人动过
  • AtomicStampedReference: 被动过几次

AtomicStampedReference

解决CAS的ABA问题

在现实中,还可能存在另外一种场景。就是我们是否能修改对象的值,不仅取决于当前值,还和对象的过程变化有关,这时,AtomicReference就无能为力了。

AtomicStampedReference它内部不仅维护了对象值,还维护了一个时间戳(我这里把它称为时间戳,实际上它可以使任何一个整数,它使用整数来表示状态值)。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。

package a;

// 处理CAS的ABA问题有两个可用的类
// AtomicMarkableReference  boolean有没有人动过
// AtomicStampedReference  被动过几次

import java.util.concurrent.atomic.AtomicStampedReference;

public class TestAtomicStampedReference {
    
    

  static AtomicStampedReference<String> asr = new AtomicStampedReference<>("zhangsan", 0); //0是版本号

  public static void main(String[] args) throws InterruptedException {
    
    

    String oldReference = asr.getReference(); // 初始的原值
    int oldStamp = asr.getStamp(); // 初始的版本号
    System.out.println("初始值" + oldReference + "======版本号" + oldStamp);

    Thread lisi =
        new Thread(
            new Runnable() {
    
    
              @Override
              public void run() {
    
    
                System.out.println(
                    Thread.currentThread().getName()
                        + "  lisi线程 cas执行前  以为当前变量值:"
                        + oldReference
                        + "   以为当前版本号"
                        + oldStamp
                        + "   尝试cas:"
                        + asr.compareAndSet(oldReference, "lisi", oldStamp, oldStamp + 1)
                        + "  cas后当前值为"
                        + asr.getReference()
                        + " 版本号为"
                        + asr.getStamp());
              }
            });

    Thread wangwu =
        new Thread(
            new Runnable() {
    
    
              @Override
              public void run() {
    
    
                String reference = asr.getReference();
                System.out.println(
                    Thread.currentThread().getName()
                        + "  wangwu线程 cas执行前 以为当前变量值:"
                        + oldReference
                        + "   以为当前版本号"
                        + oldStamp
                        + "  尝试cas:"
                        + asr.compareAndSet(oldReference, "wangwu", oldStamp, oldStamp + 1)
                        + "  cas后当前值为"
                        + asr.getReference()
                        + " 版本号为"
                        + asr.getStamp());
              }
            });
    // 让lisi先执行,且用join确保lisi执行完了wangwu再执行
    lisi.start();
    lisi.join();

    wangwu.start();
    wangwu.join();

    System.out.println("最后值和版本号" + asr.getReference() + "======" + asr.getStamp());
  }
}
/*
初始值zhangsan======版本号0
Thread-0  lisi线程 cas执行前  以为当前变量值:zhangsan   以为当前版本号0   尝试cas:true  cas后当前值为lisi 版本号为1
Thread-1  wangwu线程 cas执行前 以为当前变量值:zhangsan   以为当前版本号0  尝试cas:false  cas后当前值为lisi 版本号为1
最后值和版本号lisi======1
*/

AtomicIntegerArray

package a;

import java.util.concurrent.atomic.AtomicIntegerArray;

public class TestAtomicArray {
    
    
  static int[] arr = new int[] {
    
    1, 2};
  static AtomicIntegerArray ai = new AtomicIntegerArray(arr);

  public static void main(String[] args) {
    
    
    // 把索引为1的值改为3
    ai.getAndSet(0, 3);
    System.out.println(ai.get(0));//3
    System.out.println(arr[0]);//1
  }
}

AQS前情

在JAVA中,sun.misc.Unsafe 类提供了硬件级别的原子操作来实现这个CAS。 java.util.concurrent 包下的大量类都使用了这个 Unsafe.java 类的CAS操作。

CAS:

java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的(如 AtomicInteger.java,AtomicBoolean,AtomicLong)。下面以 AtomicInteger.java 的部分实现来大致讲解下这些原子类的实现。

一般来说在竞争不是特别激烈的时候,使用该包下的原子操作性能比使用 synchronized 关键字的方式高效的多(查看getAndSet(),可知如果资源竞争十分激烈的话,这个for循环可能换持续很久都不能成功跳出。不过这种情况可能需要考虑降低资源竞争才是)。

在较多的场景我们都可能会使用到这些原子类操作。一个典型应用就是计数了,在多线程的情况下需要考虑线程安全问题。通常第一映像可能就是:

public class Counter {
    
    //普通
    private int count;
    public Counter(){
    
    }
    public int getCount(){
    
    
        return count;
    }
    public void increase(){
    
    
        count++;
    }
}

上面这个类在多线程环境下会有线程安全问题,要解决这个问题最简单的方式可能就是通过加锁的方式,调整如下:

public class Counter {
    
    
    private int count;
    public Counter(){
    
    }
    // 加synchronized
    public synchronized int getCount(){
    
    
        return count;
    }
    public synchronized void increase(){
    
    
        count++;
    }
}

这类似于悲观锁的实现,我需要获取这个资源,那么我就给他加锁,别的线程都无法访问该资源,直到我操作完后释放对该资源的锁。我们知道,悲观锁的效率是不如乐观锁的,上面说了Atomic下的原子类的实现是类似乐观锁的,效率会比使用 synchronized关键字高,推荐使用这种方式,实现如下:

public class Counter {
    
    
    private AtomicInteger count = new AtomicInteger();
    public Counter(){
    
    }
    public int getCount(){
    
    
        return count.get();
    }
    public void increase(){
    
    
        count.getAndIncrement();
    }
}

但下面的自旋也许能给你提供另一种思路

自旋实现同步

volatile int status=0;//标识--是否有线程再同步块---是否有线程上锁成功
void lock(){
    
    
    while(!compareAndSet(0,1)){
    
    
        //本来希望的oldvalue是0,但是被其他线程修改为了1,所以当前线程就一个劲等,等到他被其他线程是否了后其他线程修改为0,当前线程就能拿到锁跳出循环了
    }
    //加锁成功
}

void unlock(){
    
    
    status=0;
}

boolean compareAndSet(int except,int newValue){
    
    
    //cas操作,修改status成功则返回true
}

缺点:耗费cpu资源,没有竞争到的线程会一直占用cpu资源进行cas操作

解决思路:让得不到锁的线程让出cpu

yield+自旋
volatile int status=0;

void lock(){
    
    
    while(!compareAndSet(0,1)){
    
    
        yield();//自己实现,拿不到锁就释放cpu,等其他线程唤醒
    }
    //拿锁成功
}

void unlock(){
    
    
    status=0;
}

要解决自旋锁的性能问题必须让竞争锁失败的线程不空转,而是在获取不到锁的时候把cpu资源让出来,yield方法能让出cpu资源,让线程竞争锁失败时,会调用yield方法让出cpu,自旋+yield的方式并没有完全解决问题,当系统只有两个线程竞争锁时,yield是由有效的,需要注意的是改方法只是当前让出cpu,有可能操作系统下次还是选择运行该线程,比如里面有2000个线程,想想会有什么问题?

sleep+自旋
volatile int status=0;
void lock(){
    
    
    while(!compareAndSet(0,1)){
    
    
        sleep(10);//时间设为固定,也很有问题
    }
    //拿锁成功
}

void unlock(){
    
    
    status=0;
}
park+自旋

这正是AQS的核心思路,park也是挂起线程,释放cpu,等其他线程唤醒

这个LockSupport.park()LockSupport.unpark(线程)是关键

volatile int status=0;
Queue parkQueue;//集合 数组 list
void lock(){
    
    
    while(!compareAndSet(0,1)){
    
    
        park();//挂起线程,释放cpu,等待其他线程唤醒
    }
    //拿锁成功
    ...;
    unlock();
}

void unlock(){
    
    
    status=0;
}

void park(){
    
    
    //将当前线程加入等待队列
    parkQueue.add(currentThread);
    //将当前线程是否cpu 阻塞
    releaseCpu();
}
void lock_notify(){
    
    
    //得到要唤醒的线程头部线程
    Thread t = parkQueue.header();
    //唤醒等待线程
    unpark(t);
}

park unpark

https://www.cnblogs.com/takumicx/p/9328459.html

park的意思是停车,等待别人告诉可以开车再启动发动机unpark,一般是利用LockSupport类完成的,而他里面的park和unpart是native的

我们可以使用他来阻塞和唤醒线程,功能和wait、notify有些相似,但是LockSupport比起wait、notify功能更强大。

LockSupport.park();//阻塞当前线程
LockSupport.unpark(Thread t);//唤醒指定线程
public class LockSupportTest {
    
    

    public static void main(String[] args) {
    
    
        Thread parkThread = new Thread(new ParkThread());
        parkThread.start();
        System.out.println("开始线程唤醒3");
        LockSupport.unpark(parkThread);
        System.out.println("结束线程唤醒4");

    }

    static class ParkThread implements Runnable{
    
    

        @Override
        public void run() {
    
    
            System.out.println("开始线程阻塞1");
            LockSupport.park();
            System.out.println("结束线程阻塞2");
        }
    }
}
//1324
public class LockSupportTest {
    
    

    public static void main(String[] args) {
    
    
        Thread parkThread = new Thread(new ParkThread());
        parkThread.start();
        System.out.println("开始线程唤醒3");
        LockSupport.unpark(parkThread);
        System.out.println("结束线程唤醒4");

    }

    static class ParkThread implements Runnable{
    
    

        @Override
        public void run() {
    
    
            try {
    
    
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("开始线程阻塞1");
            LockSupport.park();
            System.out.println("结束线程阻塞2");
        }
    }
}
// 3412
public class LockSupportTest {
    
    

    public static void main(String[] args) {
    
    
        Thread parkThread = new Thread(new ParkThread());
        parkThread.start();
        for(int i=0;i<2;i++){
    
    
            System.out.println("开始线程唤醒3");
            LockSupport.unpark(parkThread);
            System.out.println("结束线程唤醒4");
        }
    }

    static class ParkThread implements Runnable{
    
    

        @Override
        public void run() {
    
    
            try {
    
    
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            for(int i=0;i<2;i++){
    
    
                System.out.println("开始线程阻塞1");
                LockSupport.park();
                System.out.println("结束线程阻塞2");
            }
        }
    }
}
// 输出顺序
// 3434121
class Parker : public os::PlatformParker {
    
    
private:
  volatile int _counter ;
  ...
public:
  void park(bool isAbsolute, jlong time);
  void unpark();
  ...
}
class PlatformParker : public CHeapObj<mtInternal> {
    
    
  protected:
    pthread_mutex_t _mutex [1] ;
    pthread_cond_t  _cond  [1] ;
    ...
}
LockSupport

LockSupport就是通过控制变量_counter来对线程阻塞唤醒进行控制的。原理有点类似于信号量机制。

  • 当调用park()方法时,会将_counter置为0,同时判断前值,小于1说明前面被unpark过,则直接退出,否则将使该线程阻塞。
  • 当调用unpark()方法时,会将_counter置为1,同时判断前值,小于1会进行线程唤醒,否则直接退出。
    形象的理解,线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加一个凭证,但凭证最多只能有1个。
  • 为什么可以先唤醒线程后阻塞线程?
    因为unpark获得了一个凭证,之后调用park因为有凭证消费,故不会阻塞。
  • 为什么唤醒两次后阻塞两次会阻塞线程。
    因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证。

LockSupport是JDK中用来实现线程阻塞和唤醒的工具。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。
JDK并发包下的锁和其他同步工具的底层实现中大量使用了LockSupport进行线程的阻塞和唤醒,掌握它的用法和原理可以让我们更好的理解锁和其它同步工具的底层实现。

AQS

概念

所谓AQS,指的是AbstractQueuedSynchronizer,它提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。是除了java自带的synchronized关键字之外的锁机制。这个类在java.util.concurrent.locks包。

AQS是Java并发包提供的一个同步基础机制,是并发包中实现Lock和其他同步机制(如:Semaphore、CountDownLatch和FutureTask等)的基础。具体用法是通过继承AQS实现其模板方法,然后将子类作为同步组件的内部类。

  • 同步队列:AQS内部包含一个FIFO的同步等待队列,简单的说,没有成功获取控制权的线程会在这个队列中等待
  • state:AQS内部管理了一个原子的int域作为内部状态信息,并提供了一些方法来访问该域,基于AQS实现的同步机制可以按自己的需要来灵活使用这个int域,比如:
    • ReentrantLock:用它记录锁重入次数
    • CountDownLatch:用它表示内部的count;
    • FutureTask:用它表示任务运行状态(Running,Ran和Cancelled);
    • Semaphore:用它表示信号数量。
  • AQS内部提供了一个ConditionObject类来支持独占模式下的(锁)条件,这个条件的功能与Object的wait和notify/notifyAll的功能类似,但更加明确和易用。
  • AQS一般的使用方式为定义一个实现AQS接口的非公有的内部帮助类作为内部代理,来实现具体同步机制的方法,如Lock的lock和unlock;AQS中也提供一些检测和监控内部队列和条件对象的方法,具体同步机制可以按需使用这些方法;AQS内部只有一个状态,即原子int域,如果基于AQS实现的类需要做序列化/反序列化,注意这一点。

AQS的核心思想是,

  • 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,
  • 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
    • CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
    • AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
    • 用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变state状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
    • 头结点是当前获取到锁的那个线程

在这里插入图片描述

同步控制对比:

  • synchronized:基于JVM底层,基于C++,底层行为不可控
  • AbstractQueueSynchronizer:不利于任何jvm内置锁,基于java可变行为实现同步

AQS 定义了两种资源共享方式:
1.Exclusive:独占,只有一个线程能执行,如ReentrantLock
2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier

AQS–队列

AQS的实现依赖内部的同步队列,也就是FIFO的双向队列,这个队列是用双线链表实现的

  • 如果当前线程竞争锁失败,那么AQS会把当前线程以及等待状态信息构造成一个Node,放入到同步队列尾部,作为尾结点,同时再阻塞该线程。
  • 当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。只需重新设置新的队列首部(头节点)即可。

img

添加节点

当出现锁竞争以及释放锁的时候,AQS同步队列中的节点会发生变化,首先看一下添加节点的场景。

节点添加到同步队列

这里会涉及到两个变化

  • 新的线程封装成Node节点追加到同步队列中,设置prev节点以及修改当前节点的前置节点的next节点指向自己
  • 通过CAS将tail重新指向新的尾部节点

释放锁 移除节点

head节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点;如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下

移除节点的变化

这个过程也是涉及到两个变化

  • 修改head节点指向下一个获得锁的节点
  • 新的获得锁的节点,将prev的指针指向null

这里有一个小的变化,就是设置head节点不需要用CAS,原因是设置head节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要CAS保证,只需要把head节点设置为原首节点的后继节点,并且断开原head节点的next引用即可

看一下Node结点类

AQS–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;  
    /** 表示接下来的一个共享模式请求(acquireShared)要无条件的传递(往后继节点方向)下去 */  
    static final int PROPAGATE = -3;  
    /** 
     * 等待状态域, 取以下值: 
     *   SIGNAL:     当前节点的后继节点已经(或即将)被阻塞(在等待),所以如果当前节点释放(控制权)  
     *               或者被取消时,必须唤醒其后继节点运行后继。为了避免竞争,请求方法必须首先  
     *               声明它们需要一个信号,然后(原子的)调用请求方法,如果失败,当前线程 
     *               进入阻塞状态。 
     *   CANCELLED:  表示当前节点已经被取消(由于等待超时或被中断),从队列中移走。节点一旦进入被取消状态,就 
     *               不会再变成其他状态了。具体来说,一个被取消节点的线程永远不会再次被 
     *               阻塞 
     *   CONDITION:  表示当前节点正处在一个条件队列中。当前节点直到转移时才会被作为一个 
     *               同步队列的节点使用。转移时状态域会被设置为0。(使用0值和其他定义值  
     *               并没有关系,只是为了简化操作) 
     *   PROPAGATE:  表示一个共享的释放操作(releaseShared)应该被传递到其他节点。该状态 
     *               值在doReleaseShared过程中进行设置(仅在头节点),从而保证持续传递, 
     *               即使其他操作已经开始。 共享,表示状态要往后面传播
     *   0:          None of the above  初始状态
     * 
     * 这些状态值之所以用数值来表示,目的是为了方便使用,非负的值意味着节点不需要信号(被唤醒)。 
     * 所以,一些代码中不需要针对特殊值去做检测,只需要检查符号(正负)即可。 
     *  
     * 针对普通的同步节点,这个域被初始化为0;针对条件(condition)节点,初始化为CONDITION(-2) 
     * 需要通过CAS操作来修改这个域(如果可能的话,可以使用volatile写操作)。 
     */  
    volatile int waitStatus;  
    /** 
     * 指向当前节点的前驱节点,用于检测等待状态。这个域在入队时赋值,出队时置空。 
     * 而且,在取消前驱节点的过程中,可以缩短寻找非取消状态节点的过程。由于头节点  
     * 永远不会取消(一个节点只有请求成功才会变成头节点,一个被取消的节点永远不可  
     * 能请求成功,而且一个线程只能取消自己所在的节点),所以总是存在一个非取消状态节点。 
     */  
    volatile Node prev;  
    /** 
     * 指向当前节点的后继节点,释放(控制权)时会唤醒该节点。这个域在入队时赋值,在跳过 
     * 取消状态节点时进行调整,在出队时置空。入队操作在完成之前并不会对一个前驱节点的 
     * next域赋值,所以一个节点的next域为null并不能说明这个节点在队列尾部。然而,如果 
     * next域为null,我们可以从尾节点通过前驱节点往前扫描来做双重检测。取消状态节点的 
     * next域指向自身,这样可以简化isOnSyncQueue的实现。 
     */  
    volatile Node next;  
    /** 
     * 使当前节点入队的线程。在构造构造的时候初始化,使用后置为null。 
     */  
    volatile Thread thread;  
    /** 
     * 指向下一个条件等待状态节点或者为特殊值(SHARED)。由于条件队列只有在独占模式下才 
     * 能访问,所以我们只需要一个普通的链表队列来保存处于等待状态的节点。它们在重新请 
     * 求的时候会转移到同步队列。由于条件只存在于独占模式下,所以如果是共享模式,就将 
     * 这域保存为一个特殊值(SHARED)。 
     */  
    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() throws NullPointerException {
    
      
        Node p = prev;  
        if (p == null)  
            throw new NullPointerException();  
        else  
            return p;  
    }  
    Node() {
    
        // Used to establish initial head or SHARED marker  
    }  
    Node(Thread thread, Node mode) {
    
         // Used by addWaiter  
        this.nextWaiter = mode;  
        this.thread = thread;  
    }  
    Node(Thread thread, int waitStatus) {
    
     // Used by Condition  
        this.waitStatus = waitStatus;  
        this.thread = thread;  
    }  
}  

说明:节点类Node内部定义了一些常量,如节点模式、等待状态;Node内部有指向其前驱和后继节点的引用(类似双向链表);Node内部有保存当前线程的引用;Node内部的nextWaiter域在共享模式下指向一个常量SHARED,在独占模式下为null或者是一个普通的等待条件队列(只有独占模式下才存在等待条件)。

再看一下AQS中同步等待队列相关的域:

/** 
 * 同步等待队列的头节点,延迟初始化。除了初始化之外,只能通过setHead方法来改变 
 * 这个域。注:如果头结点存在,那么它的waitStatus可以保证一定不是CANCELLED。 
 */  
private transient volatile Node head;  
/** 
 * 同步等待队列的尾节点,延迟初始化。只有通过enq方法添加一个新的等待节点的时候 
 * 才会改变这个域。 
 */  
private transient volatile Node tail;  

AQS用到的设计模式

AQS底层使用了模板方法模式

同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):

  1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
  2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
    这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。

自定义同步器在实现的时候只需要实现共享资源state的获取和释放方式即可,至于具体线程等待队列的维护,AQS已经在顶层实现好了。自定义同步器实现的时候主要实现下面几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):。尝试获取资源(独占方式),成功则返回true,失败则返回false。
  • tryRelease(int):尝试释放资源(独占方式),成功则返回true,失败则返回false。
  • tryAcquireShared(int):尝试获取资源(共享方式)。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):尝试释放资源(共享方式),如果释放后允许唤醒后续等待结点返回true,否则返回false。

ReentrantLock为例(可重入独占式锁):state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire()独占锁并将state+1.之后其他线程再想tryAcquire的时候就会失败,直到A线程unlock()到state=0为止,其他线程才有机会获取该锁。A释放锁之前,自己也是可以重复获取此锁(state累加),这就是可重入的概念。

注意:获取多少次锁就要释放多少次锁,保证state是能回到零态的。

以CountDownLatch为例,任务分N个子线程去执行,state就初始化 为N,N个线程并行执行,每个线程执行完之后countDown()一次,state就会CAS减一。当N子线程全部执行完毕,state=0,会unpark()主调用线程,主调用线程就会从await()函数返回,继续之后的动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

在acquire()、acquireShared()两种方式下,线程在等待队列中都是忽略中断的,acquireInterruptibly()/acquireSharedInterruptibly()是支持响应中断的。

AQS–state

AQS中有一个这样的属性定义,这个对于重入锁的实现来说,表示一个同步状态。它有两个含义的表示

  • 当state=0时,表示无锁状态
  • 当state>0时,表示已经有线程获得了锁,也就是state=1,但是因为ReentrantLock允许重入,所以同一个线程多次获得同步锁的时候,state会递增,比如重入5次,那么state=5。 而在释放锁的时候,同样需要释放5次直到state=0其他线程才有资格获得锁
private volatile int state;

需要注意的是:不同的AQS实现,state所表达的含义是不一样的。

private volatile int state;  

// 没有用cas的get和set是在release时用的,因为release的线程肯定有锁,无需用cas
protected final int getState() {
    
      
    return state;  
}  
protected final void setState(int newState) {
    
    
    state = newState;  
}

protected final boolean compareAndSetState(int expect, int update) {
    
      
    // See below for intrinsics setup to support this  
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);  
}

上面已经看到AQS内部的整体数据结构,一个同步等待队列+一个(原子的)int域。下面来从请求和释放两条主线来进行相关代码分析。

分析ReentrantLock源码

相比 synchronized,ReentrantLock 增加了一些高级功能。主要来说主要有三点:① 等待可中断;lock.lockInterruptibly() ② 可实现公平锁;③ 可实现选择性通知(锁可以绑定多个条件)

先分析独占模式,下面是我们常用的lock()方法

// ReentrantLock.lock()
public void lock(){
    
    
    sync.lock();
}

ReentrantLock中lock方法的时序图

sync

sync是一个静态内部类,它继承了AQS这个抽象类(同时Sync还是抽象的),前面说过AQS是一个同步工具,主要用来实现同步控制。我们在利用这个工具的时候,会继承它来实现同步控制功能。(设计模式的模板方法设计模式)

通过进一步分析,发现Sync这个类有两个具体的实现,分别是 NofairSync(非公平锁), FailSync(公平锁)。下面是类之间的关系

public class ReentrantLock implements Lock, java.io.Serializable 
//下面3个都是ReentrantLock的内部类
    
// Sync继承了AQS
abstract static class Sync extends AbstractQueuedSynchronizer 
//下面两个继承了Sync,简介继承了AQS
static final class NonfairSync extends Sync
static final class FairSync extends Sync

重新看lock方法

// ReentrantLock.lock()
public void lock(){
    
    
    sync.lock();// sync为构造ReentrantLock实例时根据传入的bool后new出来NonfairSync实例或FairSync实例
}

调用流程:lock—>acquire->tryAcquire

①NonfairSync

非公平同步器:先CAS争抢,争抢不到再排队

实现lock和tryAcquire

  • 非公平锁lock时直接cas获取锁一下,获取到就把当前线程设置为持有锁的线程,即把AQS的exclusiveOwnerThread设置为持有锁的线程
  • 一次尝试没获取锁成功就去acquire(1)竞争锁
  • 流程:NonfairSync.lock()–>AQS.acquire()–>NonfairSync.tryAcquire()–>NonfairSync.nonfairTryAcquire()
// 内部类NonfairSync
static final class NonfairSync extends Sync {
    
    
    private static final long serialVersionUID = 7316153563782823691L;

    // 非公平锁不排队,直接cas一下
    final void lock() {
    
    
        // 通过cas修改state状态,表示争抢锁的操作。只要cas加锁成功,那么就执行,不去排队,抢了别人的所以叫非公平锁
        if (compareAndSetState(0, 1))
            // 争抢到了state,设置排他锁
            setExclusiveOwnerThread(Thread.currentThread());
        else
            // 抢占锁失败,即state不为0。调用acquire来走锁竞争逻辑  // 这个1指的是如果可以重入的话就+1
            acquire(1);
    }

    /*说明:
    tryAcquire在AQS为抛异常的方法,所以子类要用必须重写
    tryAcquire的意义就是独占模式中尝试获取锁
    在独占模式下尝试请求(控制权)。这个方法(实现)应该查看一下对象的状态是否允许在独占模式下请求,如果允许再进行请求。 
 * 
 * 这个方法总是被请求线程执行,如果方法执行失败,会将当前线程放到同步等待队列中(如果当前线程还不在同步等待队列中),
 直到被其他线程的释放操作唤醒。可以用来实现Lock的tryLock方法。 
    */
    // 独占模式中尝试获取锁,如果成功就返回true,失败返回false // 它是重写AQS类中的tryAcquire方法,并且大家仔细看一下AQS中tryAcquire方法的定义,并没有实现,而是抛出异常。按照一般的思维模式,既然是一个不实现的模版方法,那应该定义成abstract,让子类来实现呀?大家想想为什么
    protected final boolean tryAcquire(int acquires) {
    
    
        // 非公平锁的尝试获取锁 // 父类Sync内的方法
        return nonfairTryAcquire(acquires);
    }
}

//-----------顺便对比一下公平锁和非公平锁-----------

在Sync的nonfairTryAcquire()里还有一次cas尝试获取锁的操作,所以非公平锁有2次直接cas获取锁的机会。2次都获取不到才返回false。

而公平锁上来就判断state是否为0,是0还不够,还得判断一下有没有队列,没有队列才敢cas。、

//Sync内部类方法
final boolean nonfairTryAcquire(int acquires) {
    
    //非公平锁NonfairSync的tryAcquire会调用 //公平锁FairSync的tryAcquire没调用,只是返回bool
    // 获取当前线程
    final Thread current = Thread.currentThread();
    int c = getState();//获取state的值
    if (c == 0) {
    
    //如果state为0代表无锁状态 
        if (compareAndSetState(0, acquires)) {
    
     //非公平锁第二次cas ,第一次cas在lock()刚开始就进行了
            // 用cas拿锁成功,设置为独占线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 状态值不为0,但是拿锁的线程是当前线程,那么就重入
    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;
}

②FairSync

实现lock和tryAcquire

  • 公平锁直接去排队竞争锁,不能直接cas
  • state为0才有权利去cas进行改变state
  • 持有锁的线程和当前线程一样也有权利设置state
  • acquire会先调用NonFair/Fair的tryAcqiure(),然后才执行入队操作。
static final class FairSync extends Sync {
    
    
    private static final long serialVersionUID = -3000897897090466540L;

    // 公平锁直接去排队竞争锁,不能直接cas
    final void lock() {
    
    
        acquire(1);
    }

    /**
- 第一步判断锁是不是自由状态,如果是则判断直接是否需要排队(
---hasQueuedPredecessors方法判断队列是否被初始化(如果没有初始化显然不需要排队)和是否需要排队(队列如果被初始化了,则自己有可能需要排队));
---如果hasQueuedPredecessors返回false,由于取反了故而不需要排队则进行CAS操作去上锁,如果需要排队则不会进入if分支当中,也不会进else if,会直接返回false表示加锁失败。
- 第二步如果不是自由状态则判断是不是重入,判断持有锁的线程是不是当前线程,如果不是则直接返回false加锁失败,如果是重入则把计数器+1。也说明了reentrantLock是可重入锁。
         */
    protected final boolean tryAcquire(int acquires) {
    
    
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
    
     // 锁空闲也不能直接占用,后面可能有排队,先去排队
            // 不是直接加锁,而是先入队,这才是公平锁
            if (!hasQueuedPredecessors() && // 加!后的含义:判断是否下一个轮到自己了,要轮到自己了就去cas一下,免得还得进行入队操作
                compareAndSetState(0, acquires)) {
    
     
                // 当前线程拿到锁了
                //设置当前线程为拥有锁的线程,方面后面判断是不是重入(只需把这个线程拿出来判断是否当前线程即可判断重入) 
                setExclusiveOwnerThread(current);
                return true;
            }
        }else if (current == getExclusiveOwnerThread()) {
    
    //当前线程是否跟持有锁的线程是同一线程
            //如果C!=0,而且当前线程不等于拥有锁的线程则不会进else if 直接返回false,加锁失败
            //如果C!=0,但是当前线程等于拥有锁的线程则表示这是一次重入,那么直接把状态+1表示重入次数+1
            //那么这里也侧面说明了reentrantlock是可以重入的,因为如果是重入也返回true,也能lock成功
            // 思考问题:当前线程释放了锁后怎么办
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}
hasQueuedPredecessors:

首先说明:这个函数是因为我们原来判断state是0才进来的,也就是说,刚才判断过没有线程运行,那我们应该可以cas。state不为0的话,根本不执行这个函数,而是去判断一下是否是重入线程。

  • 主要是用来判断线程需不需要排队,因为队列是FIFO的,所以需要判断队列中有没有相关线程的节点已经在排队了。有则返回true表示线程需要排队,没有则返回false则表示线程无需排队。
  • 判断队列有没有很多前驱结点,如果队列中第二个结点为null或者第二个结点的线程是自己,就去cas(0,1)试一下
  • 返回true:
    • 条件1:h!=t:头尾没有指向同一个
      • 结点超过2个,都不为null
      • 结点刚刚由2个变为1个,第二个为null(有没有这种情况,待验证,跟h/t这两个更新顺序有关)
    • 条件2:((s = h.next) == null || s.thread != Thread.currentThread()) 只要有一个条件为true即可
      • (s = h.next) == null :没有第二个结点(我觉得这也该去cas一下,没绕明白这里)
      • s.thread != Thread.currentThread()):第二个结点的线程不是我
      • 反正不是第二个结点
    • 当前线程 : 既然我是null 或者 我不是老二 那我也没啥必要cas获取锁 说明我还排在后面,去排队
  • 返回false:
    • 情况1:h==t:也就表示队列只有一个元素 或为空
      • 都为null:没有在等待的(根本没有线程执行) 或者 队列都没有初始化(有线程执行,但是之前没发生过冲突,没有队列),去cas吧
      • 指向同一个:当前只有一个在运行,去cas碰碰运气,万一他已经执行完了呢
    • 情况2:h!=t 为 true, ((s = h.next) == null || s.thread != Thread.currentThread())都为false
      • h!=t 代表了队列必为两个或以上元素 并且 前两个不相同
      • (s= h.next )== null 为false 表示 第二个元素不为空
      • s.thread != Thread.currentThread() 表示 第二个元素已经是当前线程
      • 这种情况就是有2个结点,但是第二个结点是我
    • 总结:①h/t都为null、②第二个结点的线程是我
      • ①没有初始化队列的时候,h、t都为null,那么没有线程在执行,或者只有一个线程在执行,但没有创建队列(当然也就没用ht了)
      • ②有队列,但是第二个结点就是我(除去正在运行的,下个就该我了)
      • 以上两种情况有权cas一下,免得入队耗时

要想去cas得满足:

//AQS.java
// tryAcquire里 锁空闲  判断自己是否需要排队 //有没有很多前驱
public final boolean hasQueuedPredecessors() {
    
    
    // 如果没有队列的时候,h和t都是null,返回false,说明他不需要排队,用cas加锁
    // 如果队列被初始化的时候,如果队列中元素>1,队列中元素==1,
    // >1的时候,第一个h != t满足,头结点的后继结点不为空,当前线程不是头结点的后继结点 ,不满足
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    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()); 
    // h!=t且没有第二个结点了,就返回true
    // h!=t且有第二个结点,但是第二个结点的线程和当前线程一样,返回true
}

执行完上面获取锁成功就无需进行队列操作了(当然得遵循公平/非公平的原则),获取锁失败就去入队。

此时的入队是真的要入队,非公平锁也不是cas了,而是先创建Node结点再操作cas的事

// 上面并没有重写acquire,但是重写了lock和tryAcquire,所以会调用到重写的
// 公平锁和非公平锁的lock()都是调用acquire(1)//公平锁是直接调用,非公平锁是cas拿不到才调用
public final void acquire(int arg) {
    
    
    // 公平锁与非公平锁都重载了自己的tryAcquire()
    if (!tryAcquire(arg) && //尝试获取独占锁,获取成功tryAcquire返回true,获取失败返回false后继续调用acquireQueued
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 没获取到锁,那么就添加到队列 //addWaiter:获取锁失败,把当前线程封装为Node,添加到AQS队列。将Node作为参数,通过自旋去尝试获取锁。// Node.EXCLUSIVE为null
        // 将当前线程封装成Node添加到AQS队列尾部
        selfInterrupt();
}
对比tryAcquire

调用的是FairSync、NonFairSync子类的tryAcquire()方法,我们可以通过观察下面得知:

  • 都是进行了2个判断,①如果state==0,去cas一下②如果≠0,去看看是否是可重入的情况。
  • 如果都是上面2个判断,那就没有公平性可言了,所以公平锁的在调用①前,要看看队列是否只有一个在运行,或者第二个该自己运行了,才有机会cas,这样就保证公平性了,有其他人在排队就不能cas
  • tryAcquire()不成功就走排队的逻辑
// NonfairSync的nonfairTryAcquire(),间接调用的是Sync  
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;
}
//FairSync
protected final boolean tryAcquire(int acquires) {
    
    
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    
    
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
    
    
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
    
    
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

Sync

  • nonfairTryAcquire尝试拿锁,如果锁空闲或者完成锁重入,那么就代表拿锁成功。否则返回false拿锁失败
  • tryRelease:尝试释放,state状态值-1,减后如果state为0就释放
abstract static class Sync extends AbstractQueuedSynchronizer {
    
    
    private static final long serialVersionUID = -5179523762034025860L;

    // 抽象方法
    abstract void lock();

    protected final boolean tryRelease(int releases) {
    
     
        // state-1
        int c = getState() - releases;
        // 当前线程并不是拿锁的线程,抛出错误
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        // 标记锁是否空闲
        boolean free = false;
        // state为0了,锁空闲了
        if (c == 0) {
    
    
            // 锁空闲,是否成功
            free = true;
            // 排他线程设置为null
            setExclusiveOwnerThread(null);
        }
        // 更新state值,这里无需加锁
        setState(c);
        return free;
    }

    protected final boolean isHeldExclusively() {
    
    
        // While we must in general read state before owner,
        // we don't need to do so to check if current thread is owner
        // 当前线程和拥有锁的线程是否为同一线程,即不是的话就排他
        return getExclusiveOwnerThread() == Thread.currentThread();
    }

    final ConditionObject newCondition() {
    
    
        return new ConditionObject();
    }
}

AbstractQueuedSynchronizer.java

  • acquire获取锁
  • tryAcquire尝试获取锁,只是尝试,获取不到就回来。重入锁也算获取成功
  • addWaiter:没有获取锁成功,把当前线程放到队尾。
    • 如果addWaiter第一次尝试没有后没有把当前线程的node放到队尾成功,则调用enq方法去while{cas}
    • enq:给addWaiter服务用的,把操作队尾的cas循环和队列初始化放到这里了,addWaiter调用它enq
  • acquireQueued

逻辑关系:

acquireQueued(addWaiter(null), arg))、

addWaiter{enq(node)}

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    
    
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long stateOffset;
    private static final long headOffset;
    private static final long tailOffset;
    private static final long waitStatusOffset;
    private static final long nextOffset;
    static {
    
    
        try {
    
    
            stateOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
            headOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
            tailOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
            waitStatusOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("waitStatus"));
            nextOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("next"));

        } catch (Exception ex) {
    
     throw new Error(ex); }
    }
    // 上面的逻辑我们似曾相识,在AtomicInteger中见过

    private transient volatile Node head;
    private transient volatile Node tail;
    // 在这个类中持有着,compareAndSetState()也是AQS中的方法
    private volatile int state;
   

上面要注意的是state变量是volatile的,且是由AQS持有的,不是子类持有。

lock.lock()调用时就会调用acquire()方法【AQS的方法】,而acquire()会调用子类重写过的tryAcquire(),如果尝试获取锁没有获取成功,就入队。入队后调用selfInterrupt()进行阻塞。

// 上面并没有重写acquire,但是重写了lock和tryAcquire,所以会调用到重写的
// 公平锁和非公平锁的lock()都是调用acquire(1)//公平锁是直接调用,非公平锁是cas拿不到才调用
public final void acquire(int arg) {
    
    
    // 公平锁与非公平锁都重载了自己的tryAcquire()
    if (!tryAcquire(arg) && //尝试获取独占锁,获取成功tryAcquire返回true,获取失败返回false后继续调用acquireQueued
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 没获取到锁,那么就添加到队列 //addWaiter:获取锁失败,把当前线程封装为Node,添加到AQS队列。将Node作为参数,通过自旋去尝试获取锁。// Node.EXCLUSIVE为null
        // 将当前线程封装成Node添加到AQS队列尾部
        selfInterrupt();
}
获锁失败,入队

下面看看入队的操作。

  • 把当前线程包装为Node结点,准备入队
  • 直接cas方式设置到队尾,设置成功直接返回后阻塞。设置不成功就调用enq()继续while入队操作
  • 返回新队尾结点
//addWaiter:获取锁失败,把当前线程封装为Node,添加到AQS队列。将Node作为参数,通过自旋去尝试获取锁。
private Node addWaiter(Node mode) {
    
     // 排队过来的mode为null
    // 把当前线程封装为Node
    Node node = new Node(Thread.currentThread(), mode);//mode为Node.EXCLUSIVE为null
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;

    // 其实下面的操作都是入队的操作,没有其他的操作。入队后返回入队元素node到acquireQueued
    // 队尾不为空,说明队列中有结点在排队了,所以就当前线程去末尾排队
    if (pred != null) {
    
    
        // 当前结点的前向指针为上一个尾结点
        node.prev = pred;
        // cas替换尾结点
        if (compareAndSetTail(pred, node)) {
    
     // 期望替换的是pred这个尾结点,但如果他被其他线程替换了之后,当前线程就替换不成功了
            // 上一个尾结点的后继结点为当前结点
            pred.next = node;
            return node;
        }
    }
    // 执行到这说明没在队尾放置成功线程,在while{CAS}吧
    // 尾结点为空,或者原来队列有元素但在替换尾结点的过程中出错,被其他线程把尾结点替换了 // 第一个线程进来的时候一定走这,初始化队列,直接把当前结点设置为头结点,表示正在运行的线程 
    enq(node); // 执行完这里都会把线程入队尾
    // 返回当前线程
    return node;
}

enq():

  • 要放到队尾,必须得先有队头才能入队,没有对头将创建个空队头head(而不是把我们的线程当作对头),同步这个空对头先设置为队尾tail
  • 当然设置的过程中得保证同步,用的是while(cas)操作,cas不成功就下一次循环
  • 设置成功就return前驱结点
// 前面说了两种情况会来这:队列为空 或 队列有值但是cas替换尾结点时替换失败返回false(被其他线程cas抢了)
private Node enq(final Node node) {
    
     //enqueue入队,传进来的node是要放到队尾的当前线程node
    for (;;) {
    
    
        // 获取尾结点(最新的)
        Node t = tail;
        // 如果尾结点为空,代表队列空,即代表队列没人使用过还没初始化。//同时设置头结点和尾结点,头结点随便设置,尾结点设为当前结点
        if (t == null) {
    
     // 进入这只用作初始化队列使用,全程只会进入一次,后面tail总是有值,head==tail即代表对列为空
            // 设置头结点的过程中如果返回false代表被其他线程创建头结点了
            if (compareAndSetHead(new Node())) //为什么随便new了个Node?因为走到这说明已经有个线程在执行了获取了锁,我们创建个队列把当前线程放入队列。而正常来说我们要把头结点当做正在执行的线程,但没有办法了,我们第一个获取到锁的线程并没有给我们留下信息,那我们干脆随便new个头结点让他代表正在执行的线程就行了,我们方面把当前线程添加到他后面就行 //CAS头结点的时候要求原值位null才能cas成功
                tail = head;// 把没有信息的头结点为设置为尾结点,而我们当前线程的结点continue下个再设置
            // 这里仅仅设置了head,而tail虽然设置了,但不是我们当前线程的node,所以continue下次循环,把当前线程设置为tail
            // 一定要搞懂真正的当前线程的tail没在这次设置,否则你会疑惑为什么这里tail不用cas设置?因为这个tail根本就没有用,被其他线程设置了也无所谓

            // 还是要搞懂一下为什么这里不需要加锁,我们假设要插入的结点为node
            // ①如果tail为空,cas头结点成功,那么tail=head=new Node,但是node还没插入,下次循环把node插入,间情况③④
            // ②如果tail为空,cas头结点失败,头结点被其他线程抢了(非公平锁时)。下次循环tail和head都不为空了,进入③④情况
            // ③tail不为空,顺利拿到尾结点,然后更新尾结点成功即可
            // ④tail不为空,拿到了尾结点后,又被其他线程抢先注册新的尾结点了,进入下一次if重新CAS node结点
            // 总结:有for了这里不用把代码块加锁,不成功再cas一次即可。
            // 头结点只有刚开始的时候为空,一旦有元素入队后就不会为空了(释放的情况再说)
        } else {
    
    
            // 队列被初始化过了// 到这的原因是原来要添加尾结点的,但是cas时原来尾结点被其他线程更改了,cas没成功,这里重新cas
            // 获取原来尾结点,然后当前线程重新替换该尾结点 // 其实就相当于addWaiter里循环cas
            node.prev = t;
            if (compareAndSetTail(t, node)) {
    
    //如果又失败了怎么办?反正外面有for,失败了重新来就行,直到放到队尾
                t.next = node;
                return t;//返回前驱结点
            }
        }
    }
    // 队头里的thread永远为空
}

前面addWaiter和end入队成功后,把刚加入的tail作为参数传给acquireQueued()

  • 入参是新队尾结点,刚才入队成功后,在acquireQueued()中循环2次
  • 获取当前线程结点node的前驱结点p(不要用尾tail拿,可能已经被改变了)
  • 如果当前运行的结点是前驱结点p,就尝试获取锁。获取锁成功就把当前结点设置为head
  • 获取锁失败就看看是要进行下一次循环还是阻塞线程
// 尝试获取独占锁失败,当前线程添加到尾结点后,循环询问下一个是不是该我了, // 询问队列
final boolean acquireQueued(final Node node, int arg) {
    
    //参数node为尾结点(获取刚刚还是尾结点) //arg一般为1
    boolean failed = true;
    try {
    
    
        boolean interrupted = false;
        for (;;) {
    
    
            // 获取尾结点的前驱结点(也可能不是尾结点了,但刚刚还是尾结点) // 下次循环可能还进来,但前驱结点相对头结点的位置是变化的,一旦前驱结点的称为头结点,当前结点就应该去准备获取锁了
            final Node p = node.predecessor();
            // 如果尾结点为队里中除头结点外唯一结点(只有前驱为head才有资格进行锁的争夺) // 如果获取锁失败又重新插入到尾结点
            // 如果下一个该我了再看前一个执行完没有
            if (p == head && tryAcquire(arg)) {
    
     // 当前线程的上个结点为头结点,且获取到了锁才进入if
                // 获取了锁,将当前结点设置为头结点,表示我开始运行了。头结点是获得了锁的结点
                setHead(node);
                // 原来头结点的后继结点为null,帮助GC
                p.next = null; // help GC
                // 没有失败,获取锁成功
                failed = false;
                // 传递中断状态,并返回
                return interrupted;//正常情况下死循环唯一的出口
            }
            // 前驱结点还没有执行/执行完,不用急,还轮不到我,那我可以先挂起让出cpu
            // 现在是获取失败了,那应不应该挂起呢 // 根据结点的waitStatus决定是否需要挂起线程
            if (shouldParkAfterFailedAcquire(p, node) && // 传入了前驱结点和当前结点
                parkAndCheckInterrupt()) // 如果应该没阻塞,那么就阻塞,并检测终端状态
                // 若前面为true,则执行挂起,待下次唤醒的时候检测中断的标志
                interrupted = true;
        }
    } finally {
    
     // 唤醒后才执行
        if (failed)// 如果抛出异常则取消锁的获取,进行出队(sync queue)操作
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire:

还在循环cas,检查是否该阻塞本线程,先暂停循环

  • 通过前驱结点的waitStatus来判断本线程怎么办
  • waitStatus初始为0
  • 看到为0后就cas设置为-1,退出本次shouldParkAfterFailedAcquire(),下次再进来为-1就直接阻塞了
  • 第二次进来shouldParkAfterFailedAcquire,waitStatus为Node.SIGNAL==-1直接阻塞当前线程。也就是放到队尾尝试获取锁两次后阻塞
  • 在阻塞的过程中,会把取消的结点去掉
  • 什么时候被唤醒,后面说
// 获取锁失败,查看当前线程是否要挂起(阻塞)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    
     // 参数为前驱结点和当前结点
    // 获取前驱结点的等待状态waitStatus  // 前面并没有改过,所以是0
    int ws = pred.waitStatus;
    // 如果前驱结点为SIGNAL代表当前结点已经声明了需要唤醒,那么就可以阻塞当前结点了,返回true
    //  一个节点在其被阻塞之前需要线程"声明"一下其需要唤醒(就是将其前驱节点的等待状态设置为SIGNAL,注意其前驱节点不能是取消状态,如果是,要跳过) 
    // 如果前驱结点的waitStatus为-1 SIGNAL,说明前驱结点具有唤醒后继结点的功能,直接返回后接着进行挂起操作就可以,以后前驱结点执行的时候会判断到他需要唤醒别的结点,就把当前结点唤醒了
    if (ws == Node.SIGNAL)//-1 // 当前线程需要被unpark唤醒 // 为什么不直接=0时候就park?因为为了让他多自旋一次。此外0时候会做一些事情
        return true;

    // 前驱结点 CANCELLED==1,说明前驱结点的线程被取消了,也就不会唤醒其他结点了,我们如果把他放到我们当前结点的前面,那么当前结点就没人唤醒了,所以我们要把前面被取消的结点过滤掉,连接到个有效的前驱结点,让当前结点能被前面的结点唤醒。
    //从前驱节点开始逐步循环找到一个没有被“CANCELLED”节点设置为当前节点的前节点,返回false。在下次循环执行shouldParkAfterFailedAcquire时,返回true。这个操作实际是把队列中CANCELLED的节点剔除掉。 // 直接把他们丢弃了 他们进入不了线程了
    if (ws > 0) {
    
     // 设置 “当前节点”的 “当前前继节点” 为 “‘原前继节点'的前继节点”。
        do {
    
    
            node.prev = pred = pred.prev;//把取消的结点扔掉
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
    
     // CONDITION-2/PROPAGATE-3/0 如果前继节点为“0”或者“共享锁”状态,则设置前继节点为SIGNAL状态。
        /*
            前面的源码中,都没有看到有设置waitStatus的,所有每个新的node入队时,waitStatus都为0(成员属性都默认值0)
            正常情况下,前驱结点就是之前的tail,那么它的waitStatus应该是0
            用CAS将前驱结点的waitStatus设置为NODE.SIGNAL,然后返回false,然后acquireQueue就进入下次循环,再执行到shouldParkAfterFailedAcquire的时候,就进入前面的if返回true
            这样当前结点就能被他唤醒了
            接下来方法会返回false,还会继续尝试一下请求,以确保在阻塞之前确实无法请求成功。 
            改的是上一个结点的
            上一个结点本来是0,然后改成1
             */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//前驱结点改为-1
        // 最后去执行阻塞的时候,当前线程的ws还是0,上上一个结点是-1了
    }
    // 方法返回false,再走一次之前函数里的for循环
    // 然后再次来到此方法,然后进入第一个if返回true
    // 为什么不自己改成-1而是让别人来改呢?他阻塞了不能改为-1了,我自己看不到自己睡觉
    return false;

    /*
       接下来说说如果 shouldParkAfterFailedAcquire(p, node) 返回false的情况:仔细看shouldParkAfterFailedAcquire(p, node),我们可以发现,其实第一次进来的时候,一般都不会返回true的,原因很简单,
       前驱节点的 waitStatus=-1 是依赖于后继节点设置的。
       也就是说,我都还没给前驱设置-1呢,怎么可能是true呢,但是要看到,这个方法是套在循环里的,所以第二次进来的时候状态就是-1了。
       */
}

parkAndCheckInterrupt:

阻塞线程

// 当前线程应该被挂起阻塞,去执行阻塞
private final boolean parkAndCheckInterrupt() {
    
    //如果shouldParkAfterFailedAcquire返回了true,则会执行: parkAndCheckInterrupt()方法,它是通过LockSupport.park(this)将当前线程挂起到WATING状态,它需要等待一个中断、unpark方法来唤醒它,通过这样一种FIFO的机制的等待,来实现了Lock的操作。
    // 真正地负责阻塞当前线程 // 在这阻塞了
    LockSupport.park(this);//LockSupport类是Java6引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数:park()和unpark()
    //线程被唤醒,方法返回当前线程的中断状态,并重置当前线程的中断状态(置为false)。  
    return Thread.interrupted();
}

LockSupport类是Java6引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数:

  1. public native void unpark(Thread jthread);
  2. public native void park(boolean isAbsolute, long time);

unpark函数为线程提供“许可(permit)”,线程调用park函数则等待“许可”。这个有点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。 permit相当于0/1的开关,默认是0,调用一次unpark就加1变成了1.调用一次park会消费permit,又会变成0。 如果再调用一次park会阻塞,因为permit已经是0了。直到permit变成1.这时调用unpark会把permit设置为1.每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark不会累积

img

释放ReentrantLock.unlock

解锁我们把公平和非公平锁写一起即可,因为只有非公平锁才会在上个线程解锁后不排队直接来抢锁。如果他没抢到锁就去排队了,抢到的话本来该执行的那个节点重新连接到头结点。

  • ①调用release()的对象不是持有锁的线程:报错
  • ②state还不为0,只是减少可重入次数
  • ③state变为0了,把exclusiveOwnerThread属性变为null,释放锁的同时唤醒头结点

lock.lock(),调用AQS的release(),其内部调用子类的tryRelease(),ReentrantLock持有公平/非公平的对象

// ReentrantLock.java
public void unlock() {
    
    
    sync.release(1);
}

用tryRelease释放锁,释放成功后,检查头结点的waitStatus

唤醒后继结点

//AbstractQueuedSynchronizer.java
public final boolean release(int arg) {
    
    
    if (tryRelease(arg)) {
    
    //tryRelease防御true代表可重入锁释放完了 // 判断完if后已经把state变为0,可能非公平锁已经又抢了,而该非公平锁线程只是设置了个state和当前占用线程,并没有设置任何node相关
        Node h = head;
        // 接下来要考虑释放后非公平就抢到了state的情况
        if (h != null && // head为空的话没有线程排队,执行释放完成,无需唤醒其他线程。别的线程先不要设置头结点,直接执行即可
            h.waitStatus != 0) //防止队列中还有线程,但是该线程还未阻塞。前面说过,线程再阻塞自己前必须设置前驱结点状态为SIGNAL,否则不会唤醒自己
            // 把头结点的后继结点unpark。常规流程是进入后unpark那个后继结点的线程,而且unpart后,接下来执行的其实是我们之前获取锁时候阻塞的地方,从阻塞的地方接着去自旋获取锁,回到acQuireQueued()
            unparkSuccessor(h);//unpark后继者
        return true;
    }
    return false;//返回false代表还有重入锁没有释放完
}

尝试释放当前线程结点,就是对state进行操作

设置exclusiveOwnerThread为空,其他线程就可以设置了

为什么下面set和get时不用加锁:因为只有持有锁的线程才会执行release,而那个线程只有一个线程,只能可能发生并发

// ReentrantLock
protected final boolean tryRelease(int releases) {
    
    
    // state-1 // 由于该方法的线程必然持有锁,所以无需加同步操作。(其他向cas从0遍1的操作不会成功,而其他这个cas不成功就拿不到锁,而对于可冲入锁,他都是一个线程,一个线程不可能多个地方同时执行)
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        // 如果释放的线程和获取锁的线程不是同一个,抛出非法监视器状态异常
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果释放后锁为空了,就清空state后,返回true
    if (c == 0) {
    
     //在排它锁中,加锁的时候状态会增加1(当然可以自己修改这个值),在解锁的时候减掉1,同一个锁,在可以重入后,可能会被叠加为2、3、4这些值,只有unlock()的次数与lock()的次数对应才会将Owner线程设置为空,而且也只有这种情况下才会返回true。
        // 由于重入的关系,不是每次释放锁c都等于0,
        // 直到最后一次释放锁时,才会把当前线程释放
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);//设置为0后非公平锁就会开始抢了,他可能在这就抢到了,而我们公平锁还要往下执行
    return free;
}

释放完了原来线程的锁,唤醒头结点的后继结点

  • 如果没有后继结点,或者后继结点被取消了:从tail往前遍历结点,获取从头数第一个没被取消的结点s
  • 如果s有值,唤醒该s;如果s为null,代表没有线程阻塞获取该锁,直接结束
// AbstractQueuedSynchronizer.java
private void unparkSuccessor(Node node) {
    
    //继任者 // 参数为已经执行完的head // 此时可能已经被非公平锁的其他线程抢了,这里只是拿队首第一个node尝试获得锁 // 传入的是头结点
    /* 
     * 如果node的等待状态为负数(比如:可能需要一个信号),尝试去清空 
     * "等待唤醒"的状态(将状态置为0),即使设置失败,或者该状态已经被正在等待的线程修改,也没有任何影响。  
     */  
    int ws = node.waitStatus;
    // 如果头结点的waitStatus为负的,把头结点waitStatus改为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);//设置head的状态为0,失败了也无所谓

    /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
     /* 
     * 需要唤醒的线程在node的后继节点,一般来说就是node的next引用指向的节点。  
     * 但如果next指向的节点被取消或者为null,那么就同步等待队列的队尾反向查找离  
     * 当前节点最近的且状态不是"取消"的节点。  
     */  
    Node s = node.next;//头结点的后继结点
    // 如果没有后继结点 或 头结点的waitStatus>0即CANCEL(被取消了)
    if (s == null || s.waitStatus > 0) {
    
    
        s = null;
        // 从尾结点向前遍历,拿到从前往后的第一个能被唤醒的结点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                //至于为什么从尾部开始向前遍历,因为在doAcquireInterruptibly.cancelAcquire方法的处理过程中只设置了next的变化,没有设置prev的变化,在最后有这样一行代码:node.next = node,如果这时执行了unparkSuccessor方法,并且向后遍历的话,就成了死循环了,所以这时只有prev是稳定的
                s = t;
    }
    // 拿到了倒霉蛋,但是倒霉蛋被唤醒后可能又陷入阻塞
    //内部首先会发生的动作是获取head节点的next节点,如果获取到的节点不为空,则直接通过:“LockSupport.unpark()”方法来释放对应的被挂起的线程,这样一来将会有一个节点唤醒后继续进入循环进一步尝试tryAcquire()方法来获取锁 
    if (s != null)//如果存在(需要唤醒的节点),将该节点的线程唤醒。
        LockSupport.unpark(s.thread);//释放后继结点,让他去获取cpu,虽然他可能还得去阻塞
}

img

上面非公平锁分析完了,但哪里不公平了?

答案是线程刚进来要获取锁的时候,如果直接cas就是非公平了。如果先去排队尾再cas就公平

取消排队

// acquireQueued最后finally块中的cancelAcquire方法。
private void cancelAcquire(Node node) {
    
      
    // Ignore if node doesn't exist  
    if (node == null)  
        return;  
    //跳过首先将要取消的节点的thread域置空。  
    node.thread = null;  
    //跳过状态为"取消"的前驱节点。  
    Node pred = node.prev;  
    //node前面总是会存在一个非"取消"状态的节点,所以这里不需要null检测。  
    while (pred.waitStatus > 0)  
        node.prev = pred = pred.prev;  
    // predNext节点(node节点前面的第一个非取消状态节点的后继节点)是需要"断开"的节点。   
    // 下面的CAS操作会达到"断开"效果,但(CAS操作)也可能会失败,因为可能存在其他"cancel"   
    // 或者"singal"的竞争  
    Node predNext = pred.next;  
    // Can use unconditional write instead of CAS here.  
    // After this atomic step, other Nodes can skip past us.  
    // Before, we are free of interference from other threads.  
    node.waitStatus = Node.CANCELLED;  
    // 如果当前节点是尾节点,那么删除当前节点(将当前节点的前驱节点设置为尾节点)。  
    if (node == tail && compareAndSetTail(node, pred)) {
    
      
        //将前驱节点(已经设置为尾节点)的next置空。  
        compareAndSetNext(pred, predNext, null);  
    } else {
    
      
        //如果当前节点不是尾节点,说明后面有其他等待线程,需要做一些唤醒工作。  

        // 如果当前节点不是头节点,那么尝试将当前节点的前驱节点  
        // 的等待状态改成SIGNAL,并尝试将前驱节点的next引用指向  
        // 其后继节点。否则,唤醒后继节点。  
        int ws;  
        if (pred != head &&  
            ( (ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)) )  
            && pred.thread != null) {
    
      
            //如果当前节点的前驱节点不是头节点,那么需要给当前节点的后继节点一个"等待唤醒"的标记,  
            //即 将当前节点的前驱节点等待状态设置为SIGNAL,然后将其设置为当前节点的后继节点的前驱节点....(真绕!)  
            Node next = node.next;  
            if (next != null && next.waitStatus <= 0)  
                compareAndSetNext(pred, predNext, next);  
        } else {
    
      
            //否则,唤醒当前节点的后继节点。  
            unparkSuccessor(node);  
        }  
        //前面提到过,取消节点的next引用会指向自己。  
        node.next = node; // help GC  
    }  
}  

}

不需要排队的两种情况

AQS的

1、队列没有初始化,则不需要排队,直接去加锁,但是可能会失败;为什么会失败呢? 假设两个线程同时来lock,都看到队列没有初始化,都认为不需要排队,都去进行CAS修改计数器;但是肯定有一个会失败,这个时候他就会初始化队列并排队。

2、队列被初始化了,但是当前线程过来加锁,发觉队列当中头结点h就是自己,比如重入,因此不需要排队。

h != t 判断首不等于尾这里要分三种情况

1、队列没有初始化,也就是第一个线程来加锁,h和t都是null,&&运算所以后面不执行,直接返回false,但是这个方法取反了,所以会直接去cas加锁。

第一种情况总结:队列没有初始化,没人排队,那么我也不需要排队,直接上锁,直接去看能不能办理业务。

2、队列被初始化了,后面我们会分析队列初始化的流程,如果队列被初始化那么h!=t则成立;h != t 返回true;但是是&&运算,故而还需要进行后续的判断 ,(有人可能会疑问,比如队列里面只有一个数据,那么头和尾都是同一个怎么会成立呢?其实这是第三种情况–队列里面只有一个数据;这里先不考虑,假设现在队列里面有大于1个数据),继续判断把h.next赋值给s;s有是头的下一个,则表示他是队列当中参与排队的线程而且是排在最前面的;为什么是s最前面不是h嘛?诚然h是队列里面的第一个,但是不是排队的第一个;因为h是持有锁的,但是不参与排队;这个也很好理解,比如你去买火车票,你如果是第一个这个时候售票员已经在给你服务了,你不算排队,你后面的才算排队;然后判断s是否等于空,其实就是判断队列里面是否只有一个数据;假设队列大于1个,那么肯定不成立(s==null---->false),因为大于一个h.next肯定不为空;由于是||运算如果返回false,还要判断s.thread != Thread.currentThread();这里又分为两种情况:

2.1 s.thread != Thread.currentThread() 返回true,就是当前线程不等于在排队的第一个线程s;那么这个时候整体结果就是h!=t:true; (s==null false || s.thread != Thread.currentThread() true------> 最后true)结果: true && true 方法最终放回true,那么去则需要去排队,其实这样符合情理,队列不为空,有人在排队,而且第一个排队的人和现在来参与竞争的人不是同一个,那么你就乖乖去排队。

2.2 s.thread != Thread.currentThread() 返回false 表示当前来参与竞争锁的线程和第一个排队的线程是同一个线程 * 那么这个时候整体结果就是h!=t:true; (s==null false || s.thread != Thread.currentThread() false------> 最后false)结果 true && false 方法最终放回false,那么去则不需要去排队 * 不需要排队则调用 compareAndSetState(0, acquires) 去改变计数器尝试上锁;这里又分为两种情况:

<2.2.1>、第一种情况加锁成功?有人会问为什么会成功啊,很简单假如这个时候h也就是持有锁的那个线程执行完了,释放锁了,那么肯定成功啊;成功则执行 setExclusiveOwnerThread(current); 然后返回true 。

<2.2.2> 、第二种情况加锁失败?有人会问为什么会失败啊。很简单假如这个时候h也就是持有锁的那个线程没执行完,没释放锁,那么肯定失败啊;失败则直接返回false,不会进else if,但是他会去看看那个第一个排队的人是不是自己,如果是自己那么他就去尝试加锁;尝试看看锁有没有释放

第二种情况总结,如果队列被初始化了,而且至少有一个人在排队那么自己也去排队;但是他会去看看那个第一个排队的人是不是自己,如果是自己那么他就去尝试加锁;尝试看看锁有没有释放。

3、队列被初始化了,但是里面只有一个数据;什么情况下才会出现这种情况呢?可能有人会说ts加锁的时候里面就只有一个数据;其实不是,因为队列初始化的时候会虚拟一个h作为头结点,当前线程作为第一个排队的节点, 为什么这么做呢?因为aqs认为h永远是不排队的,假设你不虚拟节点出来那么ts就是h,而ts其实需要排队的,因为这个时候tf可能没有执行完,ts得不到锁,故而他需要排队;,那么为什么要虚拟为什么ts不直接排在tf之后呢,上面已经时说明白了,tf来上锁的时候队列都没有,他不进队列,故而ts无法排在tf之后,只能虚拟一个null节点出来;那么问题来了,究竟什么时候才会出现队列当中只有一个数据呢?假设原先队列里面有5个人在排队,当前面4个都执行完了,轮到第五个线程得到锁的时候;他会把自己设置成为头部,而尾部又没有,故而队列当中只有一个h就是第五个 * 至于为什么需要把自己设置成头部;其实已经解释了,因为这个时候五个线程已经不排队了,他拿到锁了,所以他不参与排队,故而需要设置成为h;即头部;所以这个时间内,队列当中只有一个节点 * 关于加锁成功后把自己设置成为头部的源码,后面会解析到;继续第三种情况的代码分析,记得这个时候队列已经初始化了,但是只有一个数据,并且这个数据所代表的线程是持有锁 * h != t false 由于后面是&&运算,故而返回false可以不参与运算,整个方法返回false;不需要排队

第三种情况总结:如果队列当中只有一个节点,而这种情况我们分析了,这个节点就是当前持有锁的那个节点,故而我不需要排队,进行cas。

区别

synchronized改的是this(的对象头),而lock改的是state

简单应用

Mutex:不可重入互斥锁,锁资源(state)只有两种状态:0:未被锁定;1:锁定。

class Mutex implements Lock, java.io.Serializable {
    
    
    // 自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {
    
    
        // 判断是否锁定状态
        protected boolean isHeldExclusively() {
    
    
            return getState() == 1;
        }

        // 尝试获取资源,立即返回。成功则返回true,否则false。
        public boolean tryAcquire(int acquires) {
    
    
            assert acquires == 1; // 这里限定只能为1个量
            if (compareAndSetState(0, 1)) {
    
    //state为0才设置为1,不可重入!
                setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
                return true;
            }
            return false;
        }

        // 尝试释放资源,立即返回。成功则为true,否则false。
        protected boolean tryRelease(int releases) {
    
    
            assert releases == 1; // 限定为1个量
            if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);//释放资源,放弃占有状态
            return true;
        }
    }

    // 真正同步类的实现都依赖继承于AQS的自定义同步器!
    private final Sync sync = new Sync();

    //lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。
    public void lock() {
    
    
        sync.acquire(1);
    }

    //tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。
    public boolean tryLock() {
    
    
        return sync.tryAcquire(1);
    }

    //unlock<-->release。两者语文一样:释放资源。
    public void unlock() {
    
    
        sync.release(1);
    }

    //锁是否占有状态
    public boolean isLocked() {
    
    
        return sync.isHeldExclusively();
    }
}

同步类在实现时一般都将自定义同步器(sync)定义为内部类,供自己使用;而同步类自己(Mutex)则实现某个接口,对外服务。

Condition

每个Condition包含一个等待队列,他是一个单向链表,但也维护这firstWaiter、lastWaiter

如果一个锁有两个condition,那么它就有两个队列

同步器维护这正常的双向链表,还维护这若干个conditoin单向队列

如果调用了await方法,就会把当前结点从同步队列里移动(重新包装)到condition等待队列中

如果调用了signal方法,就会加入到同步队列的尾部。

为什么非公平锁性能好

非公平锁对锁的竞争是抢占式的(队列中线程除外),线程在进入等待队列前可以进行两次尝试,这大大增加了获取锁的机会。这种好处体现在两个方面:

  • 1.线程不必加入等待队列就可以获得锁,不仅免去了构造结点并加入队列的繁琐操作,同时也节省了线程阻塞唤醒的开销,线程阻塞和唤醒涉及到线程上下文的切换和操作系统的系统调用,是非常耗时的。在高并发情况下,如果线程持有锁的时间非常短,短到线程入队阻塞的过程超过线程持有并释放锁的时间开销,那么这种抢占式特性对并发性能的提升会更加明显。
  • 2.减少CAS竞争。如果线程必须要加入阻塞队列才能获取锁,那入队时CAS竞争将变得异常激烈,CAS操作虽然不会导致失败线程挂起,但不断失败重试导致的对CPU的浪费也不能忽视。除此之外,加锁流程中至少有两处通过将某些特殊情况提前来减少CAS操作的竞争,增加并发情况下的性能。一处就是获取锁时将非重入的情况提前

参考

https://mp.weixin.qq.com/s/-MXuwOEaupFyh_2yylEZoA

https://blog.csdn.net/zhousenshan/article/details/77815022

https://blog.csdn.net/TJtulong/article/details/105345940

视频:https://www.bilibili.com/video/BV19J411Q7R5

视频文档:https://blog.csdn.net/hskw444273663/article/details/103018276

猜你喜欢

转载自blog.csdn.net/hancoder/article/details/107853193