[面试]synchronized

synchronized

把面试中遇到的问题进行了整理. 本篇文章copy+整理自:html

        1. http://www.cnblogs.com/lingepeiyong/archive/2012/10/30/2745973.htmljava

        2. http://www.cnblogs.com/paddix/p/5405678.html面试

        3. https://blog.csdn.net/javazejian/article/details/72828483数组

请描述synchronized底层语义以及原理

Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现安全

你是怎么知道monitorenter的?

用javap命令进行反编译. 好比有这样一个java源代码: Main.java数据结构

javac Main.java   

javap -c Main.class

 就能够看到反编译的代码了.多线程

方法级的synchronized也是根据monitor实现的吗?

方法级的同步是隐式,即无需经过字节码指令来控制的,它实如今方法调用和返回操做之中。JVM能够从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,若是设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 而后再执行方法,最后再方法完成(不管是正常完成仍是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其余任何线程都没法再得到同一个monitor。若是一个同步方法执行期间抛 出了异常,而且在方法内部没法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法以外时自动释放。app

描述一下等待唤醒机制与synchronized的联系?

所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,不然就会抛出IllegalMonitorStateException异常,这是由于调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,咱们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字能够获取 monitor ,这也就是为何notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的缘由。jvm

wait()和sleep的区别?

与sleep方法不一样的是, wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会立刻释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。性能

什么是虚假唤醒?

举个例子,咱们如今有一个生产者-消费者队列和三个线程。

1) 1号线程从队列中获取了一个元素,此时队列变为空。

2) 2号线程也想从队列中获取一个元素,但此时队列为空,2号线程便只能进入阻塞(cond.wait()),等待队列非空。

3) 这时,3号线程将一个元素入队,并调用cond.notify()唤醒条件变量。

4) 处于等待状态的2号线程接收到3号线程的唤醒信号,便准备解除阻塞状态,执行接下来的任务(获取队列中的元素)。

5) 然而可能出现这样的状况:当2号线程准备得到队列的锁,去获取队列中的元素时,此时1号线程恰好执行完以前的元素操做,返回再去请求队列中的元素,1号线程便得到队列的锁,检查到队列非空,就获取到了3号线程刚刚入队的元素,而后释放队列锁。

6) 等到2号线程得到队列锁,判断发现队列仍为空,1号线程“偷走了”这个元素,因此对于2号线程而言,此次唤醒就是“虚假”的,它须要再次等待队列非空。

描述一下Monitor中的内部队列的做用?

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每一个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其余线程进入获取monitor(锁)。以下图所示

哪些对象能够做为synchronized锁?

Java中任意对象能够做为锁. monitor对象存在于每一个Java对象的对象头中(存储的指针的指向),synchronized锁即是经过这种方式获取锁的,也是notify/notifyAll/wait等方法存在于顶级对象Object中的缘由(关于这点稍后还会进行分析)

通常而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(若是对象是数组则会分配3个字,多出来的1个字记录的是数组长度)

对象头的结构是什么样的?

Hotspot中对象在内存中的结构:

对象结构

从上面的这张图里面能够看出,对象在内存中的结构主要包含如下几个部分:

      1. Mark Word:对象的Mark Word部分占4个字节,其内容是一系列的标记位,好比轻量级锁的标记位,偏向锁标记位等等。

      2. Class对象指针:Class对象指针的大小也是4个字节,其指向的位置是对象对应的Class对象(其对应的元数据对象)的内存地址

      3. 对象实际数据:这里面包括了对象的全部成员变量,其大小由各个成员变量的大小决定,好比:byte和boolean是1个字节,short和char是2个字节,int和float是4个字节,long和double是8个字节,reference是4个字节

      4. 对齐:最后一部分是对齐填充的字节,按8个字节填充。

根据上面的图,那么咱们能够得出Integer的对象的结构以下:

Integer内存结构地址

Integer只有一个int类型的成员变量value,因此其对象实际数据部分的大小是4个字节,而后再在后面填充4个字节达到8字节的对齐,因此能够得出Integer对象的大小是16个字节。

所以,咱们能够得出Integer对象的大小是原生的int类型的4倍

