AQS(AbstractQueueSynchronizer)
- AQS原理概览
- AQS意为抽象队列同步器,JUC包下的Lock和其他一些并发工具类都是基于它来实现的。AQS维护了一个被volatile修饰的state状态变量和一个CLH(FIFO)双向队列。
- AQS的核心思想是,如果被请求的共享资源是空闲,则将当前请求资源的线程设置为有效的工作线程(exclusiveOwnerThread),并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
- CLH(Craig,Landin,and Hagersten) 是AQS内部类Node所维护的一个队列,队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
- AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态state进行原子操作实现对其值的修改。
- private volatile int state;//共享变量,使用volatile修饰保证线程可见性;
- state状态信息通过protected类型的getState,setState,compareAndSetState方法进行操作。
- AQS对资源的共享方式
- Exclusive(独占):只有一个线程能在同一时刻获取资源并执行,如ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock同时支持两种锁,默认采用非公平锁。
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁;
- 非公平锁:当线程要获取锁时,先通过两次CAS操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。
- 异同点:
- 非公平锁在调用lock后,首先就会调用CAS进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回;
- 非公平锁在CAS失败后,和公平锁一样都会进入到tryAcquire方法,在tryAcquire方法中,如果发现锁此时被释放了(state==0),非公平锁会CAS 循环竞争锁,但是公平锁会判断等待队列中是否有线程处于等待状态,如果有则不去抢锁,进入队列排队;
- 如果两次CAS都不成功,那么非公平锁和公平锁处理方式一致,都要进入到阻塞队列等待唤醒;
- 相对来说,非公平锁会有更好的性能,因为它的吞吐量大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
- 核心方法:ReentrantLock.lock()方法,实际调用的是AQS中acquire(1)方法,其处理流程如下:
public final void acquire(int arg) { if (!this.tryAcquire(arg) && this.acquireQueued(this.addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg)) { selfInterrupt(); } }
- 调用自定义同步器的tryAcquire()尝试获取资源,成功则直接返回;
- 失败,则addWaiter()将该无法获得锁的线程包装成一个Node结点,通过CAS加入等待队列的尾部,并标记为独占模式;
- acquireQueued()把已经追加到队列的线程结点进行阻塞,使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果整个等待过程中被中断过,则返回true,否则返回false;
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
- Share(共享):多线程可同时获取到资源并执行,如CountDownLatch,ReadWriteLock等;
- 自定义同步器在实现:只需实现共享资源state的获取(acquire)和释放(release)方法即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS中大部分逻辑已经被实现。其中需要继承类重写的方法有:
- tryAcquire(int arg) / tryRelease(int arg):独占式的获取资源/释放资源方法;成功则返回true,失败返回false;
- tryAcquireShared(int arg):共享式的获取资源方法,返回负数表示失败,0表示获取成功,但是没有可用资源;正数表示获取成功,且有可用资源;
- tryReleaseShared(int arg):共享式的释放资源方法,如果允许唤醒后续等待线程则返回true,不允许则返回false;
- isHeldExclusively():判断当前线程是否正在独享资源,是则返回true,否则返回false。
- Exclusive(独占):只有一个线程能在同一时刻获取资源并执行,如ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock同时支持两种锁,默认采用非公平锁。
ReentrantLock类和synchronized
-
Synchronized
- synchronized 经过编译,会在同步块的前后分别形成monitorenter 和monitorexit 两个字节码指令。
- 在执行monitorenter 指令时,首先要尝试获取对象的监视器,这个监视器是对象独有。如果这个对象没被锁定,或者当前线程已经拥有了该对象锁,把锁的计算器加1;
- 相应的,在执行monitorexit 指令时,会将锁计算器减1,当计算器为0时,锁就被释放了。
- 如果获取对象锁失败,线程会进入队列(SynchronizedQueue)当中等待,直到对象锁被另一个线程释放为止。
- 最后,当前线程执行完后,通知出队。然后继续重复当前过程。
- 从JVM 角度分析,monitorenter 和 monitorexit 指令代表着代码的执行和结束。
- jdk1.6以后,synchronized 进行了优化,引入了锁的升级机制:无锁 -》 偏向锁-》自旋锁(轻量级锁) -》 重量级锁
-
ReentrantLock
- ReentrantLock 意为可重入锁,其底层使用AQS去实现的。有两个模式:公平锁和非公平锁。
- AQS 使用state 同步状态字段(0代表无锁,1代表有锁),并暴露出 getState、setState 以及 compareAndSet 操作来读取和更新这个状态,使得仅当同步状态拥有一个期望值时,才会被原子地设置成新值。
- 高级功能:
- 实现可轮询的锁请求。可轮询的锁获取模式具有更完善的错误恢复机制,可规避死锁的发生。
- 实现可定时的锁请求。当具有时限的线程调用阻塞方法,定时锁能够在时间预算内设定相应的超时。如果线程在期待的时间内没能获得结果,定时锁能使程序提前返回。tryLock(long time, TimeUnit unit)。
- 实现可中断的锁获取请求。持有锁的线程长期不释放时,正在等待的线程可以选择放弃等待,lockInterruptibly()方法能够使你获得锁时响应中断。相对于synchronized 来说,可避免出现死锁情况。
- 公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,synchronized 锁是非公平锁。ReentrantLock 可使用公平锁和非公平锁。
- 锁绑定多个条件,一个ReentrantLock 对象可以同时绑定多个对象。
- ReadWriteLock 读写锁
- 其是在JDK5 中提供的读写分离锁,读写分离锁可以有效地帮助减少锁竞争,以提升系统性能;
-
ReentrantLock 和 Synchronized 区别
-
Synchronized 的锁升级如果最终升级为重量级锁,在使用过程中是没有办法消除的,意味着每次都要去向CPU 申请锁资源。适合于并发竞争低的情况。
- ReentrantLock 主要是提供阻塞的能力,通过在高并发下线程的挂起,来减少竞争,提高并发能力。
- Synchronized 是隐式锁,可自动释放锁,ReentrantLock 是显式锁,需要手动释放锁。
- ReentrantLock 可以让等待锁的线程响应中断,而synchronized 不行。使用synchronized 时,等待的线程会一直等待下去,不能够响应中断。
- ReentrantLock 可获取锁状态,而synchronized 不能。
-