AQS
- AbstractQueuedSynchronizer (AQS)抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,比如ReentrantLock/Semaphore/CountDownLatch
- 它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义
- state的访问方式有三种: getState() 、setState() 、compareAndSetState()
- AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share (共享,多个线程可同时执行,例如Semaphore/CountDownLatch)不同的自定义同步器争用共享资源的方式也不同
- 线程抢占同一份资源,只有被标杆节点选中的才可以访问资源,其余的进入排队队列,如果是公平锁,则按照先后顺序进行对于资源的访问;如果是非公平锁,则当标杆节点释放完之后,大家开始抢占资源,谁抢到算谁的,没有先来后到之分
自定义的同步容器
- 自定义同步器在实现时只需要实现共享资源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是能回到零态的
- 重入锁,在需要进行同步的代码部分加上锁定,但不要忘记最后一定要释放锁定,不然会造成锁永远无法释放,其他线程永远进不来的结果
- t2抢占线程,只有等t2线程结束,线程t1才有资格抢占资源
package com.example.core.aqs;
import java.util.concurrent.locks.ReentrantLock;
public class UseReentrantLock {
private ReentrantLock reentrantLock = new ReentrantLock();
public void method(){
reentrantLock.lock();
try{
System.out.println("当前线程:"+Thread.currentThread().getName()+"进入...");
Thread.sleep(2000);
System.out.println("当前线程:"+Thread.currentThread().getName()+"退出...");
}catch(InterruptedException e){
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
public static void main(String[] args) {
UseReentrantLock useLock = new UseReentrantLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
useLock.method();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
useLock.method();
}
},"t2");
t1.start();
t2.start();
}
}
/*
output:
当前线程:t2进入...
当前线程:t2退出...
当前线程:t1进入...
当前线程:t1退出...
*/
公平锁和非公平锁
- Lock lock = new ReentrantLock(boolean isFair);
lock用法
- tryLock():尝试获得锁,获得结果用true/false返回
- tryLock():在给定的时间内尝试获得锁,获得结果用true/false返回
- isFair():是否是公平锁
- isLocked():是否锁定
- getHoldCount(): 查询当前线程保持此锁的个数,也就是调用lock()次数
- lockInterruptibly():优先响应中断的锁
- getQueueLength():返回正在等待获取此锁定的线程数
- getWaitQueueLength():返回等待与锁定相关的给定条件Condition的线程数
- hasQueuedThread(Thread thread): 查询指定的线程是否正在等待此锁
- hasQueuedThreads():查询是否有线程正在等待此锁
- hasWaiters():查询是否有线程正在等待与此锁定有关的condition条件
- 再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()调用线程,然后主调用线程就会从await()函数返回,继续后余动作
AQS Condition
- 使用synchronized的时候,如果需要多线程间进行协作工作则需要Object的wait()和notify()、notifyAll()方法进行配合工作
- 那么同样,我们在使用Lock的时候,可以使用一个新的等待/通知的类,它就是Condition。这个Condition一定是针对具体某一把锁的。也就是在只有锁的基础之上才会产生Condition
- 我们可以通过一个Lock对象产生多个Condition进行多线程间的交互,非常的灵活。可以使得部分需要唤醒的线程唤醒,其他线程则继续等待通知
使用一个条件
- t1线程先执行,进入等待的时候,释放锁,t2才可以得以执行,t2线程开始执行,对于t1线程发出唤醒通知,t1得以继续执行,最后释放锁
package com.example.core.aqs;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class UseCondition {
//现在有一把锁
private Lock lock = new ReentrantLock();
//synchronized wait ---- notify
//基于这把锁产生了一个 condition: 作用是对于这把锁的 唤醒 和 等待操作
private Condition condition = lock.newCondition();
public void method1(){
lock.lock();
try {
System.out.println("当前线程:" + Thread.currentThread().getName() + "进入等待状态..");
Thread.sleep(3000);
System.out.println("当前线程:" + Thread.currentThread().getName() + "释放锁..");
condition.await();
System.out.println("当前线程:" + Thread.currentThread().getName() +"继续执行...");
} catch (Exception e) {
e.printStackTrace();
} finally {
System.err.println(Thread.currentThread().getName() + " unlock");
lock.unlock();
}
}
public void method2(){
lock.lock();
try {
System.out.println("当前线程:" + Thread.currentThread().getName() + "进入..");
Thread.sleep(3000);
System.out.println("当前线程:" + Thread.currentThread().getName() + "发出唤醒..");
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws Exception {
final UseCondition uc = new UseCondition();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
uc.method1();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
uc.method2();
}
}, "t2");
t1.start();
Thread.sleep(1);
t2.start();
}
}
/*
output
当前线程:t1进入等待状态..
当前线程:t1释放锁..
当前线程:t2进入..
当前线程:t2发出唤醒..
当前线程:t1继续执行...
t1 unlock
*/
使用多个条件
- 创建两个条件,条件c1和c2,c1条件受制于t1和t2线程,由t3线程进行唤醒;c2条件受制于t3线程,由t5线程进行唤醒
package com.example.core.aqs;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class UseManyCondition {
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
public void m1(){
try {
lock.lock();
System.out.println("当前线程:" +Thread.currentThread().getName() + "进入方法m1等待..");
c1.await();c1条件 由m4唤醒
System.out.println("当前线程:" +Thread.currentThread().getName() + "方法m1继续..");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m2(){
try {
lock.lock();
System.out.println("当前线程:" +Thread.currentThread().getName() + "进入方法m2等待..");
c1.await();//c1条件 由m4唤醒
System.out.println("当前线程:" +Thread.currentThread().getName() + "方法m2继续..");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m3(){
try {
lock.lock();
System.out.println("当前线程:" +Thread.currentThread().getName() + "进入方法m3等待..");
c2.await();c2条件 由m5唤醒
System.out.println("当前线程:" +Thread.currentThread().getName() + "方法m3继续..");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m4(){
try {
lock.lock();
System.out.println("当前线程:" +Thread.currentThread().getName() + "唤醒..");
c1.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void m5(){
try {
lock.lock();
System.out.println("当前线程:" +Thread.currentThread().getName() + "唤醒..");
c2.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final UseManyCondition umc = new UseManyCondition();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
umc.m1();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
umc.m2();
}
},"t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
umc.m3();
}
},"t3");
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
umc.m4();
}
},"t4");
Thread t5 = new Thread(new Runnable() {
@Override
public void run() {
umc.m5();
}
},"t5");
t1.start();
t2.start();
t3.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t4.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t5.start();
}
}
/*
output
当前线程:t1进入方法m1等待..
当前线程:t3进入方法m3等待..
当前线程:t2进入方法m2等待..
当前线程:t4唤醒..
当前线程:t1方法m1继续..
当前线程:t2方法m2继续..
当前线程:t5唤醒..
当前线程:t3方法m3继续..
*/
AQS-ReentrantReadWriteLock
- 读写锁ReentrantReadWriteLock,其核心就是实现读写分离的锁。在高并发访问下,尤其是读多写少的情况下,性能要远高于重入锁。
- 之前学synchronized、ReentrantLock时,我们知道,同一时间内,只能有一个线程进行访问被锁定的代码,那么读写锁则不同,其本质是分成两个锁,即读锁、写锁。在读锁下,多个线程可以并发的进行访问,但是在写锁的时候,只能一个一个的顺序访问
- 口诀:读读共享,写写互斥,读写互斥
- t1和t2都是读锁,t3是写锁,t1和t2可以并行执行,他们与t3之间不可以同时执行。读读共享,写写互斥,读写互斥
package com.example.core.aqs;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class UseReadWriteLock {
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
public void read(){
readLock.lock();
try{
System.out.println("当前线程 "+ Thread.currentThread().getName()+ " 进入了读方法");
Thread.sleep(3000);
System.out.println("当前线程 "+ Thread.currentThread().getName()+ " 退出了读方法");
}catch (Exception e){
e.printStackTrace();
}finally{
readLock.unlock();
}
}
public void write(){
writeLock.lock();
try{
System.out.println("当前线程 "+ Thread.currentThread().getName()+ " 进入了写方法");
Thread.sleep(3000);
System.out.println("当前线程 "+ Thread.currentThread().getName()+ " 退出了写方法");
}catch (Exception e){
e.printStackTrace();
}finally{
writeLock.unlock();
}
}
public static void main(String[] args) {
UseReadWriteLock rwLock = new UseReadWriteLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
rwLock.read();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
rwLock.read();
}
},"t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
rwLock.write();
}
},"t3");
t1.start();
t2.start();
t3.start();
}
}
/*
output:
当前线程 t3 进入了写方法
当前线程 t3 退出了写方法
当前线程 t1 进入了读方法
当前线程 t2 进入了读方法
当前线程 t1 退出了读方法
当前线程 t2 退出了读方法
*/
LockSupport
- 提供park()和unpark()方法实现阻塞线程和解除线程阻塞,实现的阻塞和解除阻塞是基于”许可(permit)”作为关联,permit相当于一个信号量(0,1),默认是0. 线程之间不再需要一个Object或者其它变量来存储状态,不再需要关心对方的状态
- LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦.
- unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序
package com.example.core.aqs;
import java.util.concurrent.locks.LockSupport;
public class UseLockSupport {
public static void main(String[] args) throws Exception {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
LockSupport.park(); //滞后的
System.out.println(sum);
}
});
A.start();
//后阻塞:
Thread.sleep(1000);
LockSupport.unpark(A); //优先的
}
}
package com.example.core.aqs;
public class UseObjectLock {
public static void main(String[] args) throws Exception {
Object lock = new Object();
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(sum);
}
});
A.start();
//后阻塞:
Thread.sleep(1000);
synchronized (lock) {
lock.notify();
}
}
}
AQS-锁优化
- 避免死锁
- 减小锁的持有时间
- 减小锁的粒度
- 锁的分离
- 尽量使用无锁的操作,如原子操作(Atomic系列类),volatile关键字
acquire(int)
- 此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了
- tryAcquire()尝试直接去获取资源,如果成功则直接返回
- addWaiter()将该线程加入等待队列的尾部,并标记为独占模式
- acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上