欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

并发编程线程基础

程序员文章站 2022-05-04 23:09:24
多线程并发编程并发 是指 同一个时间段内 多个任务 同时都在执行,且都没有执行结束,而 并行 是说 单位时间内 多个任务 同时在执行。 并发任务 强调在同一时间段内 同时进行,而 一个时间段 由 多个单位时间 累积而成。所以说 ,并发的多个任务 在单位时间内,不一定同时在执行。如:线程 A 和 线程 B 各自在自己的 CPU 上执行任务,就可以实现 并行运行。而在多线程编程实践中,线程的个数往往多于 CPU...

多线程并发编程
    并发 是指 同一个时间段内 多个任务 同时都在执行,且都没有执行结束,而 并行 是说 单位时间内 多个任务 同时在执行。 并发任务 强调在同一时间段内 同时进行,而 一个时间段 由 多个单位时间 累积而成。所以说 ,并发的多个任务 在单位时间内,不一定同时在执行。
    如:线程 A 和 线程 B 各自在自己的 CPU 上执行任务,就可以实现 并行运行。而在多线程编程实践中,线程的个数往往多于 CPU 的个数,所以一般称为 多线程并发编程 而不是 多线程并行编程。

    可以从 任务管理器 里看 CPU 核数:
并发编程线程基础

    多个 CPU 意味着 每个线程可以使用自己的 CPU 运行,减少了线程上下文切换的开销,但随着 对 应用系统性能 和 吞吐量 要求的提高,出现了 海量数据 和 请求 的要求,所以 高并发编程 是很有必要的。
    
线程安全
    线程安全问题 是指 当多个线程同时读写一个共享资源 并且没有任何同步措施时,导致出现 脏数据 或者 其他不可预见的结果 的问题。
    如果多个线程都只是读取 共享资源 而不去修改,那么就不会存在线程安全的问题,只有当 至少一个线程 修改 共享资源时,才会存在线程安全问题。
    
Java *享变量的内存可见性问题
    多线程下 处理共享变量 时 Java 的内存模型
并发编程线程基础
    Java内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存中的变量 复制到 自己的工作空间 或者叫作 工作内存,线程读写变量操作的是 自己工作内存中的变量。
    Java 内存模型只是一个抽象概念,实际实现中线程的工作内存:
并发编程线程基础
    如图所示是个 双核 CPU 系统架构 ,每个核有自己的控制器 和 运算器,其中 控制器 包含一组 寄存器 和 操作控制器,运算器执行算术逻辑运算。每个核都有自己的 一级缓存,在有些架构里还有一个 所有 CPU 都共享的 二级缓存。Java 内存模型中的工作内存,就对应 这里的 一级缓存 或者 CPU 的寄存器。
        当一个线程操作共享变量时,它首先从 主内存 复制共享变量 到自己的 工作内存 ,然后对工作内存里的变量进行处理,处理完后将变量值更新到主内存。
        假如线程 A 和线程 B 同时处理一个共享变量,假设如上图所示,线程 A 和线程 B 使用不同 CPU 执行,并且当前两级缓存都会空,那么这时由于缓存的存在,将会导致内存不可见问题
(1)线程 A 首先获取共享变量 X 的值,由于两级缓存都没有命中,所以加载主内存中 X 的值,假如为 0。然后把 X=0 的值缓存到两级缓存,线程 A 修改 X 的值为 1,然后将其写入二级缓存,并且刷新到主内存中。线程 A 操作完毕后,线程 A 所在的二级缓存和主内存里的 X 的值都是 1 。
(2)线程 B 获取变量 X 的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回 X=1。到这里一切都是正常的,因为这时主内存也是 X=1。然后线程 B 修改 X 的值为2 ,并将其存放在线程 B 所在的一级缓存和共享二级缓存中,最后更新主内存的值是2。
(3)线程 A 这次又要修改 X 的值,获取时一级缓存命中,并且 X=1,到这里问题就出现了,明明线程 B 已经把 X 的值修改成 2 了,可是线程 A 获取的还是 1 。这就是 共享变量的内存不可见问题, 也就是线程 B 写入的值对线程 A 不可见。
    
