进阶笔录-深入理解Java线程之Synchronized

深入理解Java线程之Synchronized

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

问题:两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

public class SyncDemo {

    private static volatile int counter = 0;

    public static void increment() {
        counter++;
    }

    public static void decrement() {
        counter--;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                increment();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                decrement();
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        //思考: counter=?
        log.info("counter={}", counter);


    }
}

问题分析

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

我们可以查看 i++和 i–(i 为静态变量)的 JVM 字节码指令 ( 可以在idea中安装一个jclasslib 插件)

//increment
0 getstatic #2 <com/apexl/jucdemo/jucdemo/sync/SyncDemo.counter : I>
3 iconst_1
4 iadd
5 putstatic #2 <com/apexl/jucdemo/jucdemo/sync/SyncDemo.counter : I>
8 return
//decrement
0 getstatic #2 <com/apexl/jucdemo/jucdemo/sync/SyncDemo.counter : I>
3 iconst_1
4 isub
5 putstatic #2 <com/apexl/jucdemo/jucdemo/sync/SyncDemo.counter : I>
8 return

如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题。 但多线程下这 8 行代码可能交错运行

调用过程:
在这里插入图片描述

临界区( Critical Section)

  • 一个程序运行多个线程本身是没有问题的

  • 问题出在多个线程访问共享资源

      • 多个线程读共享资源其实也没有问题
      • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,其共享资源为临界资源

//临界资源
private static int counter = 0;

public static void increment() { //临界区
    counter++;
}

public static void decrement() {//临界区
    counter--;

竞态条件( Race Condition )

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

为了避免临界区的竞态条件发生,有多种手段可以达到目的:

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

注意:

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码

同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

synchronized的使用

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

加锁方式:

在这里插入图片描述

解决之前的共享问题:

方式一:

public static synchronized void increment() {
    counter++;
}

public static synchronized void decrement() {
    counter--;
}

方式二:

private static String lock = "";

public static void increment() {
    synchronized (lock){
        counter++;
    }
}

public static void decrement() {
    synchronized (lock) {
        counter--;
    }

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

在这里插入图片描述

synchronized 什么时候释放锁

任何线程进入同步代码块、同步方法之前,必须获得同步监视器的锁定,那么何时会释放这个锁定呢?在程序中,是无法显式释放对同步监视器的锁的,而会在如下几个情况下释放锁。

1、当前线程的同步方法、代码块执行结束的时候释放

2、当前线程在同步方法、同步代码块中遇到break 、 return 终于该代码块或者方法的时候释放。

3、。。。。出现未处理的error或者exception导致异常结束的时候释放

4、。。。。程序执行了 同步对象 wait 方法 ,当前线程暂停,释放锁

如下情况不会释放锁

1、。。。。。。程序调用 Thread.sleep() Thread.yield() 这些方法暂停线程的执行,不会释放。

2、线程执行同步代码块时,其他线程调用 suspend 方法将该线程挂起,该线程不会释放锁 ,所以我们应该避免使用 suspend 和 resume 来控制线程

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中实现管程技术的组成部分。

MESA模型

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

在这里插入图片描述

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

入口等待队列:只有一个线程能进来,其他阻塞

条件队列:需要配合wait/notify,可以有多个

wait()的正确使用姿势

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

while(条件不满足) {
  wait();
  }

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

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

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

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

Java语言的内置管程synchronized

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

在这里插入图片描述

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;

在这里插入图片描述

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

synchronized加锁加在对象上,锁对象是如何记录锁状态的?

答:Java在对象的在内存的存储上有相应的空间来记录锁状态,锁状态被记录在每个对象头的Mark Word中

对象的内存布局

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

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

HotSpot虚拟机的对象头包括:

  • 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字节

在这里插入图片描述

可以使用JOL工具查看内存布局

<!-- 查看Java 对象布局、大小工具 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
<dependency>
    
public static void main(String[] args) throws InterruptedException {
    Object obj = new Object();
    //查看对象内部信息
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

偏向锁|轻量级锁|重量级锁

Mark Word中各种锁标记
在这里插入图片描述

偏向锁

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

开始只有偏向状态,真正new一个线程的时候才会 指向锁偏向的线程。

/***StringBuffer内部同步***/
public synchronized int length() { 
   return count; 
} 
//System.out.println 无意识的使用锁 
public void println(String x) { 
  synchronized (this) {
     print(x); newLine(); 
  } 

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

偏向锁延迟偏向

偏向锁模式存在偏向锁延迟机制:HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。(如果使用了偏向锁,会导致在锁竞争条件下升级,偏向锁会撤销,而撤销会带来一定的开销)

//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay=0
//禁止偏向锁
-XX:-UseBiasedLocking 
//启用偏向锁
public class LockEscalationDemo{

    public static void main(String[] args) throws InterruptedException {

        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
        Thread.sleep(5000);
        Object obj = new Object();
        // 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗
        //obj.hashCode();
        log.debug(ClassLayout.parseInstance(obj).toPrintable());

        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj){
                    // 思考:偏向锁执行过程中,调用hashcode会发生什么?
                    //obj.hashCode();
//                    //obj.notify();
                    try {
                        obj.wait(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
                            +ClassLayout.parseInstance(obj).toPrintable());
                }
//                try {
//                    Thread.sleep(2000);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
                log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
        },"thread1").start();

偏向锁撤销之调用对象HashCode

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

  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode

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

  • 当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁;
  • 当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁。

偏向锁撤销,升级成为轻量级锁

偏向锁撤销之调用wait/notify

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

偏向锁释放后一直偏向

发现偏向锁释放锁后不会变成无锁状态,而是一直偏向,就算在升级成轻量级锁前也还是处于偏向状态

轻量级锁

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

轻量级锁跟踪

public class LockEscalationDemo{

    public static void main(String[] args) throws InterruptedException {

        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
        Thread.sleep(5000);
        Object obj = new Object();
        // 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗
//        obj.hashCode();
        log.debug(ClassLayout.parseInstance(obj).toPrintable());

        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj){
                    // 思考:偏向锁执行过程中,调用hashcode会发生什么?
                    //obj.hashCode();
//                    obj.notify();
                   try {
                        obj.wait(50);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                  }

                    log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
                            +ClassLayout.parseInstance(obj).toPrintable());
                }
//                try {
//                    Thread.sleep(2000);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
                log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
        },"thread1").start();


        //控制线程竞争时机
        Thread.sleep(1);

        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj){

                    log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
                            +ClassLayout.parseInstance(obj).toPrintable());
                }
                log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
        },"thread2").start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj){

                    log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
                            +ClassLayout.parseInstance(obj).toPrintable());
                }
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
        },"thread3").start();

