zoukankan      html  css  js  c++  java
  • 秋招之路8:JAVA锁体系和AQS抽象队列同步器

    整个的体系图

    悲观锁,乐观锁

    是一个广义概念;体现的是看待线程同步的不同角度。

    悲观锁

    认为在自己使用数据的时候一定有别的线程来修改数据,在获取数据的时候会先加锁,确保数据不被别的线程修改。
    实现:关键字synchronized,接口Lock的实现类
    适用场景:写操作多,先加锁可以保证写操作时的数据正确。

    乐观锁

    认为自己在使用数据的时候不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。
    实现:CAS算法,例如AtomicInteger类的原子自增是通过CAS自旋实现
    适用场景:读操作比较多,不加锁的特点能使读操作性能大幅提升。

    悲观锁,乐观锁的执行流程

    CAS算法

    全名:Compare And Swap
    也叫无锁算法:基于硬件原语实现,在不使用锁(没有线程被阻塞)的情况下,实现多线程之间的变量同步。
    jdk中实现:java.util.concurrent包中的原子类(AtomicInteger)就是通过CAS来实现了乐观锁。

    实现原理

    算法设计的三个操作数
    需要读写的内存值V
    进行比较的值(版本)A
    要写入的新值B

    流程如下:
    如果内存位置的值V与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B;
    否则,不要更改该位置,只告诉我这个位置现在的值,只告诉我这个位置现在的值即可(具体//)。
    具体图为:

    基于CAS实现的原子类关键方法

    AtomicInteger.getAndDecrementUnsafe.getAndAddInt
    源码:稍后补

    CAS存在问题

    ABA问题

    因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,
    但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化过。

    ABA问题的解决思路就是使用版本号。
    在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

    循环开销时间长

    只能保证一个共享变量的原子操作

    从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,
    可以把多个变量放在一个对象里来进行CAS操作。

    自旋锁

    它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。

    但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

    线程的上下文切换

    一个重要的问题:线程切换为什么会耗资源耗时间?

    1. 线程切换的时候,CPU需要将此线程的所有执行状态保存起来,如线程编号,执行到的位置等,然后再去执行其它线程。
    2. CPU运行状态分为用户态和内核态。线程切换状态会使CPU运行状态从用户态转换到内核态。
      这里面有一篇:专门讲用户态和内存态的[https://www.cnblogs.com/maxigang/p/9041080.html]。

    AQS队列同步器原理


    ReentrantLock中的一个内部类

    其具体是基于CLH队列实现。

    CLH:CLH是Craig,Landia,Hagerasten三人发明的一种基于双向链表的数据结构队列。
    java中的CLH队列是原CLH队列的一个变种,线程由原来的自旋机制变为了阻塞机制。

    具体原理

    AQS 是构建锁或者其他同步组件的基础框架(如 ReentrantLock、ReentrantReadWriteLock、Semaphore 等), 包含了实现同步器的细节(获取同步状态、FIFO 同步队列)。AQS 的主要使用方式是继承,子类通过继承同步器,并实现它的抽象方法来管理同步状态。

    1. 维护一个同步状态 state。当 state > 0时,表示已经获取了锁;当state = 0 时,表示释放了锁。
    2. AQS 通过内置的 FIFO 同步队列来完成资源获取线程的排队工作:如果当前线程获取同步状态失败(锁)时,AQS 则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

    AQS 内部维护的是** CLH 双向同步队列**

    具体原理图

    公平锁,非公平锁

    在公平的锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个所,那么新发出的请求的线程将被放入到队列中。
    而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中(此时和公平锁是一样的)。
    所以,它们的差别在于非公平锁会有更多的机会去抢占锁。

    总之就是:先来的先排队,先来的先获取资源,而非公平锁则无法提供这个保障。
    其实对于非公平锁,只要线程进入了等待队列,队列里面依然是FIFO的原则,跟公平锁的顺序是一样的。
    具体倒时候,看代码验证吧。//倒时候补

    可重入锁

    可重入锁作用:避免死锁.
    场景:

    public class Demo1 {
        public synchronized void functionA(){
            System.out.println("iAmFunctionA");
            functionB();
        }
        public synchronized void functionB(){
            System.out.println("iAmFunctionB");
        }
    }
    

    functionA()和functionB()都是同步方法,当线程进入funcitonA()会获得该类的对象锁,这个锁"new Demo1()",在functionA()对方法functionB()做了调用,但是functionB()也是同步的,因此该线程需要再次获得该对象锁(new Demo1())。其他线程是无法获该对象锁的。
    可重入锁的作用就是为了避免死锁,java中synchronized和ReentrantLock都是可重入锁

    可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
    其实,就是改变上图的state的状态++。

    锁的特性

    • 可重入锁:指的是在一个线程中可以多次获取同一把锁。 ReentrantLock 和 synchronized 都是可重入锁。
    • 可中断锁:顾名思义,就是可以相应中断的锁。synchronized 就不是可中断锁,而 Lock 是可中断锁。
    • 公平锁:即尽量以请求锁的顺序来获取锁。synchronized 是非公平锁,ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

    偏向锁

    “偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。

    偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。

    自适应锁

    自适应自旋锁就是线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数

  • 相关阅读:
    集合-ArrayList 源码解析
    >>《配色设计原理.pdf》
    >>《[美国视觉设计学院用书:完成设计(从理论到实践)].(萨马拉).温迪等.扫描版.pdf》
    《Photoshop智能手机APP界面设计.pdf》
    《贴心设计:打造高可用性的移动产品》——5.平板电脑和电子书阅读器
    3H Buy a Ticket —— Dij
    最短路板子
    3E A Simple Problem —— 数论
    3D 城池攻占 —— 左偏树
    3A Least Cost Bracket Sequence —— 贪心
  • 原文地址:https://www.cnblogs.com/whyaza/p/12336777.html
Copyright © 2011-2022 走看看