关于对象的内存结构,须要注意数组的内存结构和普通对象的内存结构稍微不一样,由于数据有一个长度length字段,因此在对象头后面还多了一个int类型的length字段,占4个字节,接下来才是数组中的数据,以下图:

数组对象内存结构

对象头中的MarkWord 结构以下: 

锁状态

25 bit

4bit

1bit

2bit

23bit

2bit

是不是偏向锁

锁标志位

轻量级锁

指向栈中锁记录的指针

00

重量级锁

指向互斥量(重量级锁)的指针

10

GC标记

11

偏向锁

线程ID

Epoch

对象分代年龄

1

01

无锁

对象的hashCode

对象分代年龄

0

01

synchronized实现可重入的count存在哪儿?

ObjectMonitor在openjdk中的源码路径: openjdk/hotspot/src/share/vm/runtime/objectMonitor.hpp 

数据结构里有_count一项, 用于在重入时进行自增操做, 释放时进行自减操做.

  // initialize the monitor, exception the semaphore, all other fields
  // are simple integers or pointers
  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

Java虚拟机对synchronized的优化

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

自旋锁与自适应自旋

一般咱们称Sychronized锁是一种重量级锁,是由于在互斥状态下,没有获得锁的线程会被挂起阻塞,而挂起线程和恢复线程的操做都须要转入内核态中完成。同时,虚拟机开发团队也注意到,许多应用上的数据锁只会持续很短的一段时间,若是为了这段时间去挂起和恢复线程是不值得的,因此引入了自旋锁。所谓的自旋,就是让没有得到锁的线程本身运行一段时间的自循环,这就是自旋锁。自旋锁能够经过-XX:+UseSpinning参数来开启。
但这显然并非最好的一种方法,不挂起线程的代价就是该线程会一直占用处理器。若是锁被占用的时间很短,自旋等待的效果就会很好,反之,自旋会消耗大量处理器资源。所以,自旋的等待时间必须有必定的限度,若是超过限度尚未得到锁,就要挂起线程,这个限度默认是10次,可使用-XX:PreBlockSpin改变。
在JDK6之后又引入了自适应自旋锁,也就说自旋的时间限度不是一个固定值了,而是由上一次同一个锁的自旋时间及锁的拥有者状态来决定。虚拟机认为,若是同一个锁对象自旋刚刚成功得到锁,那么下一次极可能得到锁,因此容许此次自旋锁自旋很长时间、而若是某个锁不多得到锁,那么之后在获取锁的过程当中可能忽略到自旋过程。

偏向锁

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

偏向锁其实是一种锁优化的,其目的是为了减小数据在无竞争状况下的性能消耗。其核心思想就是锁会偏向第一个获取它的线程,在接下来的执行过程当中该锁没有其余的线程获取,则持有偏向锁的线程永远不须要再进行同步。

        偏向锁的获取

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里储存锁偏向的线程ID。之后该线程在进入和退出同步块时不须要进行CAS操做来加锁和解锁,只须要检查当前Mark Word中储存的线程是否指向当前线程,若是成功,表示已经得到对象锁;若是检测失败,则须要再测试一下Mark Word中偏向锁的标志是否已经被置为1(表示当前锁是偏向锁):若是没有则使用CAS操做竞争锁,若是设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

