悦书阁 悦书阁
首页
学习笔记
技术文档
idea插件开发
更多
  • 分类
  • 标签
  • 归档

Felix

大道至简 知易行难
首页
学习笔记
技术文档
idea插件开发
更多
  • 分类
  • 标签
  • 归档
  • JVM

  • spring

  • 并发编程

    • 深入理解JMM和并发三大特性
    • CPU缓存一致性协议MESI
    • 深入理解synchronized
      • 并发锁机制
      • Java共享内存模型带来的线程安全问题
        • 临界区( Critical Section)
        • 竞态条件( Race Condition )
        • synchronized的使用
        • synchronized底层原理
        • Monitor(管程/监视器)
        • 对象的内存布局
        • 对象头详解
        • JOL工具查看内存布局
        • 偏向锁
        • 偏向锁撤销之调用对象HashCode
        • 偏向锁撤销之调用wait/notify
        • 轻量级锁
        • 重量级锁
        • 总结:锁对象状态转换
      • 锁升级的原理分析
      • synchronized锁优化
        • 偏向锁批量重偏向&批量撤销
        • 原理
        • 应用场景
        • JVM的默认参数值
        • 总结
        • 自旋优化(只有重量级锁才有自旋)
        • 锁粗化
        • 锁消除
        • 逃逸分析(Escape Analysis)
        • 方法逃逸(对象逃出当前方法)
        • 线程逃逸((对象逃出当前线程)
    • AQS之独占锁ReentrantLock
    • 深入理解AQS之Semaphorer&CountDownLatch
    • 深入理解AQS之CyclicBarrier
    • 深入理解AQS之ReentrantReadWriteLock
    • Collections之Map&List&Set详解
    • 阻塞队列BlockingQueue实战及其原理分析
    • 深入理解Java线程
    • Executor线程池原理与源码解读
    • 并发编程之定时任务&定时线程池
  • 消息中间件

  • 微服务

  • 三高架构

  • 学习笔记
  • 并发编程
liufei379
2022-07-13
目录

深入理解synchronized

# 并发锁机制

  • 加锁的目的:
    • 解决线程安全问题
      • 多线程资源共享
  • 加锁的效果:
    • 序列化的访问临界资源
  • java中如何实现锁
    • 阻塞
    • synchronized
    • reentrantLock
    • 非阻塞
      • cas+自旋

# Java共享内存模型带来的线程安全问题

@Slf4j
public class SyncDemo {

    private static int counter = 0;//临界资源
    public static void add() { //临界区
        counter++;
    }
    public static void del() { //临界区
        counter--;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                add();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                del();
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        log.info("{}", counter);
    }
}
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

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作。

查看JVM字节码指令可以看出,在执行add() getstatic i 的时候 可能由于线程上线文切换,然后执行了 del() 的 getstatic i ,这样就导致结果的不准确

getstatic i // 获取静态变量i的值 
iconst_1 // 将int常量1压入操作数栈
iadd // 自增 

getstatic i // 获取静态变量i的值 
iconst_1 // 将int常量1压入操作数栈
isub // 自减   
1
2
3
4
5
6
7

# 临界区( Critical Section)

  • 一个程序运行多个线程本身是没有问题的
  • 问题在于多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区(add() del()),其共享资源为临界资源(count)

# 竞态条件( Race Condition )

  • 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

  • 如何避免竞态条件发生

    • 阻塞式的解决方案:synchronized,Lock
    • 非阻塞式的解决方案:原子变量

# synchronized的使用

  • synchronized 同步块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内置锁,也叫作监视器锁。

  • synchronized 实际是用对象锁保证了临界区内代码的原子性

加锁方式

image-20220713135102144

# synchronized底层原理

synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量),它是一个重量级锁,性能较低。当然,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。

Java虚拟机通过一个同步结构支持方法和方法中的指令序列的同步:monitor。

同步方法是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;同步代码块是通过monitorenter和monitorexit来实现。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

# Monitor(管程/监视器)

Monitor,直译为“监视器”,而操作系统领域一般翻译为“管程”。管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。在Java 1.5之前,Java语言提供的唯一并发语言就是管程,Java 1.5之后提供的SDK并发包也是以管程为基础的。除了Java之外,C/C++、C#等高级语言也都是支持管程的。synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。

在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在正在广泛使用的是MESA模型。

image-20220714103530167

管程中每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。

wait()的正确使用姿势

对于MESA管程来说,有一个编程范式:

while (<condition does not hold>)
    obj.wait(timeout);
1
2

唤醒的时间和获取到锁继续执行的时间是不一致的,被唤醒的线程再次执行时可能条件又不满足了,所以循环检验条件。MESA模型的wait()方法还有一个超时参数,为了避免线程进入等待队列永久阻塞。

