Java内存模型和线程安全

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

Java内存模型和线程安全

Java内存模型

引言

在这里插入图片描述

对于多核处理器而言,每个核都会有自己单独的高速缓存,又因为这多个处理器共享同一块主内存,为了在并行运行的情况下,包装各个缓存中缓存的结果的一致性,需要引用缓存一致性协议。

注意: 处理器只和自己的高速缓存交换,如果修改了高速缓存中的数据,就需要同步回主内存,并且通过缓存一致性协议让其他核的高速缓存失效。

高速缓存的出现主要是为了解决CPU运算速度和主内存速度不匹配而引入的缓冲模块


在这里插入图片描述

上图是java的内存模型,Java线程的数据读写都只能从工作内存获取,不同线程的工作内存是隔离的、

此处的工作内存主要对应线程私有的虚拟机栈部分,而主内存则对应Java堆中的对象实例数据部分。

同样JVM也必须通过一种一致性协议来保证多个工作内存间的数据一致性问题。


volatile关键字

volatile具有两个作用:

  • volatile关键字修饰的变量对所有线程立马可见
  • 禁止指令重排优化

为什么volatile变量具有上面两个作用呢? 是因为Java内存模型中对volatile变量定义了特殊处理规则:

  • 每次使用volatile变量前都必须从主内存中获取最新结果
  • 每次修改volatile变量后都必须立刻同步到主内存中
  • volatile修饰的变量不会被指令重排序优化

目前还存在两个问题,我们依次来解决一下:

  • 防止指令重排序如何实现的 ?

在这里插入图片描述

对应汇编代码:

在这里插入图片描述

volatile修饰的变量,赋值后,会生成一个lock addl指令,该指令作用是将本处理器的缓存写入内存,该写入通过缓存一致性协议也会使得其他高速缓存失效。

因为lock alld指令把修改同步到内存,那么意味着所有之前的操作都已经执行完成了,这样便形成了指令重排序无法越过内存屏障的效果。

指令重排序只会在多线程情况下存在并发问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zgJi8zx8-1673796664803)(Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.assets/image-20230115225020488.png)]


  • volatile修饰的变量一定是并发安全的吗?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IqHUWwPD-1673796664803)(C:/Users/zdh/AppData/Roaming/Typora/typora-user-images/image-20230115223112467.png)]

volatile修饰符提供的两个作用并没有体现出其一定是并发安全的,上面的例子也证明了,那么为什么呢

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AoGABo1U-1673796664803)(C:/Users/zdh/AppData/Roaming/Typora/typora-user-images/image-20230115223232246.png)]

一个自增操作在字节码层面就对应字条指令,如果在算上解释执行每条指令需要使用到多行代码,或者编译执行对应多条机器码,那么光一个自增操作底层可能就需要执行十几条c代码或者几十条汇编指令,并且不保证这些c代码和汇编指令的原子执行,因此不是线程安全的。


synchronized关键字

synchronized关键字具有可见性,因为对一个变量执行lock前,必须从主内存获取最新值,对一个变量执行unlock前,必须先把此变量同步回主内存。


Java线程

线程调度方式:

  • 协同式调度: 线程执行时间由线程本身控制
  • 抢占式调度: 线程执行时间由系统进行分配

java线程采用1:1内核线程方式实现,因此采用的是抢占式调度。

状态转换:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sp6IrVr0-1673796664804)(Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.assets/image-20230115232325626.png)]


Java线程安全

  • 不可变对象一定是线程安全的,如: String,Integer等
  • synchronized关键字实现互斥同步: 通过monitorEnter和monitorExit两个字节码指令完成抢锁和释放锁步骤释放锁由JVM保证一定能够完成

在这里插入图片描述

  • java的Lock接口在java层面实现的互斥锁如: ReentrantLock, 优势: 等待可中断,实现了公平抢锁机制,可以关联多个条件队列。劣势: 需要在finally块中手动释放锁。
  • 如果要实现非阻塞同步可以使用CAS加重试机制CAS需要硬件方面提供支持如: java提供的原子类,AtomicInteger 。CAS会产生ABA问题可以通过添加版本号解决如: AtomicStampedReference。
  • 对于不需要任何同步的场景有: 可重入代码(结果可预测)线程本地存储: ThreadLocal

在这里插入图片描述


synchronized锁优化

‘synchronized锁在jdk5之前最大的问题在于一旦抢不到锁就需要阻塞,而不能通过短暂的重试机制来避免阻塞造成的线程切换, 因为java线程是直接映射到内核线程的,因此线程上下文切换开销很大。

锁优化技巧列举

自旋锁

核心思想: 如果抢锁失败,那么通过CAS重新尝试多次,如果成功,那么就避免了阻塞造成的线程上下文切换,如果失败那么就进行阻塞等待。

JDK 1.4.2中引入了最简单的自旋锁实现,说它简单是因为它的自旋次数默认是十次不会根据运行状态动态变更。

JDK 6中引入了自适应自旋锁,相比于JDK 1.4.2简单的自旋锁实现而言,对自旋次数进行动态变更。

  • 如果上一次自旋等待过程中成功获得某个对象锁,那么这一次会动态调大自旋次数。
  • 如果多次自旋等待过程中都没能成功获得某个对象锁,那么下一次可能会跳转自旋过程,避免浪费吹起资源。

锁消除

虚拟机即时编译器运行时,对一些代码要求同步但是通过逃逸分析检测发现当前方法内部所有在堆上的数据都不会逃逸出去从而被其他线程访问到,那么就可以把它们认为是线程私有的因此会消除该方法内部所有同步措施。

    public static String concatString(String ... str) {
        StringBuffer stringBuffer = new StringBuffer();
        for (String s : str) {
            stringBuffer.append(s);
        }
        return stringBuffer.toString();
    }