锁降级不存在,轻量级锁以及重量级锁释放锁只会变成无锁状态

轻量级锁膨胀为重量级锁

 //控制线程竞争时机
       // Thread.sleep(1);
 new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj){

                    log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
                            +ClassLayout.parseInstance(obj).toPrintable());
                }
                log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
        },"thread2").start();

        Thread.sleep(5000);
        log.debug(ClassLayout.parseInstance(obj).toPrintable());

在这里插入图片描述

锁对象状态转换

在这里插入图片描述

误区:

关于偏向锁轻量级锁重量

  1. 无锁–>偏向锁–>轻量级锁–>重量级锁(不存在这样的升级方式,偏向锁可以变为无锁、轻量级多、重量级锁)

  2. 轻量级锁自旋获取锁失败,会膨胀升级为重量级锁(错误,轻量级锁不存在自旋,一次CAS不成功后偏向锁会撤销变为轻量级锁)

  3. 重量级锁不存在自旋(重量级锁存在自旋)

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)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操 作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。 批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适 的。

JVM的默认参数值

设置JVM参数-XX:+PrintFlagsFinal,在项目启动时即可输出JVM的默认参数值

我们可以通过-XX:BiasedLockingBulkRebiasThreshold 和 - XX:BiasedLockingBulkRevokeThreshold 来手动设置阈值

intx BiasedLockingBulkRebiasThreshold = 20 //默认偏向锁批量重偏向阈值
intx BiasedLockingBulkRevokeThreshold = 40 //默认偏向锁批量撤销阈值

批量撤销代码测试:

当撤销偏向锁阈值超过 40 次后,jvm 会认为不该偏向,于是整个类的所有对象都会变为不可 偏向的,新建的对象也是不可偏向的。 注意:时间-XX:BiasedLockingDecayTime=25000ms范围内没有达到40次,撤销次数清为0, 重新计时

public class BiasedLockingTest {
    public static void main(String[] args) throws  InterruptedException {
        //延时产生可偏向对象
        Thread.sleep(5000);
        // 创建一个list,来存放锁对象
        List<Object> list = new ArrayList<>();
        
        // 线程1
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                // 新建锁对象
                Object lock = new Object();
                synchronized (lock) {
                    list.add(lock);
                }
            }
            try {
                //为了防止JVM线程复用,在创建完对象后,保持线程thead1状态为存活
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thead1").start();

        //睡眠3s钟保证线程thead1创建对象完成
        Thread.sleep(3000);
        log.debug("打印thead1,list中第20个对象的对象头:");
        log.debug((ClassLayout.parseInstance(list.get(19)).toPrintable()));
        
        // 线程2
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                Object obj = list.get(i);
                synchronized (obj) {
                    if(i>=15&&i<=21||i>=38){
                        log.debug("thread2-第" + (i + 1) + "次加锁执行中\t"+
                                ClassLayout.parseInstance(obj).toPrintable());
                    }
                }
                if(i==17||i==19){
                    log.debug("thread2-第" + (i + 1) + "次释放锁\t"+
                            ClassLayout.parseInstance(obj).toPrintable());
                }
            }
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thead2").start();


