深入理解CAS和AQS

CAS(compare and swap)

悲观锁和乐观锁

悲观锁:悲观的认为每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁,比如synchronized和数据库层面的 for update,加锁和释放锁会导致比较多的上下文切换和调度延时,引起性能问题

//保证方法每次只能被一个线程访问
public synchronized void test() {}
//select 不会主动加锁,for update加锁
select * from tb where xxx for update

乐观锁:乐观的认为每次去拿数据的时候都认为不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,使用版本号或CAS实现

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

//版本号或者比对时间戳
update task set version =  #{version} + 1 where version = #{version};

CAS 底层原理

从内存模型角度去理解,假设线程A和线程B都取到了CL值,A修改值之后,对比主内存的值和缓存副本原值,如果相同则认为没有人刷新过内存,则将修改过的新值刷新到主内存中;而B修改之后,对比发现原来的值与主内存的值不相同,则不会将新值刷新到主内存。失败的线程不会挂起,仅是被告知失败,并且允许再次尝试,当然也允许实现的线程放弃操作。有点类似于Git的操作,本地随意修改,但是提交到远程分支时,如果和原理的内容不一样就会有冲突,所以每次提交代码前会更新一下,也就是重新刷一下内存。

偏移量的概念:内存中物理地址就是基地址x16+偏移地址,比如 0BAC:0100,0BAC是基地址,0100是偏移地址

Unsafe类使用

Unsafe类可以手动管理内存

//package sun.misc;
public final class Unsafe {
    private static final Unsafe theUnsafe;
    // 构造函数是private的,不允许外部实例化
    private Unsafe() {
    }
    
    static {
        theUnsafe=new Unsafe();
    }
    ...
}

通过反射获取私有字段的属性值

public class LoanVo {
    private static final String name;

    static {
        name = "nihao";
    }
}

    Field field = LoanVo.class.getDeclaredFields()[0];
    field.setAccessible(true);
    Object o = field.get(new LoanVo());
    System.out.println(o);//nihao

获取Unsafe实例并进行操作

Field field = Unsafe.class.getDeclaredFields()[0];
//在访问时会忽略私有修饰符的检查
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
//对象操作
LoanVo loanVo = (LoanVo) unsafe.allocateInstance(LoanVo.class);
loanVo.setName("loan");
System.out.println(loanVo);//LoanVo(name=loan)

偏移量:字段内存的地址相对于对象内存地址的偏移量

操作对象:objectFieldOffset

//name字段在内存中的地址相对于对象内存地址的偏移量
Field name = LoanVo.class.getDeclaredField("name");
long fieldOffset = unsafe.objectFieldOffset(name);
System.out.println(fieldOffset); //16
//读写一个Object属性
System.out.println(unsafe.getObject(loanVo,fieldOffset));//loan
unsafe.putObject(loanVo,fieldOffset,"newValue");
System.out.println(loanVo);//LoanVo(name=newValue)

动态创建类:defineClass

String classPath = "F:\\Program Files\\UnsafeTest.class";
File file = new File(classPath);
FileInputStream classIn = new FileInputStream(file);
byte[] buffer = new byte[(int) file.length()];
classIn.read(buffer);
Class<?> aClass = unsafe.defineClass(null, buffer, 0, buffer.length, null, null);
System.out.println(aClass);//class cn.mytest.samplemq.service.UnsafeTest

内存操作:

可以在Java内存区域中分配内存(allocateMemory),初始化内存(setMemory),清除内存( freeMemory)

//直接脱离jvm,gc将无法管理以下方式申请的内存,一定要手动释放内存,避免内存溢出
//申请一个大小为8字节的内存
long memory = unsafe.allocateMemory(8);//地址为33333530
//像内存中设置值为1
unsafe.putInt(memory,(byte)1);
System.out.println(unsafe.getInt(memory)); //1
//直接获取内存的值 getByte(long addr)/putByte(long addr,byte value)
//和map类型的操作
byte value = unsafe.getByte(memory); // 1

unsafe.putByte(memory,(byte) 3);
System.out.println(unsafe.getByte(memory));//3

//继续向地址偏移出设置非基本类型的值
//putXXX(XXX ,long , XXX),第一个参数相当于作用域,第二个参数就是,第三个参数就是设置的值
LoanVo loanVo = new LoanVo();
loanVo.setName("haha");
//比如unsafe.putObject(loanVo,fieldOffset,"newValue") 相当于 unsafe.setName("newValue")
//设置的是偏移量内存的值,而下面这个相当于对当前地址的内存设值
unsafe.putObject(LoanVo.class,memory+2,loanVo);
System.out.println(unsafe.getObject(LoanVo.class,memory+2));//LoanVo(name=haha)