指令重排序
    Java 内存模型 允许 编译器 和 处理器 对指令重排序 以提高运行性能,并且只会对不存在数据依赖的指令重排序。
    
伪共享
#160;       为了解决计算机系统中 主内存 与 CPU 之间运行速度差问题,会在 CPU 与主内存之间添加一级或多级高速缓冲寄存器 (Cache)。这个 Cache 一般是被集成到 CPU 内部的,所以也叫 CPU Cache ,如图是两级 Cache结构:
并发编程线程基础
        在 Cache 内部是按 行 存储的,其中每一行称为 Cache行 ,Cache行是 Cache 与 主内存 进行数据交换的单位,Cache行的大小一般是 2 的幂次数字节
        当 CPU 访问某个变量时,首先会去看 CPU Cache 内是否有该变量,如果有则直接从中获取,否则就去主内存里获取该变量,然后把该变量所在内存区域的一个 Cache 行大小的内存复制到 Cache中。由于存放到 Cache 行的是内存块而不是单个变量,所以可能会把多个变量存放到一个 Cache 行中。当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享。
        伪共享的产生是因为多个变量被放入了一个缓存行中,并且多个线程同时去写入缓存行中的不同变量。
        地址连续的多个变量才有可能被放到一个缓存行中。
        单个线程下顺序修改一个缓存行中的多个量,会充分利用程序运行的局部性原则,从而加速程序的运行; 但在多线程下并发修改一个缓存行中的多个变量就会竞争缓存行,从而降低程序运行性能。
        JDK 8 提供了一个 sun.misc.Contended 注解,用来解决伪共享问题,默认情况下只用于 Java 核心类,如 rt 包下的类。如果用户路径下的类 需要使用这个注解,则需要添加 JVM 参数:-XX:-RestrictContended 。填充的宽度默认为 128,要自定义宽度 则 可以设置 -XX:ContendedPaddingWidth 参数。)