        Thread.sleep(3000);

        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                Object lock =list.get(i);
                if(i>=17&&i<=21||i>=35&&i<=41){
                    log.debug("thread3-第" + (i + 1) + "次准备加锁\t"+
                            ClassLayout.parseInstance(lock).toPrintable());
                }
                synchronized (lock){
                    if(i>=17&&i<=21||i>=35&&i<=41){
                        log.debug("thread3-第" + (i + 1) + "次加锁执行中\t"+
                                ClassLayout.parseInstance(lock).toPrintable());
                    }
                }
            }
        },"thread3").start();


        Thread.sleep(3000);
        log.debug("查看新创建的对象");
        log.debug((ClassLayout.parseInstance(new Object()).toPrintable()));

        LockSupport.park();

    }
}

总结:

  1. 批量重偏向和批量撤销是针对类的优化,和对象无关。

  2. 偏向锁重偏向一次之后不可再次重偏向。

  3. 当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类 的新实例对象使用偏向锁的权利

自旋优化

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

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

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

锁粗化

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

tringBuffer buffer = new StringBuffer();
/**
 * 锁粗化
 */
public void append(){
    buffer.append("aaa").append(" bbb").append(" ccc");

​ 上述代码每次调用 buffer.append 方法都需要加锁和解锁,如果JVM检测到有一连串的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

锁消除

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

public class LockEliminationTest {

    StringBuffer buffer = new StringBuffer();
    /**
     * 锁粗化
     */
    public void append(){
        buffer.append("aaa").append(" bbb").append(" ccc");
    }

    /**
     * 锁消除
     * -XX:+EliminateLocks 开启锁消除(jdk8默认开启)
     * -XX:-EliminateLocks 关闭锁消除
     * @param str1
     * @param str2
     */
    public void append(String str1, String str2) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(str1).append(str2);
    }

    public static void main(String[] args) throws InterruptedException {
        LockEliminationTest demo = new LockEliminationTest();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            demo.append("aaa", "bbb");
        }
        long end = System.currentTimeMillis();
        System.out.println("执行时间:" + (end - start) + " ms");
    }

}
//开启锁粗化 执行时间:2242 ms
//未开启 执行时间:4240 ms                       

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

逃逸分析(Escape Analysis)

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

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

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

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

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

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

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

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

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

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

-XX:+DoEscapeAnalysis  //表示开启逃逸分析 (jdk1.8默认开启)
-XX:-DoEscapeAnalysis //表示关闭逃逸分析。
-XX:+EliminateAllocations   //开启标量替换(默认打开)

代码测试:

/**
* 进行两种测试
 * 关闭逃逸分析,同时调大堆空间,避免堆内GC的发生,如果有GC信息将会被打印出来
 * VM运行参数:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
 *
 * 开启逃逸分析  jdk8默认开启
 * VM运行参数:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
 *
 * 执行main方法后
 * jps 查看进程
 * jmap -histo 进程ID
 *
 */
@Slf4j
public class EscapeTest {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 500000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        log.info("执行时间:" + (end - start) + " ms");
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
    }


    /**
     * JIT编译时会对代码进行逃逸分析
     * 并不是所有对象存放在堆区,有的一部分存在线程栈空间
     * Ponit没有逃逸
     */
    private static String alloc() {
        Point point = new Point();
        return point.toString();
    }

    /**
     *同步省略(锁消除)  JIT编译阶段优化,JIT经过逃逸分析之后发现无线程安全问题,就会做锁消除
     */
    public void append(String str1, String str2) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(str1).append(str2);
    }

    /**
     * 标量替换
     *
     */
    private static void test2() {
        Point point = new Point(1,2);
        System.out.println("point.x="+point.getX()+"; point.y="+point.getY());

//        int x=1;
//        int y=2;
//        System.out.println("point.x="+x+"; point.y="+y);
    }


}

@Data
@AllArgsConstructor
@NoArgsConstructor
class Point{
    private int x;
    private int y;

}

运行后使用jps,jmap工具可以看到point对象的实例化个数远远没有500000个,这是因为JTI逃逸分析后优化了,是的大量对象不在堆中分配而是在栈中分配

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值