AQS基础与volatile

567 阅读8分钟

并发编程归纳总结

  • AQS:AbstractQueueSynchronized

    • 这个可以扩展出许多的并发类

      image-20220224165822250

    • 举例:FurtureTask,Synchronized也是根据其原理进行实现的

    • 源码分析:

      • state:同步状态,记录当前线程对锁的持有情况(解决锁的可重入)
    • 使用场景:自定义并发同步工具类(搞一个内部类去继承AQS)

    • AQS设计模式:模板方法设计模式

      • 模板方法:在抽象类中定义好模板(任务有一定顺序),具体任务类A继承抽象类,执行具体业务逻辑

        1. 抽象类:

           package cn.enjoyedu.concurrent.theory.aqs.templatepattern;
           ​
           /**
            * 类说明:抽象蛋糕模型
            */
           public abstract class AbstractCake {
               protected abstract void shape();//蛋糕造型
               protected abstract void apply();//蛋糕涂抹
               protected abstract void brake();//烤蛋糕
           ​
               /*模板方法*/
               public final void run(){
                   this.shape();
                   this.apply();
                   this.brake();
               }
           }
          
        2. 具体任务类:继承抽象类,重写方法(具体逻辑)

           /**
            * 类说明:芝士蛋糕
            */
           public class CheeseCake  extends AbstractCake {
           ​
               @Override
               protected void shape() {
                   System.out.println("芝士蛋糕造型");
               }
           ​
               @Override
               protected void apply() {
                   System.out.println("芝士蛋糕涂抹");
               }
           ​
               @Override
               protected void brake() {
                   System.out.println("芝士蛋糕烘焙");
               }
           }
          
        3. 使用具体类实例执行任务

           /**
            * 类说明:生产蛋糕
            */
           public class MakeCake {
               public static void main(String[] args) {
                   AbstractCake cake = new CheeseCake();
                   cake.run();
               }
           }
          
        4. 模板模式在Android中的体现:View.Draw() 需要去重写onDraw(takeView)与dispatch(ServiceView),实现自己的具体方法,

    • 自定义并发工具类创建:需要继承AQS(已经定义好了模板方法),并且重写里面的方法

      • 独占锁:实现AQS 中的独占的方法:tryAcquire

         protected boolean tryAcquire(int arg) {
             //空实现,需要我们实现这个方法
             throw new UnsupportedOperationException();
         }
        
      • 实现一个共享并发工具类:需要重写tryAcquireShared

         protected int tryAcquireShared(int arg) {
             throw new UnsupportedOperationException();
         }
        
    • 独占锁:线程拿到同步代码块的锁后,才能执行里面的业务

      • 假如说原state是0,拿到锁后变成1,就证明拿到锁了
    • 自定义显示独占锁:所有的显示锁均继承Lock接口

      1. 实现Lock接口(需要重写里面的一些方法:lock,unlock)

      2. 定义静态(1.工具类,2.方便调用)内部类继承AQS

        1. 判断锁的占用状态:

           /*判断处于占用状态*/
           @Override
           protected boolean isHeldExclusively() {
               return getState()==1;
           }
          
        1. 获得锁,需要实现一个独占的方法(进行CAS操作(尝试修改state属性),CAS成功登记锁,返回true;如果不成功,那么就返回false(证明其他类拿到这把锁))

           /*获得锁,需要实现一个独占的方法*/
           @Override
           protected boolean tryAcquire(int arg) {
               //里面是有一步CAS操作的,当CAS比较成功(其他的线程拿到了锁,CAS),将这个锁的内部登记一下,只是试了一次
               if(compareAndSetState(0,1)){
                   //这里是不支持锁的可重入的,一个线程只能拿对一把锁拿一次
                   
                   //下面那个是排他的所有线程,就是说谁拿到这把锁,现在这把锁被我拿到了
                   setExclusiveOwnerThread(Thread.currentThread());
                   return true;
               }
               //其他的线程拿到了锁,CAS
               return false;
           }
          
        2. 释放锁(这个时候,同步工具类已经拿到了这个锁):修改state属性

           /*释放锁,在同步工具类中拿到(修改了state变量)了这个锁,现在就是对齐释放*/
           @Override
           protected boolean tryRelease(int arg) {
               //将state从1改成0
               if(getState()==0){
                   throw new IllegalMonitorStateException();
               }
               setExclusiveOwnerThread(null);
               //将state从1改成0
               setState(0);
               return true;
           }
          
        3.  // 返回一个Condition,每个condition都包含了一个condition队列
           Condition newCondition() {
               return new ConditionObject();
           }//此时静态内部类完成
          
        4. 实现lock与onLock:调用这个acquire方法

           public void lock() {
              System.out.println(Thread.currentThread().getName()+" ready get lock");
               sync.acquire(1);
               System.out.println(Thread.currentThread().getName()+" already got lock");
           }
          
          • 细节:在静态内部类中调用的是tryAcuire(),在外部类中调用的是acquire方法:因为AQS内部的模板方法acquire内调用了tryAcquire,后者就是我们需要去实现的(具体业务逻辑)

            对比两个方法

             public final void acquire(int arg) {
                 if (!tryAcquire(arg) &&
                     acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                     selfInterrupt();
             }
            
  • AQS基本思想CLH队列锁

    • 概述:

      • 任意时刻只能有一个线程拿到锁,没有拿到锁的线程排成一个队列(打包成QNode)

      • CLH队列锁基本思想:

        • QNode:没有拿到锁的线程组成的队列

          • 组成部分

            • 当前线程
            • myPred:指向前驱结点,线程形成链表(单向的链表形成队列)
            • locked:当前是否需要锁
        • 工作表现:当线程A想要获得锁

          1. 使用CAS将自己所在的QNode挂载CLH队列尾部
          2. 将所在QNode中的locked变量设置为true;
          3. 将所在QNode的myPred执行原来队列的尾节点
          4. 当线程B想要获得锁,还是这种操作
        • 线程A怎么去拿到锁:对所在节点内部的myPred自旋(查看线程A前面那个QNode中的locked是否为false--->表示将锁释放掉了)

        • 对线程B,就是去对A操作

        • 但是不是这样的:

          • AQS里面是双向链表
          • 自旋超过一定次序,那么就让当前线程阻塞
        • 公平锁非公平:

          • 概述:

            • 公平锁:所有的线程想要拿锁都要先插到CLH队列尾巴上面来
            • 非公平锁:链表上面已经有很多在等了,但是来了一个线程,进来不排队,直接就拿锁
          • 代码:在显示锁ReentrantLock(显示锁,独占锁要去实现tryAcquire)中实现了公平与非公平

            • 示意图:两者都实现了tryAcquire,基本一样

            图片.png

            • 区别:有没有去检查等待队列里面

               //公平锁,会判断当前队列中有没有元素在等待
               if (!hasQueuedPredecessors() &&
                   compareAndSetState(0, acquires))
               //非公平锁,直接就兴起CAS操作了
               if (compareAndSetState(0, acquires)) 
              
        • 锁的可重入

          • 锁不可重入:一个线程只能对一把锁拿一次,第二次就拿不到(造成死锁了)

            • 就像上面写的那个独占锁一样:第二次拿CAS失效

               if(compareAndSetState(0,1)){
                       //这里是不支持锁的可重入的,一个线程只能拿对一把锁拿一次
                       
                       //下面那个是排他的所有线程,就是说谁拿到这把锁,现在这把锁被我拿到了
                       setExclusiveOwnerThread(Thread.currentThread());
                       return true;
                   }
              
            • 解决锁的不可重入性:增加一个else判断这个线程的名字是不是已经拿到了的

               public boolean tryAcquire(int acquires) {
                   if (compareAndSetState(0, 1)) {
                       setExclusiveOwnerThread(Thread.currentThread());
                       return true;
                   }else if(getExclusiveOwnerThread()==Thread.currentThread()){
                       setState(getState()+1);//因为要释放锁,拿了几次;那么我在释放锁的时候,也要对state减1,直到等于0,就说明已经退出了最外围的同步代码块,那么就可以拿给其他线程用了
                       return  true;
                   }
                   return false;
               }
              
          • 测试方法:

            • 递归:一个线程不断获取同一把锁

            • 方法调用

               sync A(){
                   B()
               }
               sync B(){
                   
               }
              

  • JMM:java内存模型

    • 操作与响应时间:IO极大影响了CPU计算

      image-20220224195656295

    • 为了解决IO的问题,引入了高速缓存

      • 概述:在内存与CPU之间引入高速缓存

        • 离CPU越近,越贵,内存越小

        • Android虚拟机(这个是基于寄存器的)好像是两级缓存

        • 多级缓存之间相互传递数据

        • 示意图:

          image-20220224200109221

    • 为了充分利用高速缓存,引入了JMM(工作内存与主内存)

      • 工作内存与主内存:抽象概念,是许多存储设备的综合

        • 工作内存:

          • CPU寄存器
          • 高速缓存
          • 主内存(就是那个内存条)的一部分(百分之一)
      • 工作表现:

        1. 当有一个变量count在主内存中,
        2. 当线程需要操作变量的时候,放到每一个线程的工作内存中有一个count副本,每个线程的工作内存是线程独享的,类似ThreadLocal
        3. 线程不能直接操作主内存的东西,也不能操纵其他线程工作内存中的东西,只能在自己的工作内存中搞;
    • JMM的问题:

      • 场景:线程A与线程B,需要执行count = count+1;

      • 但是需要编写许多的指令,进行数据的装入等

volatile详解:

  • 概述:

    • 在取值,赋值操作中可以保证并发安全(相当于在get/set上加了锁)
    • 进行复核操作时,就不能保证了,例如i++
  • 指令重排序与流水线:CPU是可以同时执行多条指令的

    • CPU是可以同时执行多条指令的(流水线),指令并发会导致程序不一定按照预设(处理那些没有关联的,调整顺序后可以加快,在单线程中不用担心,但是在多线程中就会出问题)去走,这个时候CPU中有个缓冲,来处理指令执行循序不同
    • 但是CPU里面还有条件预测,因特尔支持10级流水线(意思就是可以同时执行10条CPU指令),Android使用的ARM架构,一般是三级流水线(CPU执行3条语句)
    • volatile可以保持可见性(ThreadLocalMap)还可以抑制重排序,不准指令重排,但是不能保证原子性(还是要加锁)

      • 可见性:JMM

        • 强制写线程将变量放到主内存
        • 强制读线程,每次都要去主内存中读一个
  • volatile应用场景:

    • 一个线程写,多个线程读

    • 写操作之间没有任何关联:

       count = count+1;//不行
       //这种就可以,写操作不关联
       count = 5;
       count = 6;
      
    • 在JDK并发工具包中常用volatile+CAS替换锁:在counrrentHashMap1.8中就是这个干的,1.7就不是这样的;无锁化编程(循环CAS操作+volatile)

  • volatile实现原理:

    • 有volatile修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀(特殊的CPU指令)

      • 线程对变量进行操作,都是在缓存中,这个Lock前缀将当前处理器缓存行的数据写会系统内存(从高速缓存写会主内存)

      • 这个写回内存的操作会使其他CPU缓存的这个数据失效

        • 因为在其他线程中也是缓存了这个变量的,让其失效后,就会从主内存从新拉一次,实际上每次都要去拿
        • 线程是运行在CPU之上的,每个线程在执行任务的时候都会占据一个CPU