unsafe.freeMemory(memory);
System.out.println(unsafe.getByte(memory));

Unsafe的CAS操作

unsafe有很多操作暂时没有去了解,因为最重要的是CAS操作;cas方法都有4个参数:第一个是需要更新的作用域,第二个参数是需要比较的内存地址,第三个拿去跟地址内的值进行比较的期望值,第四个参数就是如果期望值和地址内的值相同则将这个值更新设置为新内存值

//包含了compareAndSwapInt、compareAndSwapLong、compareAndSwapObject三个方法
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

//还是申请一个内存,获取其地址
long memory = unsafe.allocateMemory(2);
LoanVo loanVo = new LoanVo();
loanVo.setName("loan");
//缩小一下域的范围
Field name = LoanVo.class.getDeclaredField("name");
//获取字段偏移地址
long fieldOffset = unsafe.objectFieldOffset(name);
boolean c = unsafe.compareAndSwapObject(loanVo, fieldOffset, "name", "dog fuc");
boolean d = unsafe.compareAndSwapObject(loanVo, fieldOffset, loanVo.getName(), "dog fuc");
System.out.println(unsafe.getObject(loanVo,fieldOffset)); //dog fuc

CAS机制的缺点

cas是一个非阻塞的轻量级锁,采用自旋锁进行CAS拿锁,当线程竞争激烈时长时间自旋不成功则会占用cpu较高,在JUC中就加了限制CAS次数

只能保证一个共享变量原子操作,如果对多个变量操作,可以加锁来解决或者封装成对象类解决

ABA问题:可能会造成其他线程修改过多次之后,原值却不变的情况,比如先修改为1,然后修改为2,又修改为1,解决的方式就是增加一个版本号,每次修改都会加一个版本,其实完全不用进行CAS,直接比对版本号也行!

Atomic

atomic:原子的,所以这个包提供原子变量的类

深入理解CAS和AQS_第1张图片

借助硬件的相关指令来实现,不会阻塞线程(或者说只是在硬件级别上阻塞了)。可以对基本数据、数组中的基本数据、对类中的基本数据进行操作。原子变量类相当于一种泛化的volatile变量,能够支持原子的和有条件的读-改-写操作。

可以分成4组:

标量类(Scalar):AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

复合变量类:AtomicMarkableReference,AtomicStampedReference

标量类用来处理布尔,整数,长整数,对象四种数据,实现方式CAS + volatile,从而避免了synchronized的高开销。

以AotomicInteger为例,先获取内存操作的Unsafe实例,通过objectFieldOffset获取字段value的valueOffset偏移量,对变量属性进行读写

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicReference.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}
//
private volatile int value;

底层以CAS方式去自旋设值

public final int getAndSet(int newValue) {
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}

public final int getAndAdd(int delta) {
	return unsafe.getAndAddInt(this, valueOffset, delta);
}

举个实例:getAndIncrement()类似于i++

AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.getAndSet(10);

new Thread(()->{
    for (int i=0;i<10;i++) {
        int andIncrement = atomicInteger.getAndIncrement();
        System.out.println(andIncrement);
    }
}).start();
new Thread(()->{
    for (int i=0;i<10;i++) {
        int andIncrement = atomicInteger.getAndIncrement();
        System.out.println(andIncrement);
    }
}).start();
new Thread(()->{
    for (int i=0;i<10;i++) {
        int andIncrement = atomicInteger.getAndIncrement();
        System.out.println(andIncrement);
    }
}).start();

AQS (AbstractQueuedSynchronizer)

抽象队列式的同步器提供了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch

深入理解CAS和AQS_第2张图片

同步状态:AQS中维持一个全局的int状态码(state),线程通过修改(加/减指定的数量)码是否成功来决定当前线程是否成功获取到同步状态。

//state是AQS的核心,因为AQS队列就是用于维护这个值
private volatile int state;
//用于指向-->队列中的头结点和尾部节点
private transient volatile Node head;
private transient volatile Node tail;

同步队列:先进先出(FIFO)的双向队列,每个节点Node就代表一个线程,节点中又会有两个指针去指向前驱节点和后继节点,同步器Head指向获取同步状态的的第一个节点,Tail指向最后一个节点

static final class Node {
    //前驱节点
    volatile Node prev;
    //后继节点
    volatile Node next;
    //当前线程
    volatile Thread thread;
    //封装新的node节点
    Node(Thread thread, Node mode) {
        //mode指节点类型,共享锁或独占锁
        this.nextWaiter = mode;
        this.thread = thread;
    }
}