1、synchronized 关键字

    synchronized 块 是 Java 提供的一种 原子性 内置锁,Java 中的每个对象 都可以把它当作一个同步锁来使用,这种 Java 内置的 使用者看不到的锁 被称为 内部锁,也叫做监视器锁。 线程的执行代码 在 进入 synchronized 代码块前 会自动获取内部锁,这时 其他线程访问该同步代码块 就会被阻塞挂起。拿到 内部锁 的线程 会在正常退出同步代码块 或 抛出异常后 或者在同步块内调用了 该内置锁资源的 wait 系列方法时 释放该内置锁。内置锁是排他锁,也就是说,当一个线程获取这个锁后,其他线程必须等待该线程释放锁后,才能获取该锁。
    另外,由于 Java 中的线程是与 操作系统 的 原生线程 一一对应的,所以当阻塞一个线程时,需要从用户态 切换到 内核态 执行阻塞操作,这很耗时,而 synchronized 的使用就会导致上下文切换

    到了 JDK 1.6,对 synchronized 加入了很多优化措施,有自适应自旋、锁清除、锁粗化、轻量级锁、偏向锁等。导致 synchronized 性能并不比 lock 差。 官方也表示,他们更支持 synchronized 。在未来版本中还有优化余地。所以提倡在 synchronized 能实现需求的情况下,优先考虑 synchronized 。
    ✨ 推荐使用顺序: JUC > synchronized >lock/condition。
    

  • synchronized 的内存语义
        进入 synchronized 块的内存语义是 把在 synchronzed 块内使用到的变量 从线程的工作内存 中清除, 这样在 synchronized 块内使用到该变量时 就不会从线程的工作内存中获取,而是直接从主内存中获取。
        退出 synchronized块 是对共享变量的修改刷新到主内存。
        其实这也是加锁和释放锁的语义,当获取锁后,会清空锁块内 本地内存 中将会被用到的共享变量,在使用这些变量时从主内存进行加载,在释放锁时将本地内存中修改的共享变量刷新到主内存。
        除可以解决 共享变量内存可见性问题外, synchronized 经常被用来实现原子性操作
        
  • 从 JVM 的层面讲 synchronized
        多线程的线程同步机制是靠“锁”???? 控制的 。
        在 Java 程序运行环境中,JVM 需要对两类 线程共享的数据进行协调:
    (1)保存在堆中的实例变量;
    (2)保存在方法中的类变量;
        在线程中,每个 类 和 对象 在逻辑上都是与一个监视器相关联的,对于对象来说,相关联的监视器保护的是对象的实例变量,而类的监视器保护的是类的类变量。如果对象无实例变量,类无类变量,监视器就神马也不监视。
        类锁实际上是用对象锁来实现的,在 JVM 装载一个 class 文件时,它就会创建一个 java.lang.Class 类的实例,当锁住一个类时,实际上锁住的是那个类的Class类对象。
        Java 不需要开发人员手动加锁,对象锁是 JVM 内部使用的,只需使用 synchronized 块或 synchronized 方法,就可标志一个监视区域,每当进入一个监视区域时 JVM 都会自动锁上对象或类。
        synchronized 关键字经过编译之后,会在同步块的前后形成 monitorentermonitoreexit 这两个字节码指令,这两个字节码都需要一个 reference 类型的参数来指明要锁定 和 解锁对象,如果 Java 程序中的 synchronized 明确指定了 对象参数,那就是这个对象的 reference,如果没有明确指定 ,那就根据 synchronized 修饰的是实例方法还是类方法,去取对应的对象实例或 Class 对象来作为锁对象。
        在执行 monitorenter 时,首先尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了该对象的锁,把锁的计数器加 1 ,在执行 monitorexit 时,会把锁的计数器减 1,当计数器为 0 时,锁会被释放。
        ???? synchronized 内部锁 是 可重入锁,就是说,当一个线程 再次获取 它自己 已经获取的锁时 不会被阻塞,是因为 在锁内部 维护了一个线程标示,用来标示 该锁目前被哪个线程占用,然后关联一个计数器。一开始 计数器值为 0,说明 该线程没有被任何线程占用。当一个线程 获取了 该锁,计数器的值 会变成 1 ,这时 其他线程再来获取 该锁 时,会发现 锁的所有者不是自己 而被阻塞挂起。但是当获取了该锁的线程 再次获取锁 时 发现 锁的拥有者 是自己,就会把计数器值 +1,当释放锁后 计数器值 -1,当计数器值为 0 时,锁里面的线程标示 被重置为 null,这时 被阻塞的线程会被唤醒,来竞争获取该锁。
       &#160

2、volatile 关键字

    锁太笨重,因为会带来线程上下文的切换开销,对于解决 内存可见性的问题, Java 还提供了一种 弱形式 的同步,就是使用 volatile 关键字。该关键字可以保证 对一个变量的更新 对其他线程 马上可见。
    当一个变量被声明为 volatile 时,线程在 入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。
     当其他线程 取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
    写 volatile 变量时,可以确保 volatile 写 之前的操作 不会被编译器重排序到 volatile 写之后。读 volatile 变量时,可以确保 volatile 读 之后的操作 不会被编译器重排序到 volatile 读 之前。
(当线程写入了 volatile 变量值时就等价于线程退出 synchronized 同步块,读取 volatile 变量值就相当于进入同步块。)
例????
首先是没有使用同步措施的:

public class ThreadNotSafeInteger {
    private int value;
    public int get()
    {return value;}

    public void set(int value){
        this.value=value;
    }
}

使用 synchronized 关键字进行同步:

public class ThreadSafeInteger1 {
    private int value;

    public synchronized int get() {
        return value;
    }

    public synchronized void set(int value){
        this.value=value;
    }
}

使用 volatile 进行同步:

public class ThreadSafeInterger2 {
    private volatile int value;

    public int get(){
        return value;
    }