notify()和notifyAll()分别何时使用

满足以下三个条件时,可以使用notify(),其余情况尽量使用notifyAll():

  1. 所有等待线程拥有相同的等待条件;
  2. 所有等待线程被唤醒后,执行相同的操作;
  3. 只需要唤醒一个线程。

示例:

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ThreadA {
    public static void main(String[] args) {
        ThreadB b = new ThreadB();
        b.start();
        synchronized (b) {
            log.info("等待唤醒");
            try {
                b.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("被唤醒");
        }
    }
}

@Slf4j
class ThreadB extends Thread {

    @SneakyThrows
    @Override
    public void run() {
        synchronized (this) {
            Thread.sleep(5000);
            log.info("准备唤醒");
            notifyAll();
        }
    }
}
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

Java语言的内置管程synchronized

Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。模型如下图所示。

image-20220713163553397

Monitor机制在Java中的实现

java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于 ObjectMonitor 实现,这是 JVM 内部基于 C++ 实现的一套机制。

ObjectMonitor其主要数据结构如下(hotspot源码ObjectMonitor.hpp):

ObjectMonitor() {
    _header       = NULL; //对象头  markOop
    _count        = 0;  
    _waiters      = 0,   
    _recursions   = 0;   // 锁的重入次数 
    _object       = NULL;  //存储锁对象
    _owner        = NULL;  // 标识拥有该monitor的线程(当前获取锁的线程) 
    _WaitSet      = NULL;  // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
    _WaitSetLock  = 0 ;    
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
    FreeNext      = NULL ;
    _EntryList    = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

image-20220713164331438

在获取锁时,是将当前线程插入到cxq的头部,而释放锁时,默认策略(QMode=0)是:如果EntryList为空,则将cxq中的元素按原有顺序插入到EntryList,并唤醒第一个线程,也就是当EntryList为空时,是后来的线程先获取锁。_EntryList不为空,直接从_EntryList中唤醒线程。

# 对象的内存布局

Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

  • 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象才有)等。
  • 实例数据:存放类的属性数据信息,包括父类的属性信息;
  • 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

image-20220713172324112

# 对象头详解

  • Mark Word

    • 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,官方称它为“Mark Word”。
  • Klass Pointer

    • 对象头的另外一部分是klass类型指针,即对象指向它的类元(元空间)数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 32位4字节,64位开启指针压缩或最大堆内存<32g时4字节,否则8字节。jdk1.8默认开启指针压缩后为4字节,当在JVM参数中关闭指针压缩(-XX:-UseCompressedOops)后,长度为8字节。
  • 数组长度(只有数组对象有)

    • 如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度。 4字节

    image-20220713172539043

# JOL工具查看内存布局

<!-- 引入依赖 查看Java 对象布局、大小工具 -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>
1
2
3
4
5
6

查看对象内部信息

@Slf4j
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}
1
2
3
4
5
6
7

image-20220713173141480

  • OFFSET:偏移地址,单位字节;
  • SIZE:占用的内存大小,单位为字节;
  • TYPE DESCRIPTION:类型描述,其中object header为对象头;
  • VALUE:对应内存中当前存储的值,二进制32位;
  • 关闭指针压缩后,对象头为16字节:-XX:-UseCompressedOops

为什么说对象头,因为synchronized加锁加在对象上记录锁状态的

Hotspot通过markOop类型实现Mark Word,具体实现位于markOop.hpp文件中。由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间。目的是为了节约空间节省内存。

32位JVM下的对象结构描述

image-20220713182252578

64位JVM下的对象结构描述

image-20220713182315197

  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争时,JVM使用原子操作而不是OS互斥,这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的Mark Word中设置指向锁记录的指针。
  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针

锁标记枚举

image-20220713182441678

# 偏向锁

  • 偏向锁是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的优化效果。

  • 当JVM启用了偏向锁模式(jdk6默认开启),新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。

    image-20220714104854237

  • 为什么上图第一次打印锁无锁状态?第二次打印是偏向锁状态?

    • 因为偏向锁模式存在偏向锁延迟机制:HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式。
      • JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。
  • 偏向锁执行完成之后还是偏向锁,只有发生锁消除才有能变成 无锁状态,也有可能升级为 轻量锁 或 重量锁。

# 偏向锁撤销之调用对象HashCode

调用锁对象的obj.hashCode()或System.identityHashCode(obj)方法会导致该对象的偏向锁被撤销。因为对于一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存hashcode的。

注意