深入理解CAS和AQS_第3张图片

独占锁和共享锁

AQS支持两种获取同步状态的模式既独占式和共享式。独占式模式同一时刻只允许一个线程获取同步状态,而共享模式则允许多个线程同时获取。ReentrantLock就是以独占方式实现的互斥锁,ReadWriteLock读锁是共享锁,写锁是独占锁,Semaphore是一种共享锁

static final class Node {
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
}

以ReentrantReadWriteLock为例,AQS定义了独占模式的acquire()和release()方法,共享模式的acquireShared()和releaseShared()方法.

public static class ReadLock implements Lock {

    public void lock() {
        //addWaiter(Node.SHARED),设置节点为共享状态
        sync.acquireShared(1);
    }
    
    public void unlock() {
        sync.releaseShared(1);
    }
}

public static class WriteLock implements Lock {

    public void lock() {
        //addWaiter(Node.EXCLUSIVE),设置节点为独占状态
        sync.acquire(1);
    }
    
    public void unlock() {
        sync.release(1);
    }
}

公平锁和非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。初始化时, state=0,表示无人占锁。A线程请求锁并取得了锁,把 state原子性+1,这时候state被改为1,A线程继续执行其他任务,然后线程B请求锁,无法获取锁,生成节点进行排队。初始化的时候,会生成一个空的头节点,然后才是B线程节点,这时候,如果线程A又请求锁,不需要排队,把状态值进行累加。如果线程A释放了一次锁仅仅是把状态值减了,只有线程A把此锁全部释放了,状态值减到0了,其他线程才有机会获取锁。当A把锁完全释放后,state恢复为0,然后会通知队列唤醒B线程节点,使B可以再次竞争锁。当然,如果B线程后面还有C线程,C线程继续休眠,除非B执行完了,通知了C线程。当一个线程节点被唤醒然后取得了锁,对应节点会从队列中删除。

非公平锁简单来说就是不需要排队,当线程A执行完之后,要唤醒线程B是需要时间的,而且线程B醒来后还要再次竞争锁,所以如果在切换过程当中,来了一个线程C,那么线程C是有可能获取到锁的,如果C获取到了锁,B就只能继续休眠了。

ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁

//指定锁类型
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
//默认为非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}
//实现公平锁
private ReentrantLock reentrantLock = new ReentrantLock(true);

Synchronized而言,是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

以ReentrantLock为例,竞争锁和释放锁的实现

ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.unlock();

--FairSync
//公平锁中竞争锁
final void lock() {
	//都按照FIFO来竞争锁
    acquire(1);
}

--FairSync
//非公平锁中竞争锁
final void lock() {
·	//任何线程都可以先通过CAS去抢占锁
    if (compareAndSetState(0, 1))
    	setExclusiveOwnerThread(Thread.currentThread());
    else
    	//抢占失败,按照FIFO来竞争锁
    	acquire(1);
}

--AbstractQueuedSynchronizer
//释放锁就一种实现
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
        	unparkSuccessor(h);
        return true;
    }
    return false;
}

可重入机制

synchronized可重入,每个实例对象内存都有一个对象头的区域,存放锁的信息(拿到锁的线程),以此来判断是否可重入。以下面连个方法为例,synchronized作用于myClass实例对象,所以访问method2时可重入。

class MyClass {
    public synchronized void method1() {
        method2();
    }
      
    public synchronized void method2() {
          
    }
}

AQS的Lock锁也是可重入的,synchronized自动获取锁和释放锁,而Lock机制就是手动操作

以ReentrantLock为例state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其他线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),获取多少次就要释放多少次才能保证state是能回到零态的。

AQS的应用

ReentrantLock独享锁和ReentrantReadWriteLock读写锁,Semaphore和CountDownLatch并发工具

Lock(锁)的实现:

void lock();//线程去竞争锁,拿不到锁就堵塞
void lockInterruptibly();//也是竞争锁会阻塞,但是可中断
boolean tryLock();//非阻塞去竞争锁,拿不到就返回false
boolean tryLock(long time, TimeUnit unit)//尝试获取锁,如果超时还没拿到就返回false
void unlock();//释放锁

ReentrantLock的使用场景

ReentrantLock常用的api:

//查看有多少线程等待锁
int getQueueLength()
//是否有线程等待抢锁
boolean hasQueuedThreads()
//是否有指定线程等待抢锁
boolean hasQueuedThread(Thread thread)
//当前线程是否抢到锁。返回0代表没有
int getHoldCount()
//查询此锁是否由任何线程持有
boolean isLocked()
//是否为公平锁
boolean isFair() 

