synchronized详解

img

设计同步设计器的意义

多线程中,有可能出现多个线程同时访问同一个文件,可变资源的情况。这个资源简称“临界资源”(可能是对象、变量、文件等等)。由此可能引起的问题线程的执行过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问。

  • 共享:资源可以由多个线程同时访问
  • 可变:资源可以在其生命周期内被修改

解决线程并发安全问题

实际上所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源(即在同一时刻,只能有一个线程访问临界资源,也称为同步互斥访问)

Java中提供了,两种方式来实现同步互斥访问:synchronize和lock

同步器的本质就是加锁,加锁的目的就是(序列化访问临界资源——同步互斥访问)

当多个线程执行一个方法时,该方法的内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中的,不具有共享性,不会导致线程安全问题

synchronized原理详解

synchronized内置锁是一种对象锁(锁的对象而非引用),作用的粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。

加锁的方式:

  1. 同步实例方法,锁是当前实例对象
  2. 同步类方法,锁是当前类对象
  3. 同步代码块,锁是括号内对象

synchronized底层原理

synchronized 是基于JVM内置锁实现的,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。当然,JVM内置锁在1.5 之后版本做了重大的优化,如锁粗化Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,,内置锁的并发性能已经基本与 Lock持平。

Monitor监视器锁

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里实现的都是基于进入和退出Monitor对象来实现方法同步和代码块同步。

什么是Monitor

可以理解为Monitor是一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的JAVA对象是天生的Monitor。每一个JAVA对象都有成为Monitor的潜质,因为在Java的设计中,每一个Java对象自创建就自带一个锁(叫做内部锁或者Monitor锁),也就是通常说的Synchronized的对象锁。

在同步代码块中,监视器Monitor有两种方式:互斥与写作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。synchronized加锁在对象上,对象是如何记录锁状态的?(锁状态是被记录在每个对象的对象头Mark Work中)

对象的内存布局

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

img

  • **对象头:**比如Hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程ID),偏向时间,数组长度(数组对象)等等。JAVA对象头一般占有两个机器码(在32位虚拟机中,一个机器码等于4个字节,也就是32bit,在64位虚拟机中,一个机器码是8个字节,也就是64bit).但是如果对象是数组类型,则需要3个机器码但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
  • **实例数据:**存放类的属性数据信息,包括父类的属性信息。
  • **对齐填充:**由于虚拟机要求,对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
对象头

HotSpot虚拟机的对象头包括两部分信息,第一部分“Mark Word”,用于存储对象自身的运行时数据。如哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等等,它是实现轻量级锁和偏向锁的关键。这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额 外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。第二部分类型指针:即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

锁的膨胀升级过程

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。从JDK 1.6 中默认是开启偏向锁和轻量级锁的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。

img

偏向锁

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

默认开启偏向锁

开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

关闭偏向锁:-XX:-UseBiasedLocking

轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或 100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

锁消除

锁消除是虚拟机另外一种锁的优化,这种优化更加彻底,Java虚拟机在JIT编译时(可以简单的理解为当代码即将第一次被执行时进行编译,又称为即时编译)通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省无意义的锁请求时间。比如StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,所以StringBuffer不可能存在共享资源的竞争的情景,JVM会自动将其锁消除。锁消除的依据是逃逸分析的数据支持。

锁消除,前提是java必须运行在server模式(

server模式会比client模式作更多的优化),同时必须开启逃逸分析

:-XX:+DoEscapeAnalysis 开启逃逸分析

-XX:+EliminateLocks 表示开启锁消除。

逃逸分析

一、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。 二、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

在Java代码运行时,通过JVM参数可指定是否开启逃逸分析, -XX:+DoEscapeAnalysis : 表示开启逃逸分析 -XX:- DoEscapeAnalysis : 表示关闭逃逸分析。从jdk 1.7开始已经默认开启逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis

public class T0_ObjectStackAlloc {

    /**
     * 进行两种测试
     * 关闭逃逸分析,同时调大堆空间,避免堆内GC的发生,如果有GC信息将会被打印出来
     * VM运行参数:‐Xmx4G ‐Xms4G ‐XX:‐DoEscapeAnalysis ‐XX:+PrintGCDetails ‐XX:+HeapDumpOnOutOfMemoryError
     *
     * 开启逃逸分析
     * VM运行参数:‐Xmx4G ‐Xms4G ‐XX:+DoEscapeAnalysis ‐XX:+PrintGCDetails ‐XX:+HeapDumpOnOutOfMemoryError
     *
     * 执行main方法后
     * jps 查看进程
     * jmap -histo 进程ID
     */
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0;i < 500000;i++){
            alloc();
        }
        long end = System.currentTimeMillis();
        //查看运行时间
        System.out.println("cost-time " + (end - start) + "ms");
        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }

    private static TulingStudent alloc() {
        //Jit对编译时会对代码进行 逃逸分析
        //并不是所有对象存放在堆区。有的一部分存放在线程栈空间
        TulingStudent student = new TulingStudent();
        return student;

    }

    static class TulingStudent{
        private String name;

        private int age;
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值