偏向锁获取过程:

  (1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。

  (2)若是为可偏向状态,则测试线程ID是否指向当前线程,若是是,进入步骤(5),不然进入步骤(3)。

  (3)若是线程ID并未指向当前线程,则经过CAS操做竞争锁。若是竞争成功,则将Mark Word中线程ID设置为当前线程ID,而后执行(5);若是竞争失败,执行(4)。

  (4)若是CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时得到偏向锁的线程被挂起,偏向锁升级为轻量级锁,而后被阻塞在安全点的线程继续往下执行同步代码。

  (5)执行同步代码。

        偏向锁的撤销

偏向锁使用一种等待竞争出现才释放锁的机制,因此当有其余线程尝试得到锁时,才会释放锁。偏向锁的撤销,须要等到安全点。它首先会暂停拥有偏向锁的线程,而后检查持有偏向锁的线程是否活着,若是不处于活动状态,则将对象头设置为无锁状态;若是依然活动,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么从新偏向其余线程,要么恢复到无锁或者标记对象不合适做为偏向锁(膨胀为轻量级锁),最后唤醒暂停的线程。
 

偏向锁的释放:

  偏向锁的撤销在上述第四步骤中有提到偏向锁只有遇到其余线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,须要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

        关闭偏向锁

偏向锁在Java运行环境中默认开启,可是不会随着程序启动当即生效,而是在启动几秒种后才激活,可使用参数关闭延迟:
-XX:BiasedLockingStartupDelay=0 
一样能够关闭偏向锁
 -XX:UseBiasedLocking=false,那么程序默认进入轻量级锁。

        偏向锁升级为轻量级锁

 

轻量级锁

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

轻量级锁是JDK1.6之中加入的新型锁机制,它并非来代替重量级锁的,他的本意是在没有多线程竞争的前提下,减小传统的重量级锁使用操做系统互斥量产生的性能消耗。

        轻量级锁加锁

线程在执行同步块以前,JVM会如今当前线程的栈帧中建立用于储存锁记录的空间(LockRecord),并将对象头的Mark Word信息复制到锁记录中。而后线程尝试使用CAS将对象头的MarkWord替换为指向锁记录的指针。若是成功,当前线程得到锁,而且对象的锁标志位转变为“00”,若是失败,表示其余线程竞争锁,当前线程便会尝试自旋获取锁。若是有两条以上的线程竞争同一个锁,那么轻量级锁就再也不有效,要膨胀为重量级锁,锁标志的状态变为“10”,MarkWord中储存的就是指向重量级锁(互斥量)的指针,后面等待的线程也要进入阻塞状态。
 

轻量级锁的加锁过程

  (1)在代码进入同步块的时候,若是同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图2.1所示。

  (2)拷贝对象头中的Mark Word复制到锁记录中。

  (3)拷贝成功后,虚拟机将使用CAS操做尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。若是更新成功,则执行步骤(3),不然执行步骤(4)。

  (4)若是这个更新动做成功了,那么这个线程就拥有了该对象的锁,而且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图2.2所示。

  (5)若是这个更新操做失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,若是是就说明当前线程已经拥有了这个对象的锁,那就能够直接进入同步块继续执行。不然说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了避免让线程阻塞,而采用循环去获取锁的过程。

 

                     图2.1 轻量级锁CAS操做以前堆栈与对象的状态

   

                      图2.2 轻量级锁CAS操做以后堆栈与对象的状态

 

 

        轻量级锁解锁

轻量级锁解锁时,一样经过CAS操做将对象头换回来。若是成功,则表示没有竞争发生。若是失败,说明有其余线程尝试过获取该锁,锁一样会膨胀为重量级锁。在释放锁的同时,唤醒被挂起的线程。

轻量级锁的解锁过程:

  (1)经过CAS操做尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。

  (2)若是替换成功,整个同步过程就完成了。

  (3)若是替换失败,说明有其余线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

锁消除

消除锁是虚拟机另一种锁的优化,这种优化更完全,Java虚拟机在JIT编译时(能够简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),经过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,经过这种方式消除没有必要的锁,能够节省毫无心义的请求锁时间,以下StringBuffer的append是一个同步方法,可是在add方法中的StringBuffer属于一个局部变量,而且不会被其余线程所使用,所以StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

public class StringBufferRemoveSync {

    public void add(String str1, String str2) {
        //StringBuffer是线程安全,因为sb只会在append方法中使用,不可能被其余线程引用
        //所以sb属于不可能共享的资源,JVM会自动消除内部的锁
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }

    public static void main(String[] args) {
        StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
        for (int i = 0; i < 10000000; i++) {
            rmsync.add("abc", "123");
        }
    }

}

 锁粗化

锁粗化的概念应该比较好理解,就是将屡次链接在一块儿的加锁、解锁操做合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:

package com.paddx.test.string;

public class StringBufferTest {
    StringBuffer stringBuffer = new StringBuffer();

    public void append(){
        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("c");
    }
}
相关文章
相关标签/搜索