简单应用:类似于synchronized,线程会堵塞,直到拿到锁

//设置为true时是公平锁,thread0->thread10按顺序拿到锁
//默认非公平锁时则每一个线程都会试图去拿锁
static ReentrantLock reentrantLock = new ReentrantLock(true);

private static void map()  {
    System.out.println("map->"+Thread.currentThread().getName());
    reentrantLock.lock();
    try {
        System.out.println("lock->"+Thread.currentThread().getName());
        TimeUnit.SECONDS.sleep(2);
    }catch (Exception e) {
        e.printStackTrace();
    }finally {
        reentrantLock.unlock();
    }
}

public static void main(String[] args) {
    for (int i = 0; i <10; i++) {
        new Thread(()->{
            map();
        }).start();
    }
}

防止等待过长造成线程过多从而会导致内存溢出,需要设置一个等待超时时间

static ReentrantLock reentrantLock = new ReentrantLock(true);

private static void map()  {
    System.out.println("map->"+Thread.currentThread().getName());
    try {
        //队列线程等待10s之后没有拿到锁就中断队列
        if (reentrantLock.tryLock(10,TimeUnit.SECONDS)) {
            System.out.println("lock->"+Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(2);
        }
    }catch (Exception e) {
        
    }finally {
        //返回非0就表示当前线程拿到锁,所以需要释放
        if (reentrantLock.getHoldCount()>0) {
            reentrantLock.unlock();
            System.out.println("unlock->"+Thread.currentThread().getName());
        }else {
            //没有拿到锁的自然就是等待超时的线程
            System.out.println("超时->"+Thread.currentThread().getName());
        }
    }
}

public static void main(String[] args) {
    for (int i = 0; i <20; i++) {
        new Thread(()->{
            try {
                map();
            }catch (IllegalMonitorStateException e) {
                //如果没有拿到锁而去释放锁就会抛这个异常
                System.out.println("IllegalMonitorStateException->"+Thread.currentThread().getName());
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
    }
}

ReentrantReadWriteLock

ReentrantLock的升级版,提供了读锁和写锁,读锁为共享锁可以并发同时调用,写锁和ReentrantLock没区别,简单来说,涉及到数据更改的情况就会互斥

private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
private static ReadLock readLock = lock.readLock();
//写锁和ReentrantLock的应用几乎一致
private static WriteLock writeLock = lock.writeLock();

private static void map() throws InterruptedException {
    try {
        readLock.lock();
        System.out.println("抢占读锁->"+Thread.currentThread().getName());
        TimeUnit.SECONDS.sleep(1);
    }finally {
        readLock.unlock();
        System.out.println("释放读锁->"+Thread.currentThread().getName());
    }
    TimeUnit.SECONDS.sleep(2);
    System.out.println("##############");
    try {
        writeLock.lock();
        System.out.println("抢占写锁->"+Thread.currentThread().getName());
        TimeUnit.SECONDS.sleep(1);
    }finally {
        writeLock.unlock();
        System.out.println("释放写锁->"+Thread.currentThread().getName());
    }
}

public static void main(String[] args) throws Exception {
    for (int i = 0; i <3; i++) {
        new Thread(()->{
            try {
                map();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

输出的结果一目了然

抢占读锁->Thread-0
抢占读锁->Thread-1
抢占读锁->Thread-2
释放读锁->Thread-1
释放读锁->Thread-0
释放读锁->Thread-2
##############
##############
##############
抢占写锁->Thread-0
释放写锁->Thread-0
抢占写锁->Thread-2
释放写锁->Thread-2
抢占写锁->Thread-1
释放写锁->Thread-1

CountDownLatch

线程计数器,设置计数器的初始值,当执行完countDown计数器的值就-1,当计数器的值为0时,在闭锁上等待的线程就可以恢复工作了

//参数count为计数值
CountDownLatch(int count)
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
void await();   
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
boolean await(long timeout, TimeUnit unit);  
//将count值减1
void countDown();  

主线程等待子线程执行完设置的CountDownLatch值才会继续往下执行

final CountDownLatch countDownLatch = new CountDownLatch(3);
for (int i = 0; i <100; i++) {
    new Thread(()->{
        System.out.println(Thread.currentThread().getName());
        countDownLatch.countDown();
        System.out.println(countDownLatch.getCount());
    }).start();
}

System.out.println(Thread.currentThread().getName());
try {
    countDownLatch.await();
    //执行完3次countDownLatch.countDown()才会往下
    System.out.println("开始");
}catch (InterruptedException e) {
    e.printStackTrace();
}

分成三个阶段,第一阶段所有线程准备好,当准备好之后,主线程裁判宣布开始,子线程开始出发,当所有子线程跑完,宣布结束!

public static void main(String[] args) throws Exception {
    final CountDownLatch  judger = new CountDownLatch(1);
    final CountDownLatch player = new CountDownLatch(4);
    final CountDownLatch ready = new CountDownLatch(4);
    for (int i = 0; i <4; i++) {
        new Thread(()->{
            System.out.println("选手" + Thread.currentThread().getName() + "准备好");
            ready.countDown();
            try {
                judger.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("选手" + Thread.currentThread().getName() + "出发!");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("选手" + Thread.currentThread().getName() + "到达终点");
            player.countDown();
        }).start();
        TimeUnit.SECONDS.sleep(2);
    }

    try {
        ready.await();
        System.out.println("运动员已经准备好"+Thread.currentThread().getName()+"裁判即将发布口令");
        judger.countDown();
        System.out.println("裁判"+Thread.currentThread().getName()+"已发送口令,正在等待所有选手到达终点");
        player.await();
        System.out.println("所有选手都到达终点");
    }catch (InterruptedException e) {
        e.printStackTrace();
    }

}

Semaphore控制并发数

Semaphore信号量,Semaphore将AQS的同步状态用于保存当前可用许可的数量
类似于ReentrantLock和CountDownLatch的结合,控制每次获取锁的线程数

//设置只允许同时有三个线程可以拿到锁
Semaphore semaphore = new Semaphore(3, true);

第一个参数permit初始化可以同时并发的线程,每次acquire之后,status减1,减到0时所有线程阻塞,
直到release释放锁status加1

    //初始化只允许3个线程同时拿到锁,设置公平锁
    final Semaphore semaphore = new Semaphore(3, true);
    //自定义阻塞队列线程池
    for (int i = 0; i < 10; i++) {
      TimeUnit.SECONDS.sleep(1);

      new Thread(()->{
        System.out.println(Thread.currentThread().getName()+":进入队列");
        try {
          //线程开始获取锁,拿到锁之后status减1,当status减到0时其他队列进入AQS排队
          semaphore.acquire();
          System.out.println(Thread.currentThread().getName()+":获取了锁");
          TimeUnit.SECONDS.sleep(5);
          //释放锁,status加1
          semaphore.release();
          System.out.println(Thread.currentThread().getName()+":释放了锁");
        } catch (InterruptedException e) {
          e.printStackTrace();
        }finally {
          System.out.println(Thread.currentThread().getName()+":此时AQS队列个数:"+semaphore.getQueueLength());
        }
      }).start();
    }

分析返回结果,前三个队列顺利拿到锁,Thread-3和4进入队列排队,Thread-0释放了锁,Thread-3获取了锁,此时队列只剩T4;

Thread-0:进入队列
Thread-0:获取了锁
Thread-1:进入队列
Thread-1:获取了锁
Thread-2:进入队列
Thread-2:获取了锁
Thread-3:进入队列
Thread-4:进入队列
Thread-0:释放了锁
Thread-3:获取了锁
Thread-0:此时AQS队列个数:1
Thread-5:进入队列
Thread-1:释放了锁
Thread-4:获取了锁
Thread-1:此时AQS队列个数:1
Thread-6:进入队列
Thread-2:释放了锁
Thread-2:此时AQS队列个数:1
Thread-5:获取了锁
Thread-7:进入队列
Thread-8:进入队列
Thread-9:进入队列
Thread-3:释放了锁
Thread-3:此时AQS队列个数:3
Thread-6:获取了锁
Thread-4:释放了锁
Thread-4:此时AQS队列个数:2
Thread-7:获取了锁
.......

通过源码分析AQS

主要还是通过ReetranLock,CountDownLatch,Semaphore,ReentranReadWriteLock常用api进行源码分析

ReentranLock源码

获取锁acquire

公平锁/非公平锁实例获取锁

--NonfairSync
final void lock() {
    //非公平锁先通过CAS去竞争锁
    if (compareAndSetState(0, 1))
        //当非公平锁竞争成功之后该线程占用了独占锁
        //相对Node节点来说,并没有改变原有的AQS队列,只是更换了head节点的持有线程
        //简单来说就是两个男人决斗,赢了别人就取代了他的房子和女人
        setExclusiveOwnerThread(Thread.currentThread());
    else
    	//公平锁则按照队列顺序进行竞争锁
        acquire(1);
}

--AbstractQueuedSynchronizer
protected final boolean compareAndSetState(int expect, int update) {
    //期望值status为0,也就是锁没有被持有则将Status更改为1,获取锁成功返回true
	return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
--AbstractQueuedSynchronizer
//初始化时就加载获取了偏移量
static {
    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"));
}

acquire获取锁

public final void acquire(int arg) {
    //tryAcquire可以理解为:尝试直接获取独占锁
    if (!tryAcquire(arg) &&
        //tryAcquire失败之后将当前线程封装Node添加到AQS队列尾部,并继续循环获取锁
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire非公平锁的实现:

--Sync
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //获取status值,如果为0则表示仍然可以再CAS
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
        	//设置当前持有锁的线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //status不为0,继续判断获取锁的线程是否为当前线程
    else if (current == getExclusiveOwnerThread()) {
    	//如果是则status加1,增加重入次数
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //如果都不是则返回false
    return false;
}

tryAcquire公平锁的实现:

--FairSync
protected final boolean tryAcquire(int acquires) {
	...
    if (c == 0) {
    	//判断当前Node有没有前驱节点,有则需要继续进行CAS
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }	...
}

返回false,当前线程才能去CAS,目前理解有队列三种情况会返回false:1.当线程第一次尝试获取锁时,tail和head节点都是null,所以直接返回false去获取锁;2.只有一个线程在队列排队,此时status为0返回false; 3.有多个线程在队列中,第一个head节点已经释放了锁,等待next节点去获取锁,所以只需判断next节点是不是当前线程的节点

--AbstractQueuedSynchronizer
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    //如果head和tail都是null,那么也是返回false,就不存在h.next空指针
    return h != t &&
    //h != t成立,锁已经释放,等待后继节点去获取,判断是否为当前线程
    ((s = h.next) == null || s.thread != Thread.currentThread());
}

当第一个A线程tryAcquire直接拿到锁,队列没有阻塞的线程;第二个线程B也去tryAcquire,如果status为0则和前面一样,不为0则添加到队列的中去,因为队列初始化的head和tail节点都是空节点,线程进行自旋等待线程A释放,当线程A释放之后,通过自旋线程B第二次tryAcquire,因为status释放之后为0,则进行hasQueuedPredecessors从而获取了锁;第三个线程C在tryAcquire时,如果status为0则表示线程B已经释放了,线程C也同上步骤拿到了锁,并且此时的head节点还是线程B,除非线程B没有释放,线程C也加入到AQS队列中,此时队列长度为2;如果之后线程D,E,F等等依次进行tryAcquire,只要B没有释放,那么都按顺序加入到队列中;直到线程B释放之后,线程C通过自旋判定出自己排在head的next之后再tryAcquire发现status=0从而获得锁,设置线程C的节点为Head并清除线程B的节点内存,依次类推!

深入理解CAS和AQS_第4张图片

尝试获取锁失败后将失败的线程封装之后添加到AQS队列中

封装Node节点:添加节点并处理队列结构,线程第一次添加队列时首先会初始化一个new Node()节点作为head和tail,然后线程节点取代这个初始化节点作为tail节点

private Node addWaiter(Node mode) {
    //this.nextWaiter = mode; 暂且把nextWaiter理解为锁的模式为EXCLUSIVE共享锁
    Node node = new Node(Thread.currentThread(), mode);
   	//将当前节点的前驱节点设置为原tail节点
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            //将原tail节点的后继设置为当前节点
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        //tail节点为null,队列是空的
        if (t == null) { // Must initialize
            //初始化一个队列设置为head和tail节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //当循环之后,tail不为空,已经初始化了
            node.prev = t;
            //设置当前节点为tail节点
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

acquireQueued抢锁:tryAcquire尝试获取锁失败之后,设置前驱节点为SIGNAL状态表示前驱节点释放锁的时候会唤醒后继节点,设置成功之后会将当前线程park,类似于wait,等待unpark唤醒

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            //先去获取当前节点的prev(前驱)节点
            final Node p = node.predecessor();
            //前驱节点是head则再次尝试获取锁
            if (p == head && tryAcquire(arg)) {
                //获取锁之后设置head为当前node并清空node的线程信息和prev
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //获取锁失败判断是否需要挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                //前驱节点状态为SIGNAL,当前线程park阻塞
                //LockSupport.park/unpark为线程提供一个类似开关的功能
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            //取消获取锁,主要就是设置节点waitStatus为cancelled,清空节点内容
            //最主要的是unpark为下一个线程放行
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire,设置前驱节点的状态为SIGNAL表示当前线程被阻塞或者即将被阻塞,那么前驱线程释放锁或者取消后需要唤醒后继线程

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //前驱节点状态waitStatus默认为0(int 默认值)
    int ws = pred.waitStatus;
    //SIGNAL值为-1表示后继节点被阻塞需要唤醒
    if (ws == Node.SIGNAL)
        return true;
    //大于0的状态只有一个cancelled取消
    if (ws > 0) {
        do {
            //取消就是将前驱节点从队列中删除,所以当前节点的prev就直接指向了上上一个节点
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        //作为原前驱节点的前驱节点,它的next就指向了当前节点
        pred.next = node;
    } else {
        //剩下就只有0和PROPAGATE(可以表示共享模式则设置前驱节点为SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    //前驱节点不为SIGNAL则继续去循环获取锁
    return false;
}

LockSupport:针对单个线程的拦截和放行,阻塞维度是线程,和wait类似

Thread thread = new Thread(() -> {
    System.out.println(LocalTime.now()+":start park:" + Thread.currentThread().getName());
    LockSupport.park();
    System.out.println(LocalTime.now()+":park1:" + Thread.currentThread().getName());
    LockSupport.park();//只有一个通行证,所以会阻塞
    System.out.println(LocalTime.now()+":park2:" + Thread.currentThread().getName());
});
thread.start();

Thread thread2 = new Thread(() -> {
    System.out.println(LocalTime.now()+":start park:" + Thread.currentThread().getName());
    LockSupport.park();
    System.out.println(LocalTime.now()+":park1:" + Thread.currentThread().getName());
    LockSupport.park();//只有一个通行证,所以会阻塞
    System.out.println(LocalTime.now()+":park2:" + Thread.currentThread().getName());
});
thread2.start();

TimeUnit.SECONDS.sleep(5);
LockSupport.unpark(thread);
LockSupport.unpark(thread2);
TimeUnit.SECONDS.sleep(5);
LockSupport.unpark(thread);

21:05:05.766:start park:Thread-0
21:05:05.766:start park:Thread-1
21:05:10.749:park1:Thread-0
21:05:10.749:park1:Thread-1
21:05:15.749:park2:Thread-0

lockInterruptibly可中断加锁

lock方法会忽略中断请求,继续获取锁直到成功,而lockInterruptibly则直接抛出中断异常来立即响应中断,
一旦检测到中断请求,被中断线程立即返回不再参与锁的竞争并且取消锁获取操作(interrupt()操作,虽然只是恢复了中断状态为true,但是也会立即退出队列)

ReentrantLock lock = new ReentrantLock();
new Thread(()->{
    try {
        lock.lockInterruptibly();
        System.out.println(Thread.currentThread().getName()+":抢到锁");
        TimeUnit.SECONDS.sleep(3);
        lock.unlock();
        System.out.println(Thread.currentThread().getName()+":释放锁");
    } catch (InterruptedException e) {}
}).start();

Thread thread = new Thread(() -> {
    try {
        System.out.println(Thread.currentThread().getName() + ":进入队列");
        lock.lockInterruptibly();
        System.out.println(Thread.currentThread().getName() + ":抢到锁");
        lock.unlock();
    } catch (InterruptedException e) {
        System.out.println(Thread.currentThread().getName() + ":没有抢到锁");
    }
});
TimeUnit.SECONDS.sleep(2);
thread.start();
//interrupt即使只是一个中断信号,但是会立即抛出异常
thread.interrupt();

释放锁release

tryRelease先设置status减1,如果status==0则认为释放锁

public final boolean release(int arg) {
    //释放锁才返回true,如果只是重入锁减一则返回false
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //释放当前Node之后unpark后继节点的线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
    //先减一
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //如果status==0则表示释放锁,设置锁的持有线程为null并返回true
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

当前节点释放锁成功之后,后继线程还处于park,需要先unpark后继节点的线程

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
    //waitStatus>0只有cancelled状态
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        //s.thread后继线程unpark
        LockSupport.unpark(s.thread);
}

Condition条件队列

或者叫信号队列,await会将当前线程park(也就是释放了通行证锁)并添加到condition条件队列中,释放锁之后其他线程开始抢锁,知道被signalAll,将条件队列的线程唤醒并重新加入到AQS队列中竞争锁

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

new Thread(()->{
    lock.lock();
    System.out.println(Thread.currentThread().getName()+":抢到锁");
    try {
        TimeUnit.SECONDS.sleep(2);
        //①释放锁,并进入睡眠等待唤醒
        condition.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //⑤抢到锁继续跑完任务
    System.out.println("重新抢到了锁:"+Thread.currentThread().getName());
    lock.unlock();
},"await").start();

new Thread(()->{
    try {
        //如果signal线程先抢到锁则会造成await线程无法被唤醒
        TimeUnit.SECONDS.sleep(2);
        //②await线程释放锁,该线程抢到锁
        lock.lock();
        System.out.println(Thread.currentThread().getName()+":抢到锁");
        TimeUnit.SECONDS.sleep(2);
        //③await已经休眠,condition唤醒所有休眠线程,回到AQS队列等待继续获取锁
        condition.signalAll();
        TimeUnit.SECONDS.sleep(2);
        System.out.println("继续完成任务:"+Thread.currentThread().getName());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //④释放锁
    lock.unlock();
},"signal").start();

ReentrantReadWriteLock源码

AQS队列中的节点其实是独占和共享节点并存的,共享锁是允许多个线程持有;持有写锁的线程可以继续获取读锁,反之不行。

static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();

readLock.lock();
writeLock.lock();

tryAcquireShared尝试获取共享锁

独占模式和共享模式对于 state 的操作完全不一样,在共享模式下,每个线程都可以对 state 进行加减操作

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    //独享锁:status为0则exclusiveCount(c)为0
    //表示有独占锁且不是当前线程持有(当前线程持有写锁也可以继续获取读锁)
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        //表示获取失败
        return -1;
    //读锁被获取的次数
    int r = sharedCount(c);
    //如果是公平锁则会hasQueuedPredecessors判断是否有前驱节点,也就是有人排队,否则返回false
    //如果非公平锁则会判断head的后继节点是否为共享模式,如果是共享则返回false
    if (!readerShouldBlock() &&
        //MAX_COUNT是低16位的最大值
        r < MAX_COUNT &&
        //SHARED_UNIT二进制的第17位,也就是共享锁加1,此时如果有写锁改变了c值也会导致写锁失败
        compareAndSetState(c, c + SHARED_UNIT)) {
        //次数为0表示读锁还没有获取过
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            //重入锁机制
            firstReaderHoldCount++;
        } else {
            //HoldCounter封装线程id和一个count值,记录当前线程持有的读锁数量
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                //cachedHoldCounter最后一个获取读锁的线程
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    //获取锁失败进行自旋
    //大致就是重新获取status值,再判断是否有写锁,有则返回-1,没有则再CAS去获取写锁
    return fullTryAcquireShared(current);
}

位运算:<<表示左移,>>>无符号右移(当为正数时和>>没区别)

static final int SHARED_SHIFT   = 16;
//1 0000 0000 0000 0000
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
//1111 1111 1111 1111
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
//右移16位,表明共享锁值在16位以上
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
//&表示两个位都为1时,结果才为1,表明独享锁在16位以下
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

status通过高16位和低16位可以区分读锁和写锁

private void doAcquireShared(int arg) {
    //获取锁失败就加入到AQS队列中,这是个共享模式
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            //判断前驱节点是否是head
            if (p == head) {
                //获取共享锁成功,唤醒后继节点进行自旋
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    //获取锁成功,设置当前节点为head,同时唤醒后继节点
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            //获取锁失败,设置前驱节点为SIGNAL,并park阻塞自己
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

和独享锁不同,因为独享锁只允许一个线程访问,所以在释放锁的时候再去唤醒即可

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; 
    setHead(node);

    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            //如果后继节点是共享模式则唤醒
            doReleaseShared();
    }
}

对于共享锁的释放,由于不阻塞其他线程获取读锁,所以释放锁主要是唤醒写锁的线程;也证明了读锁会阻塞写锁!

public final boolean releaseShared(int arg) {
    //只有当共享锁和读写锁都释放之后才会返回true去唤醒后继节点
    if (tryReleaseShared(arg)) {
        //又由于后继是共享模式时不会在release时被唤醒,所以主要是用于唤醒后继节点是写锁的线程
        doReleaseShared();
        return true;
    }
    return false;
}

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    //也就是说当前线程是唯一个持有读锁的线程
    if (firstReader == current) {
        //重入机制
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        //cachedHoldCounter 是否缓存的是当前线程,不是的话要到 ThreadLocal 中取、
        //这也是cachedHoldCounter的作用,用于缓存
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        //啰嗦,如果只有1说明没有重入了,直接释放了锁,所以当前readHolds移出ThreadLocal
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        //大于1表示有重入,则减一即可
        --rh.count;
    }
    for (;;) {
        int c = getState();
        //SHARED_UNIT是17位为1,也就是共享数为1
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            //期望nextc值为0,只可能是没有任何线程持有锁才会返回true
            return nextc == 0;
    }
}

你可能感兴趣的:(并发编程,多线程,并发编程,java,juc)