• 聊聊JAVA中的锁优化锁升级及其底层原理剖析



    在这里插入图片描述

    1. 基础介绍

    java中的锁

    在Java中,锁是实现多线程并发控制的一种重要机制。它可以保证多个线程之间安全地访问共享资源,防止数据的不一致性。锁有两种类型:内部锁和外部锁。内部锁是通过synchronized实现的,它可以解决方法或代码块在多线程环境下的同步问题。外部锁则是通过ReentrantLock等类实现的,除了能解决同步问题外,还提供了更多高级功能,如公平锁、非公平锁、条件等待/通知等。

    什么是锁优化和锁升级

    锁优化是为了提高系统的并发性能,包括减少锁的竞争、减小锁的粒度、减少锁的持有时间等,以减少线程等待锁的时间,提高系统的吞吐量。
    锁升级是指,当锁的竞争情况变得激烈时,JVM会将锁的状态由轻量级锁升级为重量级锁,以保证线程安全。同样,当锁的竞争情况减轻时,JVM也会将锁的状态由重量级锁降级为轻量级锁,以提高系统的并发性能。
    锁优化和锁升级的重要性在于,它们可以在保证线程安全的前提下,提高系统的并发性能,从而提高系统的吞吐量。在高并发的系统中,锁优化和锁升级是不可或缺的。

    2. Java中的锁升级过程及底层原理

    1. 偏向锁实现机制和原理

    偏向锁是Java 6引入的一种新的锁优化策略。它的主要思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时记录下这个线程的信息,当这个线程再次请求锁时,无需再做任何同步操作。这样做的目的是为了消除无竞争的同步原语,进一步提高程序的运行性能。如果有其他线程尝试获取这个锁,那么偏向模式就会被关闭。偏向锁适用于只有一个线程访问同步块的场景。

    1. 偏向锁的原理

    偏向锁的核心思想是,在无竞争的情况下,把整个同步消除掉。也就是说,如果一个锁只被一个线程锁定,而没有其他线程来竞争这个锁,那么这个锁就会偏向于这个线程,从而消除这个锁的同步操作。

    2. 偏向锁的底层实现

    偏向锁的底层实现,主要是通过在对象头中的Mark Word里存储偏向的线程ID来实现的。当线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID。如果下次该线程再次尝试获取这个锁,由于检查到锁对象已经偏向于自己,所以无需再做任何同步操作。只有当其他线程尝试获取这个锁时,应用程序才需要做出真正的同步操作,例如撤销偏向锁,升级为轻量级锁等。

    2. 轻量级锁

    轻量级锁是用来提高线程并发性的一种锁优化策略,主要针对锁的竞争不激烈的场合。轻量级锁相比于重量级锁(即操作系统层面的锁),其等待是通过自旋实现的,不会将线程状态置为阻塞,从而减少了不必要的线程上下文切换。

    1. 轻量级锁的原理

    当一个线程尝试获取某个轻量级锁时,它首先会检查这个锁是否处于偏向状态,如果不是,它会在自己的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后尝试使用CAS操作将对象头中的Mark Word替换为指向锁记录的指针。如果成功,那么这个线程就持有了这个轻量级锁。如果失败,那么它会检查对象头中的Mark Word是否指向自己的锁记录,如果是,那么它就成功地获取到了轻量级锁;否则,它会自旋等待或者升级为重量级锁。

    2. 轻量级锁的底层实现

    轻量级锁的底层实现主要依赖于CAS操作和自旋技术。CAS操作是用于实现非阻塞性同步的基础,它可以在无锁的情况下保证共享数据的同步;自旋则是为了避免线程在获取不到锁时,立即进入阻塞状态,而是进行几个轮次的循环尝试,看是否能够获取到锁,从而减少线程上下文切换的开销。

    3. 重量级锁

    重量级锁是Java中最传统的synchronized锁,是一种互斥锁,它是依赖于JVM和操作系统的线程调度机制。当一个线程获得一个对象的重量级锁后,其他任何线程都无法再获得该对象的锁,如果还有线程尝试获取该对象的锁,就会被阻塞住,直到锁的所有者线程释放该锁。

    1. 重量级锁的原理

    当一个线程尝试获取一个对象的重量级锁时,如果该对象的锁已经被其他线程持有,那么该线程就会被阻塞,即不会继续执行,进入阻塞状态。直到持有锁的线程释放了锁,JVM才会从被阻塞的线程中选择一个,将锁分配给它,其他的线程仍然保持阻塞状态。

    2. 重量级锁的底层实现

    重量级锁的底层主要依赖于操作系统的Mutex Lock(互斥锁)来实现,当一个线程获取不到锁时,它会进入阻塞状态,等待锁的释放。在这个过程中,涉及到操作系统用户模式和内核模式的转换,以及线程的调度和切换,这些都是需要消耗大量系统资源的操作,因此称为“重量级锁”。

    适用场景:当锁的竞争非常激烈,即锁保护的代码经常会被多个线程同时执行时,重量级锁的开销反而相对较小。因为这种情况下线程如果不进入阻塞状态,而是采用自旋等待锁的释放,不仅会占用CPU资源,而且由于锁的竞争激烈,线程获取锁的成功率很低,因此重量级锁更加适合。

    3. Java中锁升级的详细过程剖析

    1. 锁升级的触发条件

    锁的升级主要有以下几种触发条件:

    1. 当一个线程持有偏向锁,而另一个线程也试图获取这个锁的时候,偏向锁就会升级为轻量级锁。
    2. 当一个线程持有轻量级锁,而另一个线程也试图获取这个锁的时候,轻量级锁就会升级为重量级锁。
    3. 当线程进行了一定次数的自旋尝试,但仍然无法获取轻量级锁,这个时候就会升级为重量级锁。

    2. 偏向锁、轻量级锁、重量级锁之间的转换过程

    偏向锁升级为轻量级锁:当一个线程获取了偏向锁,而另一个线程也试图获取这个锁的时候,持有偏向锁的线程会被挂起,JVM会撤销偏向锁,然后把锁升级为轻量级锁。

    轻量级锁升级为重量级锁:当轻量级锁竞争失败时,如果自旋等待也不能获取到锁,这个时候就会把轻量级锁升级为重量级锁。此时,没有获取到锁的线程会进入阻塞状态,等待锁释放。

    3. 锁升级过程的具体实例分析

    例如有两个线程A和B,它们要竞争同一个锁。开始时,该锁是偏向锁状态,线程A首先获取到了这个偏向锁,然后线程B也试图获取这个锁,这时偏向锁就会升级为轻量级锁,线程A会被挂起。然后线程B开始自旋等待,试图获取轻量级锁,如果自旋等待成功,那么线程B就获取到了轻量级锁;如果自旋等待失败,那么轻量级锁就会升级为重量级锁,线程B会进入阻塞状态,等待锁释放。

    Java代码示例0
    这段代码的运行先后顺序能够模拟出锁的升级过程。首先,线程1获取到偏向锁;然后,线程2尝试获取锁,这会导致偏向锁升级为轻量级锁;最后,线程3尝试获取锁,这会导致轻量级锁升级为重量级锁。

    偏向锁和轻量级锁的升级过程是在Java HotSpot VM中的实现,不同的虚拟机实现可能会有所不同。代码中的sleep方法只是为了模拟锁的升级过程,真实的锁升级过程是在JVM内部进行的。

    public class LockUpgradeDemo {
        private static Object lock = new Object();
        
        public static void main(String[] args) throws InterruptedException {
            // 线程1获取偏向锁
            new Thread(() -> {
                synchronized (lock) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
            
            // 确保线程1获取偏向锁
            Thread.sleep(100);
            
            // 线程2尝试获取锁会导致偏向锁升级为轻量级锁
            new Thread(() -> {
                synchronized (lock) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
            
            // 确保线程2开始运行
            Thread.sleep(100);
            
            // 线程3尝试获取锁会导致轻量级锁升级为重量级锁
            new Thread(() -> {
                synchronized (lock) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    4. Java中的锁优化

    1. 锁优化的方法

    1. 减少锁的持有时间:只在必要的时候持有锁,尽量缩短锁的持有时间,从而减少线程阻塞的可能性。

    2. 减少锁的粒度:使用更细粒度的锁,例如,如果只有一个线程会访问一个对象的某个字段,那么就不需要对整个对象加锁。

    3. 锁分离:如果一个类有多个独立的操作,那么可以为每个操作使用不同的锁,这样就可以避免不必要的同步。

    4. 锁粗化:如果一个线程在一段时间内会多次获取和释放同一个锁,那么JVM可能会尝试将这些操作合并为一次,这就是锁粗化。

    5. 锁消除:如果JVM检测到一段代码中的锁操作是不必要的,那么可能会消除这个锁操作。

    6. 使用无锁数据结构:例如,Java的java.util.concurrent包中提供了许多无锁数据结构,如ConcurrentHashMap、CopyOnWriteArrayList等。

    7. 使用读写锁:如果一个数据结构的读操作比写操作更频繁,那么使用读写锁可以提高性能。

    8. 使用偏向锁和轻量级锁:JVM在1.6之后引入了偏向锁和轻量级锁,用于优化无竞争的同步代码,可以有效减少无必要的重量级锁操作。

    我们本章节主要了解核心的两种锁消除和锁粗化

    2. 锁消除

    什么是锁消除

      锁消除是Java中的一种锁优化技术。优化后的代码可以避免因为竞争锁而导致的线程阻塞,从而提高系统的运行性能。
    
    • 1

    锁消除的原理:

    锁消除的主要原理是在编译阶段,通过一种叫做逃逸分析的技术,分析对象的作用域,发现一些在多线程环境下不可能存在共享资源竞争的情况,从而消除不必要的同步措施。

    举个例子,如果某个对象只在一个线程的作用域内使用,那么它就不可能被其他线程访问到,因此,这个对象上的synchronized关键字就没有任何实际的意义,可以被安全地删除。

    锁消除的实例剖析:

    public class LockElimination {
        public void append(String str1, String str2){
            StringBuffer sb = new StringBuffer();
            sb.append(str1).append(str2);
            System.out.println(sb.toString());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这个例子中,StringBuffer是线程安全的,内部的append方法是加了synchronized关键字的。但是在append方法中,sb这个对象只会在当前线程中使用,其他线程无法访问到这个对象,因此sb对象上的synchronized关键字实际上是没有必要的。在运行时,JVM会自动消除这个锁,提高程序的性能。

    3. 锁粗化

    什么是锁粗化

    锁粗化是Java中的另一种锁优化策略,主要用于减少线程获取和释放锁的次数。

    锁粗化的原理:

    锁粗化的基本思想是将多个连续的加锁、解锁操作合并为一次,将加锁的同步范围扩大到其外部。这样,可以减少同步的次数,提高性能。当然,锁粗化的前提是,必须保证扩大后的同步代码块的执行不会影响到程序的并行度。

    锁粗化的实例剖析:

    我们举个栗子

    public class LockCoarsening {
        private StringBuffer sb = new StringBuffer();
    
        public void append(String str){
            for (int i = 0; i < 10000; i++) {
                sb.append(str);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这个例子中,每次调用append方法时,都会连续进行10000次加锁和解锁操作。这种情况下,JVM会自动进行锁粗化,将这10000次加锁操作合并为一次,将锁的范围扩大到整个append方法。这样做可以显著减少锁的竞争,提高程序的性能。

    5. 参考文档

    1. 《深入理解Java虚拟机:JVM高级特性与最佳实践》 - 周志明

    2. https://www.baeldung.com/java-synchronized

  • 相关阅读:
    C#:实现BinaryInsertionSorter折半插入排序算法(附完整源码)
    【路径规划】基于梯度下降算法求解自定义起点终点障碍路径规划问题附matlab代码
    POJ 2836 Rectangular Covering 状态压缩DP(铺砖问题)
    字词拼音查询易语言代码
    从零开始:制作出色的产品原型图的详细教程
    信息系统项目管理师(高项)—学习笔记
    【架构师视角系列】Apollo配置中心之Client端(二)
    2022秋-Java-03-面向对象1(基础、封装)——6-1 分数【函数题】
    Linear、Logistic回归
    MxSrvs pcntl 扩展
  • 原文地址:https://blog.csdn.net/wangshuai6707/article/details/133306554