AQS的理解

在介绍AQS前我先介绍下,关于可重入锁,公平锁非公平锁的相关知识,可以帮助更好的理解AQS。

可重入锁

  • 是指同一个线程在外层方法获取锁的时候,在内层仍然可以使用,并且不会发生死锁(前提: 锁对象是同一个锁).

  • 不会因为之前已经获取锁还没有释放而阻塞,java中ReentrantLock和syncronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁

    总结: 一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入,可以获取自己的内部锁

可重入锁的种类

  • 隐式锁:关键字syncronized使用的锁,默认是可重入锁 (同步代码块,同步方法)
  • 显式锁: 即(Lock)人工手动获取锁和释放锁

举例说明

1.Syncronized重入的实现机制

  • 任意一把Syncronized锁对象,使用java -c xx.class 编译以后, 会产生两个指令monitorEnter和monitorExit锁的获取和锁的释放,多出来的monitorExit是程序异常的时候,可以正常的释放锁。
  • 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程指针当执行moniterenter时,如果目标锁对象的计数器为零,那么说明他没有被其他线程持有,java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1,在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么java虚拟机计数器可以加1否则需要等待,直至持有线程释放锁。当执行monitorexit时,java虚拟机则将锁对象的计数器减1,计数器为0说明锁已经释放
  1. ReentrantLock可重入锁
  • 实现加锁次数和释放锁次数不一致由于加锁次数和释放锁次数不一致,第二个线程始终无法获取到锁,导致一直等待正常情况下,加锁几次就要解锁几次.
  • ReentrantLock锁少释放的问题: 导致其他线程获取不到锁,程序会一直阻塞

