• 并发编程之美


    一、AQS

    什么是AQS

    fifo队列 + 原子int(表示状态)

    原子int:AtomicInteger这个类的存在是为了满足在高并发的情况下,原生的整形数值自增线程不安全的问题;

    AQS(AbstractQueuedSynchronizer),AQS是JDK下提供的一套用于实现基于FIFO(先进先出)等待队列的阻塞锁和相关的同步器的一个同步框架。这个抽象类被设计为作为一些可用原子int值来表示状态的同步器的基类。如果你有看过类似 CountDownLatch 类的源码实现,会发现其内部有一个继承了 AbstractQueuedSynchronizer 的内部类 Sync。可见 CountDownLatch 是基于AQS框架来实现的一个同步器.类似的同步器在JUC下还有不少。(eg. Semaphore)

    AQS用法

    如上所述,AQS管理一个关于状态信息的单一整数,该整数可以表现任何状态。比如, Semaphore 用它来表现剩余的许可数,ReentrantLock 用它来表现拥有它的线程已经请求了多少次锁;FutureTask 用它来表现任务的状态(尚未开始、运行、完成和取消)。

    java可重入锁-ReentrantLock实现细节

    ReentrantLock支持两种获取锁的方式,一种是公平模型,一种是非公平模型。在继续之前,咱们先把故事元素转换为程序元素。

    元素转换 

    咱们先来说说公平锁模型:

    初始化时, state=0,表示无人抢占了打水权。这时候,村民A来打水(A线程请求锁),占了打水权,把state+1,如下所示:

    线程A获取锁

    线程A取得了锁,把 state原子性+1,这时候state被改为1,A线程继续执行其他任务,然后来了村民B也想打水(线程B请求锁),线程B无法获取锁,生成节点进行排队,如下图所示:

    线程B等待

    初始化的时候,会生成一个空的头节点,然后才是B线程节点,这时候,如果线程A又请求锁,是否需要排队?答案当然是否定的,否则就直接死锁了。当A再次请求锁,就相当于是打水期间,同一家人也来打水了,是有特权的,这时候的状态如下图所示:

    可重入锁获取

    到了这里,相信大家应该明白了什么是可重入锁了吧。就是一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加。如果线程A释放了一次锁,就成这样了:

    线程A释放一次锁

    仅仅是把状态值减了,只有线程A把此锁全部释放了,状态值减到0了,其他线程才有机会获取锁。当A把锁完全释放后,state恢复为0,然后会通知队列唤醒B线程节点,使B可以再次竞争锁。当然,如果B线程后面还有C线程,C线程继续休眠,除非B执行完了,通知了C线程。注意,当一个线程节点被唤醒然后取得了锁,对应节点会从队列中删除。 

    源码

    AbstractQueuedSynchronizer(AQS)
    //队列头
    private transient volatile Node head;
    //队列尾
    private transient volatile Node tail;
    同步状态
    private volatile int state;

    /**
    * 获取锁,
    /
    public final void acquire(int arg) {
    //尝试获取锁
    if (!tryAcquire(arg) &&
    //自旋获取锁
    acquireQueued(

    addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
    }

    /**
    * 获取锁,如果获取失败则会进入CLH队列
    /
    private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // 获取原队尾
    Node pred = tail;
    if (pred != null) {
    node.prev = pred;
    //用cas更新,pred是原来队尾,作为预期值,node作为新值
    if (compareAndSetTail(pred, node)) {
    pred.next = node;
    return node;
    }
    }
    //前面cas更新失败后,再enq方法中循环用cas更新直到成功
    enq(node);
    return node;
    }

    /**
    * 自旋获取锁
    /
    final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
    boolean interrupted = false;
    for (;;) {
    //拿到当前节点的前置节点
    final Node p = node.predecessor();
    //如果当前节点的前置节点是头节点的话,就再次尝试获取锁
    if (p == head && tryAcquire(arg)) {
    //成功获取锁后,将节点设置为头节点
    setHead(node);
    p.next = null; // help GC
    failed = false;
    return interrupted;
    }
    //更改当前节点前置节点的waitStatus,只有前置节点的waitStatus=Node.SIGNAL
    //当前节点才有可能被唤醒。如果前置节点的waitStatus>0(即取消),则跳过取更前面的节点
    if (shouldParkAfterFailedAcquire(p, node) &&
    //通过Unsafe.park来阻塞线程
    parkAndCheckInterrupt())
    interrupted = true;
    }
    } finally {
    if (failed)
    cancelAcquire(node);
    }
    }

    /**
    * 线程释放锁,从前面可以知道,获取到锁的线程会设置为CLH队列的头部。
    这里如果tryRelease返回true,且head的waitStatus!=0。就会更新head的waitStatus为0并且 唤醒线程head.next节点的线程
    /
    public final boolean release(int arg) {
    if (tryRelease(arg)) {
    Node h = head;
    if (h != null && h.waitStatus != 0)
    //更新head的waitStatus为0并且唤醒线程head.next节点的线程
    unparkSuccessor(h);
    return true;
    }
    return false;
    }

    /**
    * 更新head的waitStatus为0并且唤醒线程head.next节点的线程
    /
    private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    //waitStatus不是取消状态,就设置成0
    if (ws < 0)
    compareAndSetWaitStatus(node, ws, 0);
    //获取下个waitStatus不为取消的Node
    Node s = node.next;
    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;
    }
    //LockSupport.unpark是调用了Unsafe.unpark,唤醒线程
    if (s != null)
    LockSupport.unpark(s.thread);
    }


    /**
    * CLH队列(FIFO双端双向队列)
    /
    static final class Node {
    //用于标识共享锁
    static final Node SHARED = new Node();
    //用于标识独占锁
    static final Node EXCLUSIVE = null;
    /因为超时或者中断,节点会被设置为取消状态,被取消的节点时不会参与到竞争中的,他会一直保持取消状态不会转变为其他状态
    static final int CANCELLED = 1;
    //当前节点释放锁的时候,需要唤醒下一个节点
    static final int SIGNAL = -1;
    //节点在等待队列中,节点线程等待Condition唤醒
    static final int CONDITION = -2;
    //表示下一次共享式同步状态获取将会无条件地传播下去
    static final int PROPAGATE = -3;

    //等待状态
    volatile int waitStatus;
    //前驱节点
    volatile Node prev;
    //后继节点
    volatile Node next;
    //节点线程
    volatile Thread thread;
    //
    Node nextWaiter;
    }

    二、CAS

    一、cas概念

    CAS(Compare And Swap),即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。

    二. CAS底层原理

    首先来看看atomicInteger.getAndIncrement()为什么不加synchronized也能在多线程下保持线程安全

    点开后,我们发现有个unsafe类,unsafe是CAS的核心类

    1. Unsafe
    是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native) 方法来访问,Unsafe相当于-一个后门,基于该类可以直接操作特定内存的数据。Unsafe类 存在于sun.misc包中,其内部方法操作可以像C的指针一-样直接操作内存,因为Java中Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native) 方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
    注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

    2. 变量valueOffset, 表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。

    3. 变量value用volatile修饰, 保证了多线程之间的内存可见性。

    2.1 JMM内存模型(涉及到的知识点)
    由于JMM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

     

    2.2 CAS底层

    CAS的全称为Compare-And-Swap,它是一-条CPU并发原语。

    它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

    CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会 造成所谓的数据不一致问题。

    首先, var1代表当前对象,var2代表对象的偏移地址,var4就是那个+1的值,然后getIntVolatile()这个方法去获取当前对象的这个值是多少,给他保存到var5,然后,过了一会,compareAndSwapInt()这个方法去再比较当前对象的值还是不是var5,是的话就给这个值+1,返回true,while里面就是false就退出循环,最后返回出+1后的值. 如果当前对象不是之前的var5了,返回一个false,while循环里面就是true,继续循环,拿到下一个值去比较,直到比较成功~

    再来一遍,根据JMM内存模型来看

    1. 假设有两个线程AB,根据上面的内存模型来看 AtomicInteger 里面的value原始值为5,即主内存中AtomicInteger的value为5,根据JMM模型,线程A和线程B各自持有一份值为5的value的副本分别到各自的工作内存。
    2. 线程A通过getIntVolatile(var1, var2)拿到value值5, 这时线程A被挂起。
    3. 线程B也通过getlntVolatile(var1, var2)方法获取到value值5, 此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为5,成功修改内存值为6,线程B打完收工,一切OK。
    4. 这时线程A恢复,执行compareAndSwapInt方法比较, 发现自己手里的值数字5和主内存的值数字6不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取新来一遍了。
    5. 线程A重新获取value值, 因为变量value被volatile修饰, 所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。


    2.3 总结与应用


    1. CAS (CompareAndSwap)总结

    比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,
    否则继续比较直到主内存和工作内存中的值一致为止.

    2. CAS应用

    CAS有3个操作数,内存值V;旧的预期值A,要修改的更新值B。
    当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。.

    3. CAS缺点

    由于用了while循环,当在线程数比较大的情况下,如果失败可能会出现一直循环,导致CPU过高
    只能保证一个共享变量的原子操作。
    引出来ABA问题(反正就是狸猫换太子把戏)


    三. 自旋锁SpinLock

    是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

     1 import java.util.concurrent.TimeUnit;
     2 import java.util.concurrent.atomic.AtomicReference;
     3 
     4 /**
     5  * 自旋锁Demo
     6  */
     7 public class SpinLockDemo {
     8 
     9     AtomicReference<Thread> atomicReference=new AtomicReference<>();
    10 
    11     public void myLock(){
    12        Thread thread=Thread.currentThread();
    13         System.out.println(Thread.currentThread().getName()+"	 "+"come in ^0^");
    14 
    15         while(!atomicReference.compareAndSet(null,thread)){
    16 
    17         }
    18     }
    19 
    20     public void myUnlock(){
    21         Thread thread=Thread.currentThread();
    22         atomicReference.compareAndSet(thread,null);
    23         System.out.println(Thread.currentThread().getName()+"	 "+ "invoke myUnlock()");
    24     }
    25 
    26     public static void main(String[] args) {
    27 
    28         SpinLockDemo spinLockDemo=new SpinLockDemo();
    29 
    30         new Thread(()->{
    31             spinLockDemo.myLock();
    32             try {
    33                 TimeUnit.SECONDS.sleep(5);
    34             } catch (InterruptedException e) {
    35                 e.printStackTrace();
    36             }
    37             spinLockDemo.myUnlock();
    38         },"AAA").start();
    39 
    40         try {
    41             TimeUnit.SECONDS.sleep(1); //保证线程绝对运行完毕
    42         } catch (InterruptedException e) {
    43             e.printStackTrace();
    44         }
    45 
    46         new Thread(()->{
    47             spinLockDemo.myLock();
    48             try {
    49                 TimeUnit.SECONDS.sleep(1); //加锁后延迟1s
    50             } catch (InterruptedException e) {
    51                 e.printStackTrace();
    52             }
    53             spinLockDemo.myUnlock();
    54         },"BBB").start();
    55 
    56     }
    57 }

    • 这里我开始拿了个原子线程
    • 定义一个加锁方法,是自旋锁,如果当前线程是空就把当前线程更新进去CAS,并且跳出循环,如果当前线程不是空,就会一直在while循环里一直判断
    • 定义一个解锁办法,获取当前线程,如果原子线程还是当前线程,那就把它设置为null
    • 主方法中,我先让线程AAA获取锁,并且sleep5秒钟,这个时候当前线程就会一直是AAA线程
    • 然后BBB线程进来了,发现当前线程并不是null,而是AAA,就回一直循环判断当前线程什么时候为null,等到5秒后,线程AAA解锁了把当前原子线程释放掉了,这时候BB就拿到锁了,然后跳出循环,最终解锁~

    三、各类锁总结

    四、数据结构

      抛出异常 特殊值 阻塞 超时
    插入 add(e) offer(e) put(e) offer(e, time, unit)
    移除 remove() poll() take() poll(time, unit)
    检查 element() peek() 不可用 不可用

    这里我们把上述操作进行分类

    • 插入方法

      • add(E e) : 添加成功返回true,失败抛IllegalStateException异常
      • offer(E e) : 成功返回 true,如果此队列已满,则返回 false。
      • put(E e) :将元素插入此队列的尾部,如果该队列已满,则一直阻塞
    • 删除方法:

      • remove(Object o) :移除指定元素,成功返回true,失败返回false
      • poll() : 获取并移除此队列的头元素,若队列为空,则返回 null
      • take():获取并移除此队列头元素,若没有元素则一直阻塞。
    • 检查方法

      • element() :获取但不移除此队列的头元素,没有元素则抛异常
      • peek() :获取但不移除此队列的头;若队列为空,则返回 null。

    4.2常用的几种队列

    ConcurrentLinkedQueue

    基于CAS算法

    ArrayBlockingQueue

    基于独占锁的有界列表阻塞队列

    源码解析

    LinkedBlockingQueue

    基于独占锁的有界数组阻塞队列

    PriorityBlockingQueue

    基于平衡二叉树带优先级的无界阻塞队列

    特点:

    1、直接遍历不保证有序,默认使用compareTo方法提供比较

    DelayQueue

    无界阻塞延迟队列,队列的每个元素都有过期时间,只有过去元素才会出队列,队列头是最快要过期的元素

  • 相关阅读:
    插入排序Java实现
    WMI控制IIS(2)
    WMI控制IIS
    C语言中用qsort()快速排序
    list_for_each引起的问题
    c++数组地址
    Valgrind 使用简单说明检查内存泄露
    firefox 在UBUNTU下编译
    内存泄漏检测umdh
    ubuntu GIT 安装与使用
  • 原文地址:https://www.cnblogs.com/anhaogoon/p/12932546.html
Copyright © 2020-2023  润新知