    public void set(int value){
        this.value=value;
    }
}

     在这里 使用 synchronized 和 使用 volatile 是等价的,(也就是在这个例子里,volatile 刚好是线程安全的,但是它本身是不能保证线程安全的!)都解决了 共享变量 value 的内存可见性问题,但是前者是 独占锁,同时只能有一个线程调用 get() 方法,其他调用线程会被阻塞,同时会存在 线程上下文切换 和 线程重新调度 的开销,这也是 使用锁方式 不好的地方。而后者 是 非阻塞算法,不好造成线程上下文切换开销。
     但并非在所有情况下 使用它们是等价的 ,volatile 虽然提供了可见性保证,但 并不保证操作的原子性。
     ❓ 哪时候使用 volatile 关键字呢 ❓

  • 写入变量值不依赖变量的当前值时。 因为如果依赖当前值,将是 获取 - 计算 - 写入 三步操作,这三步操作不是原子性的,而 volatile 不保证原子性。
  • 读写变量值 时 没有加锁。 因为加锁已经保证了内存可见性,这时候不需要把变量声明为 volatile 。(那其实也是要有 内存可见性 的需求,才有必要用 volatile 吧❔ )
  • 从 JVM 的层面讲 volatile
            见这篇博客 :https://blog.csdn.net/weixin_41750142/article/details/104379544

  • volatile 与 synchronized 内存语义:
    并发编程线程基础

  • volatile 与 synchronized 实现功能:
    并发编程线程基础

3、CAS 操作

     CAS ,compare and swap ,是 JDK 提供的 非阻塞 原子性操作,通过硬件保证了 比较 - 交换 操作的原子性。JDK 的 Unsafe 类 提供了一系列 compareAndSwap* 方法,如 JDK1.8 源码是:

 public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

之前的版本中,源码是:

boolean compareAndSwapLong(Object obj, long valueOffset, long except, long update)

     CAS 有 4 个操作数,分别为L对象内存位置、 对象中变量的偏移量、变量预期值 和 新的值。其操作含义是,如果 对象 obj 内存偏移量为 valueOffset 的变量值为 expect,则使用新的值 update 替换旧的值 expect。这是处理器的一个原子性指令。
     当多个线程使用 CAS 操作一个变量时,只有一个线程会成功,并成功更新,其余会失败,失败的线程会重新尝试,当然也可以选择挂起线程。

ABA问题

    关于 CAS 操作有个经典的 ABA 问题
    假设线程 1 使用 CAS 修改初始值为 A 的变量 X ,那么线程 1 会首先去获取当前变量 X 的值(为 A ),然后使用 CAS 操作尝试修改 X 的值为 B ,如果使用 CAS 操作成功了,那么程序运行一定是正确的吗?
    其实未必。 有可能在线程 1 获取变量 X 的值 A 后,在执行 CAS 之前,线程 2 使用 CAS 修改了变量 X 的值为 B ,然后又使用 CAS 修改变量 X 的值为 A 。虽然线程 1 执行 CAS 时 X 的值仍是 A ,但这个 A 已经不是线程 1 获取时的 A 了。
    ABA 问题的产生是因为变量的状态值产生了 环形转换,就是变量的值可以从 A 到 B,再从 B 到 A 。如果变量的值只能朝着一个方向转换,比如从 A 到 B,B 到 C ,不构成环形,就不会出现问题。 JDK 中的 AutomicStampedReference 类给每个变量的状态值都配备了一个时间戳,从而避免了 ABA 问题的产生。

4、Unsafe 类

并发编程线程基础

    JDK 的 rt.jar 包中的 Unsafe 类提供了硬件级别的原子性操作,Unsafe 类中的方法都是 native 方法,它们使用 JNI【Java Native Interface】的方式访问本地 C++ 库。
    熟悉几个方法:
(1)

public native long objectFieldOffset(Field var1);

    返回指定变量 在 所属类中 的 内存偏移地址。该偏移地址仅仅在 该 Unsafe 函数中访问指定字段时使用。
(2)

  public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

    比较 对象 var1 中偏移量为 var2 的 变量的值 是否与 var4 相等,相等则使用 var5 值更新;然后返回 true,否则返回 false。

例???? :

import sun.misc.Unsafe;

public class TestUnsafe {
    // 获取 Unsafe 的实例
    static final Unsafe unsafe=Unsafe.getUnsafe();

    // 记录变量 state 在类 TestUnsafe 中的偏移量
    static final long stateOffset;

    // 变量
    private volatile long state=0;

