zoukankan      html  css  js  c++  java
  • synchronized详解

    多线程编程中,有可能会出现多个线程同时访问同一个共享可变资源的情况;这种资源可能是:对象、变量、文件等。

    由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问,那么我们怎么解决线程并发安全问题?

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

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

    synchronized 关键字:对于不同线程之间看到的是有序的,但是对于synchronized代码块之内的代码有可能会发生指令重排。

    一、锁

    1、同步器的本质就是加锁

    加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)

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

    2、锁类型

    隐式锁:Synchronized加锁机制是Jvm内置锁,不需要手动加锁与解锁Jvm会自动加锁跟解锁。

    显式锁:Lock;例如:ReentrantLock,实现juc里的Lock接口,实现是基于AQS实现,需要手动加锁跟解锁ReentrantLock lock(), unlock();

    3、锁体系

    (1)synchronized的锁升级:无锁,偏向锁,轻量级锁,重量级锁; 

    (2)共享锁:读锁; 排他锁:写锁;

    二、synchronized原理详解  

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

    1、加锁方式

    (1)同步静态方法:锁是类对象

    (2)同步普通方法:锁是实例对象;(在spring容器中,Bean必须是单例的,否则加的synchronized没有什么作用)

    (3)同步代码块: 锁是括号里面的对象;

    扩展:使用其他方式加锁:

    (1)UnsafeInstance.reflectGetUnsafe().monitorEnter(obj); 和 UnsafeInstance.reflectGetUnsafe().monitorExit(obj);

    (2)ReentrantLock;

    2、synchronized底层原理  

      synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。

      当然,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。

    synchronized关键字被编译成字节码后会被翻译成 monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置。

     每一个对象被创建之后,都会在JVM内部维护一个与之对应的monitor监视器锁(ObjectMonitor对象)。当线程并发的去访问同步的代码块时,碰到了 monitorenter 指令的时候,这些线程要先去竞争这个对象的monitor对象。

    ObjectMonitor对象的部分代码:

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

        锁状态是被记录在每个对象的对象头(Mark Word)中,下面我们一起认识一下对象的内存布局。

    对象的内存布局  

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

    • 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等;
    • 实例数据:即创建对象时,对象中成员变量,方法等;
    • 对齐填充:对象的大小必须是8字节的整数倍;

    对象头

    HotSpot虚拟机的对象头包括两部分信息,第一部分是“Mark Word”,用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。这些信息随着锁的膨胀升级而不同,以 32位JVM为例:

    实例对象内存中存储在哪?

    如果实例对象存储在堆区时:实例对象内存存在堆区,实例的引用存在栈上,实例的元数据class存在方法区或者元空间;

    Object实例对象一定是存在堆区的吗?

    不一定,如果实例对象没有线程逃逸行为。JIT会进行优化。

    逃逸分析

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

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

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

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

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

    /**
    * 进行两种测试
    * 关闭逃逸分析,同时调大堆空间,避免堆内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 class Demo{
        StringBuffer stb = new StringBuffer();
        
        public void test1() {
            stb.append("1");
            stb.append("2");
            stb.append("3");
            stb.append("4");
        }
    }

    StringBuffer 是线程安全的,它的 append() 方法上增加了 synchronized, test1() 方法中连续调用了4次 append() 方法,本应该是会有4次的加锁过程,但是JVM做了优化只需要做1次加锁,如下代码所示,这就是锁的粗化

    public void test1() {
        synchronized(this) {
            stb.append("1");
            stb.append("2");
            stb.append("3");
            stb.append("4");
        }
    }

    锁的清除

      消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。如下的 test2() 方法,每次的锁对象都会去 new,就不存在共享资源竞争关系了,JVM会对其优化自动去除锁。

    public void test2(){
        //jvm的优化,JVM不会对同步块进行加锁
        synchronized (new Object()) {
            //伪代码:很多逻辑
            //jvm会进行逃逸分析
            System.out.println("-----");
        }
    }

    三、JVM内置锁的优化升级

    JDK1.6版本之后对synchronized的实现进行了各种优化,如自旋锁、偏向锁和轻量级锁并默认开启偏向锁。

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

    关闭偏向锁:-XX:-UseBiasedLocking

      锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

    1、偏向锁(单线程访问的场景)

      在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

      对于锁竞争比较激烈的场合,就不能使用偏向锁了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,先升级为轻量级锁。

    2、轻量级锁(线程的竞争不激烈,线程交替执行)

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

    3、自旋锁

    自旋不会丢弃CPU的使用权

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

      自适应自旋:JDK1.7之后,JVM会根据上次拿到锁自旋的次数去进行调整。若第一次自旋的次数为10还没有拿到,则第二次可能会调整为8;若第一次自旋次数为10就拿到了,则第二次的自旋次数可能调整为13;

     四、锁的升级膨胀过程

     

     

     锁的升级膨胀:https://www.cnblogs.com/JonaLin/p/11571482.html

  • 相关阅读:
    (转)【web前端培训之前后端的配合(中)】继续昨日的故事
    ural(Timus) 1136. Parliament
    scau Josephus Problem
    ACMICPC Live Archive 6204 Poker End Games
    uva 10391 Compound Words
    ACMICPC Live Archive 3222 Joke with Turtles
    uva 10132 File Fragmentation
    uva 270 Lining Up
    【转】各种字符串哈希函数比较
    uva 10905 Children's Game
  • 原文地址:https://www.cnblogs.com/yufeng218/p/13028549.html
Copyright © 2011-2022 走看看