当对象处于可偏向(也就是线程ID为0)和已偏向的状态下,调用HashCode计算将会使对象再也无法偏向

  • 当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁;
  • 当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁。
  • 如果没有synchronized 锁对象,obj.hashCode() 之后,对象锁状态变为无锁

    image-20220714105431542

  • 如果有synchronized 锁对象,obj.hashCode()在synchronized 之外层,对象锁状态变为轻量锁

    @Slf4j
    public class Test {
    
        public static void main(String[] args) throws InterruptedException {
            //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
            Thread.sleep(4000);
            Object obj = new Object();
            //是否处于偏向锁通过是否持有线程ID来区分
            log.info("对象未开始偏向,偏向锁:" + ClassLayout.parseInstance(obj).toPrintable());
            obj.hashCode();
            log.info("对象未开始偏向,锁撤销 变为无锁:" + ClassLayout.parseInstance(obj).toPrintable());
            synchronized (obj) {
                log.info("升级为轻量锁:" + ClassLayout.parseInstance(obj).toPrintable());
            }
            Thread.sleep(2000); //注意点:重量锁释放需要 删除monitor对象 修改markwork需要花时间,如果不睡眠那么打印的还是重量级锁
            log.info("轻量锁释放后 变为 无锁:"  + ClassLayout.parseInstance(obj).toPrintable());
            Thread.sleep(5000);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    image-20220714145036642

  • 如果有synchronized 锁对象,obj.hashCode()在synchronized 方法块内,对象锁状态变为重量锁

    @Slf4j
    public class Test {
    
        public static void main(String[] args) throws InterruptedException {
            //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
            Thread.sleep(4000);
            Object obj = new Object();
            //是否处于偏向锁通过是否持有线程ID来区分
            log.info("对象未开始偏向,偏向锁:" + ClassLayout.parseInstance(obj).toPrintable());
            synchronized (obj) {
                log.info("对象正处于偏向,偏向锁:" + ClassLayout.parseInstance(obj).toPrintable());
                obj.hashCode();
                log.info("升级为重量锁:" + ClassLayout.parseInstance(obj).toPrintable());
            }
            Thread.sleep(2000); //注意点:重量锁释放需要 删除monitor对象 修改markwork需要花时间,如果不睡眠那么打印的还是重量级锁
            log.info("重量级锁释放后 变为 无锁:"  + ClassLayout.parseInstance(obj).toPrintable());
            Thread.sleep(5000);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    image-20220714143903516

# 偏向锁撤销之调用wait/notify

偏向锁状态执行obj.notify() 会升级为轻量级锁,调用obj.wait(timeout) 会升级为重量级锁

# 轻量级锁

  • 倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。

  • 轻量级锁代码块执行完成变为无锁状态

  • 轻量级锁不存在自旋 只是 CAS 成功 owner (markwork属性) 设置成 self

  • 轻量级锁在栈空间会创建锁记录 会把markword复制到栈空间

    @Slf4j
    public class Test {
    
        public static void main(String[] args) throws InterruptedException {
            //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
            Thread.sleep(4000);
            Object obj = new Object();
            new Thread(() -> {
                //1.如果 threa1 先抢占资源就是 匿名偏向
                log.info(Thread.currentThread().getName() + "可能匿名偏向(Thread Id 0)或偏向锁" + ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj) {
                    // 2.如果没有上下文 切换到 thread2 就是 偏向锁 否则升级为 轻量锁 或 重量锁
                    log.info(Thread.currentThread().getName() + "可能偏向锁 或 轻量锁 或 重量锁:" + ClassLayout.parseInstance(obj).toPrintable());
                }
                //3.如果没有上线问切换 则还是偏向锁 否则变为 无锁 但是重量锁 打印的 还是重量锁 ,因为释放monitor对象 和 修改 markword 需要时间
                log.debug(Thread.currentThread().getName() + "如果是轻量锁 或 重量锁 释放为无锁 如果偏向锁 则 还是偏向锁:" + ClassLayout.parseInstance(obj).toPrintable());
            }, "thread1").start();
    
            //控制线程竞争时机
            Thread.sleep(1);
    
            new Thread(() -> {
                log.info(Thread.currentThread().getName() + "可能匿名偏向(Thread Id 0)或偏向锁" + ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj) {
                    log.info(Thread.currentThread().getName() + "可能偏向锁 或 轻量锁 或 重量锁:" + ClassLayout.parseInstance(obj).toPrintable());
                }
                log.debug(Thread.currentThread().getName() + "如果是轻量锁 或 重量锁 释放为无锁 如果偏向锁 则 还是偏向锁:" + ClassLayout.parseInstance(obj).toPrintable());
            }, "thread2").start();
    
            Thread.sleep(5000);
        }
    }
    
    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

    image-20220714151720422

    image-20220714151757344

# 重量级锁

  • 轻量级锁竞争激烈会膨胀为重量级锁 (CAS获取锁,失败膨胀)
  • 重量级锁释放之后变为无锁
  • 重量级锁 会自旋
  • 重量级锁会创建monitor对象, markword会记录在monitor对象中
  • monitor对象在GC的时候回收

# 总结:锁对象状态转换

image-20220713184436891

# 锁升级的原理分析

待补充

# synchronized锁优化

# 偏向锁批量重偏向&批量撤销

从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到安全点(safe point)时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。

# 原理

​ 以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。

​ 每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。

当达到重偏向阈值(默认20)后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

# 应用场景
  • 批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。
    • 当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程,重偏向会重置对象 的 Thread ID
  • 批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。
    • 当撤销偏向锁阈值超过 40 次后,jvm 会认为不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。注意:时间-XX:BiasedLockingDecayTime=25000ms范围内没有达到40次,撤销次数清为0,重新计时
# JVM的默认参数值
  • 设置JVM参数-XX:+PrintFlagsFinal,在项目启动时即可输出JVM的默认参数值

    intx BiasedLockingBulkRebiasThreshold   = 20   //默认偏向锁批量重偏向阈值
    intx BiasedLockingBulkRevokeThreshold   = 40   //默认偏向锁批量锁撤销阈值
    
    1
    2
    # 总结
    1. 批量重偏向和批量撤销是针对类的优化,和对象无关。
    2. 偏向锁重偏向一次之后不可再次重偏向。
    3. 当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利 也就是说新实例都将变为无锁。

    # 自旋优化(只有重量级锁才有自旋)

    重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

    • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
    • 在 Java 6 之后自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能。
    • Java 7 之后不能控制是否开启自旋功能

    注意:自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)

# 锁粗化

​ 假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。

StringBuffer buffer = new StringBuffer();
/**
 * 锁粗化
 */
public void append(){
    buffer.append("aaa").append(" bbb").append(" ccc");
    ==
    synchonized(buffer) { 等于在这里加锁,内部不在加锁
    	buffer.append("aaa").append(" bbb").append(" ccc");
    }
 }
1
2
3
4
5
6
7
8
9
10
11

# 锁消除

  • 锁消除即删除不必要的加锁操作。锁消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。

  • 和偏向锁撤销没有关系

  • StringBuffer的append是个同步方法,但是append方法中的 StringBuffer 属于一个局部变量,不可能从该方法中逃逸出去,因此其实这过程是线程安全的,可以将锁消除。

    public class Test {
        /**
         * 锁消除
         * -XX:+EliminateLocks 开启锁消除(jdk8默认开启)
         * -XX:-EliminateLocks 关闭锁消除
         * @param str1
         * @param str2
         */
        public void append(String str1, String str2) {
            StringBuffer sb = new StringBuffer();
            sb.append(str1).append(str2);
        }
    
        public static void main(String[] args) throws InterruptedException {
            Test test = new Test();
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100000000; i++) {
                test.append("1", "2");
            }
            long end = System.currentTimeMillis();
            System.out.println("执行时间:" + (end - start) + " ms");
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    # 逃逸分析(Escape Analysis)

    逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。逃逸分析的基本行为就是分析对象动态作用域。

    # 方法逃逸(对象逃出当前方法)

    ​ 当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。

    # 线程逃逸((对象逃出当前线程)

    这个对象甚至可能被其它线程访问到,例如赋值给类变量或可以在其它线程中访问的实例变量。

    使用逃逸分析,编译器可以对代码做如下优化:

    • 同步省略或锁消除(Synchronization Elimination)。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

    • 将堆分配转化为栈分配(Stack Allocation)。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。

    • 分离对象或标量替换(Scalar Replacement)。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

    jdk6才开始引入该技术,jdk7开始默认开启逃逸分析。在Java代码运行时,可以通过JVM参数指定是否开启逃逸分析:

     -XX:+DoEscapeAnalysis //表示开启逃逸分析 (jdk1.8默认开启)
     -XX:-DoEscapeAnalysis //表示关闭逃逸分析。
     -XX:+EliminateAllocations  //开启标量替换(默认打开)              
    
    1
    2
    3
上次更新: 2026/3/11 22:17:56
CPU缓存一致性协议MESI
AQS之独占锁ReentrantLock

← CPU缓存一致性协议MESI AQS之独占锁ReentrantLock→

最近更新
01
实现idea开发的关键步骤
10-05
02
Redis高可用架构
09-09
03
Zookeeper高可用
08-31
更多文章>
Theme by Vdoing | Copyright © 2022-2026 Felix | 粤ICP备17101757号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式