AQS实现原理-AbstractQueuedSynchronizer原理解析

文章目录

  • 模板方法模式
    • AQS中的模板方法模式
  • AQS是什么
    • 自己实现一个独占式的AQS的Lock锁
    • AQS结构
      • 同步队列中的Node节点主要内容如下
      • 节点加入和退出同步队列过程
      • Condition的原理和作用
    • 独占式AQS-获取锁源码解析
    • 独占式AQS-释放锁源码解析
  • ReentrantLock原理
    • 可重入实现原理
    • 公平锁实现原理
  • 补充
    • LockSupport
    • 一个CAS的案例
    • Synchronized 和 ReenTrantLock 的对比
    • ThreadLocal原理
    • volatile和synchronized的区别

模板方法模式

由于AQS是采用了模板方法模式,所以先了解学习设计模式中的模板方法模式。

官方解释模板方法模式:定义一个操作中的算法框架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
其实就是由父类提供一个框架方法(模板方法),在框架方法中调用具体的流程方法,而具体的流程方法则是由实现它的子类进行实现。

例如:抽象父类

public  abstract class SendCustom {
     

    /**
     * 流程方法,由子类去覆盖
     */
    public abstract void to();
    public abstract void from();
    public abstract void content();
    public void date(){
     
        System.out.println("发送时间:"+new Date());
    }
    public abstract void send();

    /**
     * 框架方法
     */
    public void sendMessage(){
     
        to();
        from();
        content();
        date();
        send();
    }

}

实现的子类:

public class SendCustomImpl extends SendCustom {
     
    @Override
    public void to() {
     
        System.out.println("发给张三");
    }

    @Override
    public void from() {
     
        System.out.println("我是李四");
    }

    @Override
    public void content() {
     
        System.out.println("好久不见,你还好吗!");
    }

    @Override
    public void send() {
     
        System.out.println("可以发送了");
    }

    public static void main(String[] args) {
     
        SendCustomImpl obj = new SendCustomImpl();
        obj.sendMessage();
    }
}

结果如下:
AQS实现原理-AbstractQueuedSynchronizer原理解析_第1张图片

AQS中的模板方法模式

在AQS中的(框架)模板方法:

- 独占式获取锁:acquire()、acquireInterruptibly()、tryAcquireNanos() 
- 共享式获取锁:acquireShared()、acquireSharedInterruptibly()、tryAcquireShredNanos()
- 独占式释放锁:release() ​		
- 共享式释放锁:releaseShared()

需要子类覆盖的方法,例如使用了AQS的ReentrantLock、ReentrantReadWriteLock……或者是我们自己实现一个同步器锁时,都要重写下面这些方法。

 - 独占式获取锁:tryAcquire()
 - 共享式获取锁:tryAcquireShared()
 - 独占式释放锁:tryRelease()
 - 共享式释放锁:tryReleaseShared()
 - 判断同步器是否是独占模式:isHeldExclusively()

AQS是什么

AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面。

AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask(jdk1.7)等等皆是基于 AQS 的。

当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。

自己实现一个独占式的AQS的Lock锁

模仿ReentrantLock通过AQS定义一个自己的同步器。先明白一个状态变量state当状态值为0时,线程可以获取锁。

state这个变量是锁的同步状态:

- 获取当前同步状态:getState()
- 设置当前同步状态:setState()
- 使用cas设置状态,保证状态设置的原子性:compareAndSetState()
//返回同步状态的当前值
protected final int getState() {
     
        return state;
}
 // 设置同步状态的值