LockSupprot是什么?

  • 他是阻塞原语,想必应该知道,线程等待唤醒机制(LuckSupport比wiat和notify更牛逼
  • LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程
  • 每个线程都有一个许可,Permit只有两个值1和0,默认是零
  • 可以把他许可看成一种(0,1)信号量(Semaphore),但与Semphore不同的是,许可的累加上上限是1

补充: 线程等待唤醒的方法

  • 方法1: 使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
  • 方法2:使用JUC包中Condition的awati()方法让线程等待,使用signal唤醒线程
  • 方法3: 使用LockSupport类可以阻塞线程以及唤醒执行被阻塞的线程

使用 LockSupprot的原因

SyncronizedLock锁等待唤醒通知的不足:

  1. wait和notify方法,两个都去掉同步代码块后看运行情况
    (Lock中如果没有lock.lock()以及locl.unlock()方法也会报IlleagalMonitorStateException)
   1. Exception in thread t1 java.lang.IlleagalMonitorStateException at java.lang.Object wait(Native Method)
   2. Exception in thread t1 java.lang.IlleagalMonitorStateException at java.lang.Object notify(Native Method)
  • Object类中的wait,notify,notifyAll用于线程等待和唤醒的方法,都必须要在Syncronized内部执行(需要携带关键字syncronized)
  1. 将notify放在wait方法前面先执行,t1先notify了,3秒后t2线程在执行wait方法
  • 程序一直无法结束
  • 先wait后notify,notifyAll方法,等待中的线程才会唤醒,否则无法唤醒
  • wait和notify方法必须在同步块或者方法里面成对出现使用
    先wait后notify才ok
    结论: 传统的Syncornized和Lock实现等待唤醒通知的约束
    线程先要获得持有锁,必须在锁块(syncronized或lock)中
    必须先等待后唤醒,线程才能够被唤醒

LockSupprot的使用:

LockSupprot的park方法:

 public static void park(){
     
 //permit默认是0,所以一开始调用park方法,当前线程就会阻塞,
 //直到别的线程将当前线程的permit设置为1时,park方法会被唤醒
 //然后会将permit再次设置为0并返回
    UNSAFE.park(false,0L);
}

LockSupprot的unpark方法:

 public static void unpark(Thread thread){
     
 //调用unpark()方法后,就会将thread线程的许可证permit设置成1.
 //注意多次调用unpark方法不会累加permit值还是1,会自动唤醒thread线程
 //即之前阻塞中的LockSupport()方法会立即返回
    UNSAFE.unpark(thread);
}

LockSupport的优势:

  • 正常+无锁块要求
  • 之前错误的先唤醒后等待,LockSupport照样支持 sleep方法3秒后醒来,执行park无效,没有阻塞效果, 先执行了unpark(t1)导致park()方法形同虚设,时间一样

LockSupport原理:

LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程

  • 在任意位置阻塞 阻塞之后也有对应的唤醒方法。归根结底,LockSupprot调用的Unsafe中的native代码。
  • LockSupprot和每个使用它的线程都有一个许可(permit)关联,permit相当于1,0的开关,默认是0;
  • 调用一次unpark就加1变成1, 调用一次park就会消费permit,也就是将1变成0,同时park立即返回
  • 如再次调用park会变成阻塞(因为permit为零会阻塞在这里,一直到permit变成1),这是调用unpark会变成1
  • 每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证

形象理解:

  • 线程阻塞需要消耗凭证(permit),这个凭证最多只有一个 当调用park方法时 如果有凭证,则会直接消耗这个凭证然后正常退出
  • 如果无凭证,就必须阻塞等待凭证可用 而unpark则相反,他会增加一个凭证,但凭证最多只能有一个,累加无效

常见的问题

为什么可以先唤醒线程后阻塞线程?

  • 因为unpark获得了一个凭证,之后调用park方法,就可以名正言顺的凭证消费,故不会阻塞

为什么唤醒两次后阻塞两次,但最终结果还是会阻塞线程?

  • 因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证。 调用两次park却需要消费两个凭证,证不够,不能放行

公平锁和非公平锁:

公平锁:

  • 是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,有个先来后到的顺序

非公平锁:

  • 多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请线程先获取锁。在高并发情况下,有可能造成优先级反转或饥饿现象
    syncronized是一种非公平锁

公平锁/非公平锁作用:

  • 并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁和非公平锁,默认是非公平锁 公平锁:
  • 在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占用锁,否则就加入等待队列中,以后就按照先进先出的规则从队列中取到自己

自旋锁:

  • 是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少上下文切换 缺点时循环会消耗cpu
//手写实现一个自旋锁  自旋锁好处: 循环比较获取知道成功为止,没有类似wait的阻塞
public AtomicReference<Thread> atomicReference= new AtomicReference<>();

public void myLock(){
     
    Thread thread = Thread.currentThread();
    System.out.println(Thread.currentThread().getName()+"\t come in ");
    while (!atomicReference.compareAndSet(null, thread)){
     

    }
}

public void unLock(){
     
    Thread thread = Thread.currentThread();
    System.out.println(Thread.currentThread().getName()+"\t come in ");
    atomicReference.compareAndSet(thread,null);
}

public static void main(String[] args) {
     
    SimponLockDemo simponLockDemo = new SimponLockDemo();

    new Thread(()->{
     
        simponLockDemo.myLock();
        try {
      TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {
      e.printStackTrace();}
        simponLockDemo.unLock();
    }, "a").start();

 try {
      TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {
      e.printStackTrace();}

    new Thread(()->{
     
        simponLockDemo.myLock();
        simponLockDemo.unLock();
    }, "b").start();

}

独占锁和共享锁:

独占锁

  • 指该锁一次只能被一个线程占有,对ReentratLock和Syncronized而言都是独占锁

共享锁

  • 指该锁可被多个线程所持有 对ReentrantReadWriterLock其读锁时共享锁,其写时独占锁
  • 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的

Syncronized和Lock有什么区别

实现方式不同

  • syncronized关键字属于JVM层面,当线程进入有syncronized修饰的方法时,底层会通过monitor对象来完成,其实wait和notify等方法也依赖于monitor对象只有在同步块和方法中才能调用wait和notify等方法)
  • Lock是具体的java类,底层实现是通过API调用完成锁的功能

使用上区别

关于释放锁的区别

  • syncronized不需要用户手动释放锁,当syncronized代码执行完后系统会自动让线程释放锁的占用
  • ReentrantLock则需要用户手动去释放锁若没有主动释放锁,就有可能导致出现死锁的线程
  • 需要lock和unlock方法配置try/finally语句块来完成

等待是否可中断区别

  • syncronized 不可中断,除非抛出异常或者正常运行完成
  • ReentrantLock 可中断
  • ReentrantLock中断的方式
  • 设置超时时间
    tryLock(long timeout TimeUnit unit)
    • lockInterruptibly()放代码块中调用,调用interrupt方法可中断

加锁是否公平区别

  • syncronized非公平锁
  • ReetrantLock两者都可以,默认非公平锁,构造方法可以传入布尔值,true为公平锁,false为非公平锁

锁绑定多个条件区别

  • syncronized没有
  • ReetrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像Syncronized随机唤醒一个或者全部唤醒

AQS

  • 抽象的队列同步器, 用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态

解释下AQS翻译后的意思

  • 抽象: 符合模板设计模式,作为核心父类给子类继承
  • 队列: 对抢占不到锁的管理
  • 同步器: 队列的排队的线程进行管理

AQS作用

加锁会导致阻塞,有阻塞就会排队,实现排队必然需要有某种形式的队列来进行管理

AQS的原理

AQS为实现阻塞锁,依赖先进先出的一个等待依靠一个原子int值来表示状态,通过占用和释放方法,改变状态值AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来表示完成获取资源的排队工作
将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值得修改

AQS的变量:

  1. state变量: 判断是否阻塞
    阻塞需要排队(前提:自旋如果达到一定时间),实现排队必须需要队列
  2. Node节点的变量
    队列中每个队列的个体也就是Node

你可能感兴趣的:(Java并发)