stringBuffer内部每个方法调用都会加锁但是该方法本身执行是不存在线程安全问题的,因此可以忽略内部所有同步措施。


锁粗化

虽然推荐尽可能将同步范围缩小,但是如果某个方法内部存在下面的情况:

    private static Integer i = 0;

    private static void display1() {
        for (int i = 0; i < 10; i++) {
            synchronized (Main.class) {
                i++;
            }
        }
    }

    private static void display2() {
        synchronized (Main.class) {
            i++;
        }
        synchronized (Main.class) {
            i++;
        }
        synchronized (Main.class) {
            i++;
        }
    }

上面举例的方法中都存在反复对一个对象加锁和解锁的步骤,这样即使在没有线程竞争的情况下,频繁地进行互斥同步操作也会导致性能损耗因此会进行锁粗化优化:

    private static Integer i = 0;

    private static void display1() {
        synchronized (Main.class) {
            for (int i = 0; i < 10; i++) {
                i++;
            }
        }
    }

    private static void display2() {
        synchronized (Main.class) {
            i++;
            i++;
            i++;
        }
    }

把多个同步代码块用一个同步块替换如果是循环体内部的同步块那么将同步块外提。


具体实现

synchronized锁最原始的实现如下:
在这里插入图片描述
该实现最大的问题在于忽略了多数同步代码块运行周期内是不存在竞争的因此频繁的加锁和解锁设置也会导致性能损耗并且还需要创建一个Monitor实例对象。

轻量级锁

因此我们也称上面这种原始实现为重量级锁为了对重量级锁进行优化jvm推出了轻量级锁:

  • 轻量级锁的核心思想是在线程获取锁时只是简单标记一下锁被当前线程获取而在释放锁时再将标记移除
  • 如果当前线程持有轻量级锁期间出现了锁竞争情况那么轻量级锁会退化为重量级锁

jvm轻量级锁和偏向锁实现都使用到了对象头我们来看一下:
在这里插入图片描述
轻量级锁实现如下:

在代码即将进入同步块的时候如果此同步对象没有被锁定锁标志位为“01”状态虚拟机首先将在当前线程的栈帧中建立一个名为锁记录Lock Record的空间用于存储锁对象目前的Mark Word的拷贝官方为这份拷贝加了一个Displaced前缀即Displaced Mark Word这时候线程堆栈与对象头的状态如图所示
在这里插入图片描述
然后虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了即代表该线程拥有了这个对象的锁并且对象Mark Word的锁标志位Mark Word的最后两个比特将转变为“00”表示此对象处于轻量级锁定状态。这时候线程堆栈与对象头的状态如图所示:
在这里插入图片描述
如果这个更新操作失败了那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧如果是说明当前线程已经拥有了这个对象的锁那直接进入同步块继续执行就可以了否则就说明这个锁对象已经被其他线程抢占了。如果出现两条以上的线程争用同一个锁的情况那轻量级锁就不再有效必须要膨胀为重量级锁锁标志的状态值变为“10”此时Mark Word中存储的就是指向重量级锁互斥量的指针后面等待锁的线程也必须进入阻塞状态。


偏向锁

轻量级锁可以在没有线程竞争的情况下避免创建对应的监视器对象但是如果锁总是被一个线程获取那么就没有必要在获取锁前打上标记而释放锁前撤销标记了可以只打一次标记如果下次还是这个同一个线程来获取锁那么就没必要重复进行打标记和释放标记了。

上述锁的实现思路被称为偏向锁偏的意思就是锁一直没有被其他线程获取那么持有偏向锁的线程永远不需要再进行同步。

偏向锁原理如下:

当锁对象第一次被线程获取的时候虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”表示进入偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中。如果CAS操作成功持有偏向锁的线程以后每次进入这个锁相关的同步块时虚拟机都可以不再进行任何同步操作例如加锁、解锁及对Mark Word的更新操作等。

一旦出现另外一个线程去尝试获取这个锁的情况偏向模式就马上宣告结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向偏向模式设置为“0”撤销后标志位恢复到未锁定标志位为“01”或轻量级锁定标志位为“00”的状态后续的同步操作就按照上面介绍的轻量级锁那样去执行。偏向锁、轻量级锁的状态转化及对象Mark Word的关系如图所示

在这里插入图片描述
在Java语言里面一个对象如果计算过哈希码就应该一直保持该值不变强烈推荐但不强制因为用户可以重载hashCode方法按自己的意愿返回哈希码否则很多依赖对象哈希码的API都可能存在出错风险。而作为绝大多数对象哈希码来源的ObjecthashCode方法返回的是对象的一致性哈希码Identity Hash Code这个值是能强制保证不变的它通过在对象头中存储计算结果来保证第一次计算之后再次调用该方法取到的哈希码值永远不会再发生改变。

因此当一个对象已经计算过一致性哈希码后它就再也无法进入偏向锁状态了而当一个对象当前正处于偏向锁状态又收到需要计算其一致性哈希码请求时它的偏向状态会被立即撤销并且锁会膨胀为重量级锁。

在重量级锁的实现中对象头指向了重量级锁的位置代表重量级锁的ObjectMonitor类里有字段可以记录非加锁状态标志位为“01”下的Mark WVord其中自然可以存储原来的哈希码。

偏向锁可以提高带有同步但无竞争的程序性能但它同样是一个带有效益权衡Trade Off性质的优化也就是说它并非总是对程序运行有利。如果程序中大多数的锁都总是被多个不同的线程访问那偏向模式就是多余的。在具体问题具体分析的前提下有时候使用参数-XX—UseBiasedLocking来禁止偏向锁优化反而可以提升性能。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Java