protected final void setState(int newState) {
     
        state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
     
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

由于我们自己实现的是一个独占锁,所以以下分析以独占式锁为主。

public class SelfLock implements Lock {
     
	//用内部类的方式实现AQS,重写流程方法
    private static class Sync extends AbstractQueuedSynchronizer{
     
			//定义一个线程调度器
	        Condition newCondition(){
     
	            return new ConditionObject();
	        }
			
			//尝试获取锁
	        @Override
	        protected boolean tryAcquire(int arg) {
     
	        	//使用了cas,判断当前的state状态是不是0,是0就拥有锁,并改状态state值为1
	            if (compareAndSetState(0,1)){
     
	                //给当前线程上一把独占锁(排他锁)
	                setExclusiveOwnerThread(Thread.currentThread());
	                return true;
	            }
	            return false;
	        }
	
	        @Override
	        protected boolean tryRelease(int arg) {
     
	            if(getState()==0){
     
	                throw new RuntimeException("没有线程占用锁异常!");
	            }
	            //当前独占线程为null
	            setExclusiveOwnerThread(null);
	            //设置状态为0
	            setState(0);
	            /*
	            问题:在获取锁时,判断当前锁状态使用的是compareAndSetState(0,1)保证了原子性,这里释放锁为什么不使用原子性,直接setState(0)?
	                因为这是实现独占锁,独占锁每次只能只有一个线程可以获取到锁,所以在释放的时候不存在多线程竞争,不用考虑原子性
	             */
	            return true;
	        }
	
	        @Override
	        protected boolean isHeldExclusively() {
     
	            //当前排他锁的状态
	            //return getState()==0;
	            //目前占有锁的线程是不是当前线程
	            return getExclusiveOwnerThread()==Thread.currentThread();
	        }
    }
	
    private final Sync sync=new Sync();

    @Override
    public void lock() {
     
        sync.acquire(1);//调用父类的模板(框架)方法
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
     
        sync.acquireInterruptibly(1);//调用父类的模板(框架)方法
    }

    @Override
    public boolean tryLock() {
     
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
     
        return sync.tryAcquireSharedNanos(1,unit.toNanos(time));
    }

    @Override
    public void unlock() {
     
         sync.release(1);//调用父类的模板(框架)方法
    }
    
	//获取调度器
    @Override
    public Condition newCondition() {
     
        return sync.newCondition();
    }
}

测试:

public class SelfLockTest {
     
    public void test(){
     
        // final Lock lock=new ReentrantLock(); //使用自己实现的锁和ReentrantLock效果一样
        final Lock lock=new SelfLock();

        class Worker extends Thread {
     
            @Override
            public void run() {
     
                while (true){
     
                    lock.lock();
                    try {
     
                        //睡一秒
                        try {
     
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
     
                            e.printStackTrace();
                        }
                        //业务代码---输出名称
                        System.out.println(Thread.currentThread().getName());
                        //睡一秒
                        try {
     
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
     
                            e.printStackTrace();
                        }
                    }finally {
     
                        lock.unlock();
                    }
                    //睡2秒
                    try {
     
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
     
                        e.printStackTrace();
                    }
                }
            }
        }

        //启动10个线程
        for (int i = 0; i <10; i++) {
     
            Worker worker=new Worker();
            worker.setDaemon(true);
            worker.start();
        }

        //主线程没隔1秒换行
        for (int i = 0; i < 10; i++) {
     
            try {
     
                Thread.sleep(1000);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
     
        SelfLockTest test=new SelfLockTest();
        test.test();
    }
}

问题:在测试中,我们创建了10个线程,去争抢自己实现的AQS独占式锁,效果和ReentrantLock一样,说明我们定义的锁是没有问题的。那么AQS内部如何安排这10个线程的,当有一个线程拿到锁时,其他9个线程是如何安排的?内部是如何实现的?

下面分享一下AQS的内部结构就大概明白这个原理了!!!

AQS结构

他的内部维护了一个双向队列,节点用来装每个线程。这个队列按照专业说法叫:

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。

AQS实现原理-AbstractQueuedSynchronizer原理解析_第2张图片

同步队列中的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;//等待队列 Link to next node waiting on condition
 }
SIGNAL:(signal)后续节点等待状态,当前节点通知后面节点去运行 
CANCELLED:(cancelled)线程等待或者被中断了,需要从队列中移走
CONDITION:当前节点处于等待duil
PROPAGATE:共享,表示状态要往后面的节点传播
0: 表示初识状态

节点加入和退出同步队列过程

当一个线程获取锁失败时,java就会把当前线程打包成一个Node节点,这些节点通过前驱和后驱指针链接在一起构成一个同步对列。

例如上面的例子中:10个线程争抢独占式锁,每次都只有一个线程获取到锁,剩下的线程就会打包成Node构成同步对列,当第一个获取到锁的线程释放锁后,会依次唤醒对列的线程去拿锁,这个同步队列可以理解为拿锁失败的线程链表。

节点在同步对列的增加和移出过程(线程从等待到获取到锁的过程)如图所示:
AQS实现原理-AbstractQueuedSynchronizer原理解析_第3张图片
注意:这里在设置尾节点时采用了cas、而设置头结点却没有,是因为:在争夺锁的线程是不确定的可能有多个,但是获取独占锁的只有一个线程,那么就有多个线程获取锁失败,会链接到队列尾节点上,这里使用cas保证了不会发生错误。而首节点只有一个是确定的。

综上所述获取独占式同步锁的过程如下图:
AQS实现原理-AbstractQueuedSynchronizer原理解析_第4张图片

Condition的原理和作用

先明白condition接口是为了Lock锁的调度的接口,每个lock锁都应该实现这个接口进行线程间的调度。
AQS实现原理-AbstractQueuedSynchronizer原理解析_第5张图片
condition中也是Node节点的链表。
AQS实现原理-AbstractQueuedSynchronizer原理解析_第6张图片
每通过锁new一个Condition对象,就会连接在这个锁的同步器同步队列的的nextWaiter指针中,也就是说同步队列只有一个,而等待队列可能有多个,不过通常建议是一个同步队列,只new一个Condition对象,也就是一个等待队列。
AQS实现原理-AbstractQueuedSynchronizer原理解析_第7张图片
如果一个condition能调用await()方法,说明已经拥有锁了,那么就会让拥有锁的线程Node移动到等待队列中去。

signal()方法唤醒时,会发出通知,让节点回到同步队列中去竞争锁,而此时锁被那个线程占用是不确定的,所以会连接在同步队列的尾部。
AQS实现原理-AbstractQueuedSynchronizer原理解析_第8张图片
Condition不建议使用signalAll()方法的原因其实就是避免这个操作,因为要把所有位于此Condition对象的等待队列中的节点,统统转移到同步队列尾部去。
synchronized中则建议使用notifyAll(), 它的内部其实也是维护了一个同步队列,但是等待队列只有一个,所以线程们调用wait()进入这个唯一的等待队列中后,是不能确保哪一个线程节点处于等待队列的头结点的,所以在notify()唤醒时就可能不是想要的那个线程,所以要使用notifyAll()唤醒所有线程节点,由线程自己去判定条件是否满足。

独占式AQS-获取锁源码解析

直接上图了,结合上面的分析,慢慢看图中的解析慢慢读源码吧。
AQS实现原理-AbstractQueuedSynchronizer原理解析_第9张图片

独占式AQS-释放锁源码解析

直接上图了,结合上面的分析,慢慢看图中的解析慢慢读源码吧。

AQS实现原理-AbstractQueuedSynchronizer原理解析_第10张图片

ReentrantLock原理

可重入实现原理

ReentrantLock的可重入原理在于他每次获取锁时都进行加操作,释放锁时进行了减操作,在做cas判断锁状态时,保证了重入多少次就释放多少次。

以非公平锁为例看看源码:
AQS实现原理-AbstractQueuedSynchronizer原理解析_第11张图片

公平锁实现原理

公平锁:
AQS实现原理-AbstractQueuedSynchronizer原理解析_第12张图片
非公平锁:
AQS实现原理-AbstractQueuedSynchronizer原理解析_第13张图片

这也就是非公平锁比公平锁效率高的原因:因为它在获取锁的时候,直接判断当前锁有没有被别的线程占用,没有就直接获取了,不管队列中有没有线程在等待。公平锁则要老老实实的等待,前驱节点的线程执行完,再去获取锁。

补充

LockSupport

LockSupport工具类提供了一些方法,方便阻塞、唤醒、构建同步组件的基础工具。park()开头的方法就是阻塞。
AQS实现原理-AbstractQueuedSynchronizer原理解析_第14张图片

一个CAS的案例

AQS实现原理-AbstractQueuedSynchronizer原理解析_第15张图片

Synchronized 和 ReenTrantLock 的对比

以下内容来自慕课网


① 两者都是可重入锁

两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

③ ReenTrantLock 比 synchronized 增加了一些高级功能

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

  • ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。

④ 性能已不是选择标准

在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作

ThreadLocal原理

volatile和synchronized的区别

  1. volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止
  2. volatile仅能使用在变量级别; synchronized则可以使用在变量、方法和类级别
  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性
  4. volatile不会造成线程的阻塞; synchronized可能会造成线程的阻塞
  5. volatile标记的变量不会被编译器优化; synchronized标记的变量可以被编译器优化

你可能感兴趣的:(java,面试,多线程,并发编程,AQS原理,java)