    static {
        try{
            // 获取 state 变量在类 TestUnsafe 中的偏移量
            stateOffset=unsafe.objectFieldOffset(TestUnsafe.class.
                    getDeclaredField("state"));
        }
        catch (Exception ex){
            System.out.println(ex.getLocalizedMessage());
            throw new Error(ex);
        }
    }
    public static void main(String[] args) {
        // 创建实例,并设置 state 的值为 1
        TestUnsafe testUnsafe=new TestUnsafe();
        Boolean sucess=unsafe.compareAndSwapInt(testUnsafe,stateOffset,0,1);
        System.out.println(sucess);
    }
}

运行抛出异常了:
并发编程线程基础
原因出在 getUnsafe 的源码:

@CallerSensitive
    public static Unsafe getUnsafe() {

		//(1)
        Class var0 = Reflection.getCallerClass();
        
		//(2)
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

    (1)处,获取 调用 getUnsafe 这个方法的对象的 Class 对象,这里是 TestUnSafe.class。 (2)处,判断是不是 Bootstrap 类加载器 加载的 loadClass,即 看 是不是 BootStrap 加载器加载了 TestUnsafe,class,但是吧, TestUnsafe,class 是 AppClassLoader 加载的,所以就抛出了异常。
    ❓ 为什么要有这个判断呢 ❓ Unsafe 类是 rt.jar 包提供的,rt.jar 包 里面的类是使用 BootStrap 类加载器 加载的,而 我们的启动 main 函数所在的类,是使用 AppClassLoader 加载的,所以 在 main 函数里加载 Unsafe 类时,根据委托机制,会委托给 BootStrap 去加载 Unsafe 类。要是没有这个判断,那么我们的应用程序就可以随意使用 Unsafe 做事情了,而 Unsafe 类可以直接操作内存,这是不安全的(怪不得叫 Unsafe),所以 JDK 开发组特意做了这个判断,不让开发人员在正规渠道使用 Unsafe 类,而是在 rt.jar 包里的核心类中 使用 Unsafe 功能。
    而开发人员想实例化 Unsafe ,应该这样,使用反射:

  static final Unsafe unsafe;
            // 通过反射获取 Unsafe 的成员变量 theUnsafe
            Field field=Unsafe.class.getDeclaredField("theUnsafe");

            // 设置为可存取
            field.setAccessible(true);

            // 获取该变量的值
            unsafe= (Unsafe) field.get(null);

    运行成功,输出 true。
    看 Unsafe 的源码,可以看到 theUnsafe 是静态字段,所以调用 field.get 方法的时候,传入任何对象都是可以的,包括null 。

 private static final Unsafe theUnsafe;

5、乐观锁 与 悲观锁

    乐观锁 与 悲观锁 是数据库中引入的名词,但是在并发包 锁 里面 也引入了类似的思想。

    悲观锁是指对 数据被外界修改 持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前进行加锁,并且在整个数据处理过程中,使数据处于锁定状态。
    悲观锁的实现往往依靠数据库提供的锁机制,即 在数据库中,在对数据记录操作前,给记录加 排他锁。如果获取锁失败,则 说明 数据正在被其他线程修改,当前线程 等待 或者 抛出异常。如果获取锁成功,则对记录进行操作,然后 提交事务后 释放排他锁。

    乐观锁是相对于悲观锁而言的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排他锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测。典型的乐观锁——CAS。
    乐观锁并不会使用数据库提供的锁机制,一般在 表中 添加 version 字段 或者 使用业务状态来实现。乐观锁直到提交时才锁定,所以不会产生任何死锁。
    

6、公平锁 与 非公平锁

     根据线程获取锁的抢占机制,锁分为公平锁和非公平锁。
     公平锁表示线程获取锁的顺序 是按照 线程请求锁的时间 早晚 来决定的,也就是 最早请求锁的线程 将最早获取到锁。而非公平锁是在运行时闯入,也就是先来不一定先得。
    ReentrantLock 提供了 公平 和 非公平锁的实现。
公平锁:

ReetrantLock pairLock=new ReetrantLock(true);

非公平锁:

ReetrantLock pairLock=new ReetrantLock(false);

    如果构造函数不传递参数,默认是非公平锁。
    在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。
    

7、独占锁 与 共享锁

    根据锁 只能被单个线程持有 还是 能被多个线程共同持有,锁 分为 独占锁 和 共享锁。
    独占锁 保证 任何时候只有一个线程能得到锁,ReetrantLock 就是以独占方式实现的。共享锁则同时可以由多个线程持有,例如 ReadeWriteLock 读写锁,它允许一个资源可以被多线程同时进行操作。
   &#160**;独占锁是一种悲观锁**,每次访问资源都要加上互斥锁,这限制了并发性,因为读操作并不会影响数据的一致性,而 独占锁只允许 在同一时间 由一个线程 读取数据 ,其他线程必须等待当前线程释放锁 才能进行读取。
    共享锁是一种乐观锁。它放宽了加锁的条件,允许多个线程同时进行读操作。
    

8、自旋锁

     自旋锁 是 当前线程在获取锁时,如果发现 锁已经被其他线程占有,它不马上阻塞自己,在不放弃 CPU 使用权的情况下,多次尝试获取( 默认次数是 10 )很有可能 在后面几次尝试中 其他线程已经释放了锁。如果尝试指定次数后 仍没有获取到锁,则当前线程才会被阻塞挂起。
    自旋锁是使用 CPU 时间来换取 线程阻塞 与 调度 的开销,但是很有可能 这些 CPU 时间白白浪费了。(与线程阻塞相比,自旋 会浪费大量的 处理器资源,因为 当前线程仍处于运行状态,只不过跑的是 无用指令。)
    

  • 自适应自旋
        自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间和锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚获取过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为 这次自旋 也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,而如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时可能就省略掉自旋过程,以避免处理器资源浪费。

9、轻量级锁

    HotSpot 虚拟机的对象头(Object Header)分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄等,称为 ”Mark Word“ ,另一部分用于存储指向方法区对象类型的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。
    在 Mark Word 中有 2 bit 用于存放标志位,分别代表状态:未锁定、轻量级锁定、重量级锁定、可偏向、GC标记。
    在代码进入同步块后,如果此同步对象没有被锁定,虚拟机首先将 在当前线程的栈帧中建立一个名为 锁记录 (Lock Record)的空间,用于存储对象目前的 Mark Word 拷贝。
    然后 虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为 指向 Lock Record 的指针。如果更新成功了,那么这个线程就拥有了该对象的锁,并且标志位变为 “轻量级锁定”;而如果没有成功,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果指向,说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个对象已经被其他线程抢占了,如果有两个以上的线程争用同一个锁,那轻量级锁就不再有效,会膨胀为 重量级锁,改变锁标志位。
    这个轻量级锁是针对于传统的”重量级锁“而言的,从 1.6 新加的。本意是 在没有多线程竞争的前提下,减少传统的重量级锁因为互斥量产生的性能损耗。
    解锁过程也是通过 CAS 操作进行的,如果对象的 Mark Word 仍然指向线程的锁记录,那就用 CAS 操作把对象当前的 Mark Word 和 线程中复制的 拷贝的原来的那个 Mark Word 替换回来,如果替换成功,同步过程就完成了;(也就是释放掉锁喽。)如果替换失败,说明有其他线程尝试过获取该锁,那在释放锁的同时,要唤醒被挂起的线程。
    

9、偏向锁

    这个锁会偏向第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,则 持有偏向锁的线程将永远不需要进行同步。
    假设当前虚拟机启用了偏向锁,那么当锁对象第一次被线程获取的时候,虚拟机会把对象头中的标志设置为 ”可偏向“,同时使用 CAS 把取得到这个锁的线程的 ID 记录在对象的 Mark Word,如果CAS 成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机不再进行任何同步操作 。而当另一个线程尝试去获取这个锁时,偏向模式就宣告结束了,根据锁对象目前是否处于被锁定的状态,将状态改为 ”轻量级锁“ 或 ”未锁定“。
    如果程序中的大多数锁总是被多个不同的线程访问,那么偏向模式是多余的。

本文地址:https://blog.csdn.net/weixin_41750142/article/details/109614624