文章较长倡议珍藏再看

这是一些高频的中高级java面试题,也是面试中常常碰见的,心愿对你有所帮忙。

说说过程和线程的区别?

过程是程序的一次执行,是零碎进行资源分配和调度的独立单位,他的作用是是程序可能并发执行进步资源利用率和吞吐率。

因为过程是资源分配和调度的根本单位,因为过程的创立、销毁、切换产生大量的工夫和空间的开销,过程的数量不能太多,而线程是比过程更小的能独立运行的根本单位,他是过程的一个实体,能够缩小程序并发执行时的工夫和空间开销,使得操作系统具备更好的并发性。

线程根本不领有系统资源,只有一些运行时必不可少的资源,比方程序计数器、寄存器和栈,过程则占有堆、栈。

晓得synchronized原理吗?

synchronized是java提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁,应用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现。他的作用次要就是实现原子性操作和解决共享变量的内存可见性问题。

执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者曾经取得了锁,锁的计数器+1。此时其余竞争锁的线程则会进入期待队列中。

执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁开释,处于期待队列中的线程再持续竞争锁。

synchronized是排它锁,当一个线程取得锁之后,其余线程必须期待该线程开释锁后能力取得锁,而且因为Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换十分耗费性能。

从内存语义来说,加锁的过程会革除工作内存中的共享变量,再从主内存读取,而开释锁的过程则是将工作内存中的共享变量写回主内存。

_实际上大部分时候我认为说到monitorenter就行了,然而为了更分明的形容,还是再具体一点_。

如果再深刻到源码来说,synchronized实际上有两个队列waitSet和entryList。

  1. 当多个线程进入同步代码块时,首先进入entryList
  2. 有一个线程获取到monitor锁后,就赋值给以后线程,并且计数器+1
  3. 如果线程调用wait办法,将开释锁,以后线程置为null,计数器-1,同时进入waitSet期待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁
  4. 如果线程执行结束,同样开释锁,计数器-1,以后线程置为null

那锁的优化机制理解吗?

从JDK1.6版本之后,synchronized自身也在一直优化锁的机制,有些状况下他并不会是一个很重量级的锁了。优化机制包含自适应锁、自旋锁、锁打消、锁粗化、轻量级锁和偏差锁。

锁的状态从低到高顺次为无锁->偏差锁->轻量级锁->重量级锁,降级的过程就是从低到高,降级在肯定条件也是有可能产生的。

自旋锁:因为大部分时候,锁被占用的工夫很短,共享变量的锁定工夫也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换重大影响性能。自旋的概念就是让线程执行一个忙循环,能够了解为就是啥也不干,避免从用户态转入内核态,自旋锁能够通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,能够应用-XX:PreBlockSpin设置。

自适应锁:自适应锁就是自适应的自旋锁,自旋的工夫不是固定工夫,而是由前一次在同一个锁上的自旋工夫和锁的持有者状态来决定。

锁打消:锁打消指的是JVM检测到一些同步的代码块,齐全不存在数据竞争的场景,也就是不须要加锁,就会进行锁打消。

锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范畴扩大到整个操作序列之外。

偏差锁:当线程拜访同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏差锁的线程ID,之后这个线程再次进入同步块时都不须要CAS来加锁和解锁了,偏差锁会永远偏差第一个取得锁的线程,如果后续没有其余线程取得过这个锁,持有锁的线程就永远不须要进行同步,反之,当有其余线程竞争偏差锁时,持有偏差锁的线程就会开释偏差锁。能够用过设置-XX:+UseBiasedLocking开启偏差锁。

轻量级锁:JVM的对象的对象头中蕴含有一些锁的标记位,代码进入同步块的时候,JVM将会应用CAS形式来尝试获取锁,如果更新胜利则会把对象头中的状态位标记为轻量级锁,如果更新失败,以后线程就尝试自旋来取得锁。

整个锁降级的过程非常复杂,我尽力去除一些无用的环节,简略来形容整个降级的机制。

简略点说,偏差锁就是通过对象头的偏差线程ID来比照,甚至都不须要CAS了,而轻量级锁次要就是通过CAS批改对象头锁记录和自旋来实现,重量级锁则是除了领有锁的线程其余全副阻塞。

那对象头具体都蕴含哪些内容?

在咱们罕用的Hotspot虚拟机中,对象在内存中布局理论蕴含3个局部:

  1. 对象头
  2. 实例数据
  3. 对齐填充

而对象头蕴含两局部内容,Mark Word中的内容会随着锁标记位而发生变化,所以只说存储构造就好了。

  1. 对象本身运行时所需的数据,也被称为Mark Word,也就是用于轻量级锁和偏差锁的关键点。具体的内容蕴含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏差锁线程ID、偏差锁工夫戳。
  2. 存储类型指针,也就是指向类的元数据的指针,通过这个指针能力确定对象是属于哪个类的实例。

如果是数组的话,则还蕴含了数组的长度

对于加锁,那再说下ReentrantLock原理?他和synchronized有什么区别?

相比于synchronized,ReentrantLock须要显式的获取锁和开释锁,绝对当初根本都是用JDK7和JDK8的版本,ReentrantLock的效率和synchronized区别根本能够持平了。他们的次要区别有以下几点:

  1. 期待可中断,当持有锁的线程长时间不开释锁的时候,期待中的线程能够抉择放弃期待,转而解决其余的工作。
  2. 偏心锁:synchronized和ReentrantLock默认都是非偏心锁,然而ReentrantLock能够通过构造函数传参扭转。只不过应用偏心锁的话会导致性能急剧下降。
  3. 绑定多个条件:ReentrantLock能够同时绑定多个Condition条件对象。

ReentrantLock基于AQS(AbstractQueuedSynchronizer 形象队列同步器)实现。别说了,我晓得问题了,AQS原理我来讲。

AQS外部保护一个state状态位,尝试加锁的时候通过CAS(CompareAndSwap)批改值,如果胜利设置为1,并且把以后线程ID赋值,则代表加锁胜利,一旦获取到锁,其余的线程将会被阻塞进入阻塞队列自旋,取得锁的线程开释锁的时候将会唤醒阻塞队列中的线程,开释锁的时候则会把state从新置为0,同时以后线程ID置为空。

CAS的原理呢?

CAS叫做CompareAndSwap,比拟并替换,次要是通过处理器的指令来保障操作的原子性,它蕴含三个操作数:

  1. 变量内存地址,V示意
  2. 旧的预期值,A示意
  3. 筹备设置的新值,B示意

当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。

那么CAS有什么毛病吗?

CAS的毛病次要有3点:

ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,而后筹备赋值的时候依然是A,然而实际上有可能A的值被改成了B,而后又被改回了A,这个CAS更新的破绽就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终成果。

Java中有AtomicStampedReference来解决这个问题,他退出了预期标记和更新后标记两个字段,更新时不光查看值,还要查看以后的标记是否等于预期标记,全副相等的话才会更新。

循环工夫长开销大:自旋CAS的形式如果长时间不胜利,会给CPU带来很大的开销。

只能保障一个共享变量的原子操作:只对一个共享变量操作能够保障原子性,然而多个则不行,多个能够通过AtomicReference来解决或者应用锁synchronized实现。

好,说说HashMap原理吧?

HashMap次要由数组和链表组成,他不是线程平安的。外围的点就是put插入数据的过程,get查问数据以及扩容的形式。JDK1.7和1.8的次要区别在于头插和尾插形式的批改,头插容易导致HashMap链表死循环,并且1.8之后退出红黑树对性能有晋升。

put插入数据流程

往map插入元素的时候首先通过对key hash而后与数组长度-1进行与运算((n-1)&hash),都是2的次幂所以等同于取模,然而位运算的效率更高。找到数组中的地位之后,如果数组中没有元素间接存入,反之则判断key是否雷同,key雷同就笼罩,否则就会插入到链表的尾部,如果链表的长度超过8,则会转换成红黑树,最初判断数组长度是否超过默认的长度*负载因子也就是12,超过则进行扩容。

get查问数据

查问数据相对来说就比较简单了,首先计算出hash值,而后去数组查问,是红黑树就去红黑树查,链表就遍历链表查问就能够了。

resize扩容过程

扩容的过程就是对key从新计算hash,而后把数据拷贝到新的数组。

那多线程环境怎么应用Map呢?ConcurrentHashmap理解过吗?

多线程环境能够应用Collections.synchronizedMap同步加锁的形式,还能够应用HashTable,然而同步的形式显然性能不达标,而ConurrentHashMap更适宜高并发场景应用。

ConcurrentHashmap在JDK1.7和1.8的版本改变比拟大,1.7应用Segment+HashEntry分段锁的形式实现,1.8则摈弃了Segment,改为应用CAS+synchronized+Node实现,同样也退出了红黑树,防止链表过长导致性能的问题。

1.7分段锁

从构造上说,1.7版本的ConcurrentHashMap采纳分段锁机制,外面蕴含一个Segment数组,Segment继承与ReentrantLock,Segment则蕴含HashEntry的数组,HashEntry自身就是一个链表的构造,具备保留key、value的能力能指向下一个节点的指针。

实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是反对16个线程的并发写,Segment之间互相不会受到影响。

put流程

其实发现整个流程和HashMap十分相似,只不过是先定位到具体的Segment,而后通过ReentrantLock去操作而已,前面的流程我就简化了,因为和HashMap基本上是一样的。

  1. 计算hash,定位到segment,segment如果是空就先初始化
  2. 应用ReentrantLock加锁,如果获取锁失败则尝试自旋,自旋超过次数就阻塞获取,保障肯定获取锁胜利
  3. 遍历HashEntry,就是和HashMap一样,数组中key和hash一样就间接替换,不存在就再插入链表,链表同样

get流程

get也很简略,key通过hash定位到segment,再遍历链表定位到具体的元素上,须要留神的是value是volatile的,所以get是不须要加锁的。

1.8CAS+synchronized

1.8摈弃分段锁,转为用CAS+synchronized来实现,同样HashEntry改为Node,也退出了红黑树的实现。次要还是看put的流程。

put流程

  1. 首先计算hash,遍历node数组,如果node是空的话,就通过CAS+自旋的形式初始化
  2. 如果以后数组地位是空则间接通过CAS自旋写入数据
  3. 如果hash==MOVED,阐明须要扩容,执行扩容
  4. 如果都不满足,就应用synchronized写入数据,写入数据同样判断链表、红黑树,链表写入和HashMap的形式一样,key hash一样就笼罩,反之就尾插法,链表长度超过8就转换成红黑树

get查问

get很简略,通过key计算hash,如果key hash雷同就返回,如果是红黑树依照红黑树获取,都不是就遍历链表获取。

volatile原理晓得吗?

相比synchronized的加锁形式来解决共享变量的内存可见性问题,volatile就是更轻量的抉择,他没有上下文切换的额定开销老本。应用volatile申明的变量,能够确保值被更新的时候对其余线程立即可见。volatile应用内存屏障来保障不会产生指令重排,解决了内存可见性的问题。

咱们晓得,线程都是从主内存中读取共享变量到工作内存来操作,实现之后再把后果写会主内存,然而这样就会带来可见性问题。举个例子,假如当初咱们是两级缓存的双核CPU架构,蕴含L1、L2两级缓存。

  1. 线程A首先获取变量X的值,因为最后两级缓存都是空,所以间接从主内存中读取X,假如X初始值为0,线程A读取之后把X值都批改为1,同时写回主内存。这时候缓存和主内存的状况如下图。

  1. 线程B也同样读取变量X的值,因为L2缓存曾经有缓存X=1,所以间接从L2缓存读取,之后线程B把X批改为2,同时写回L2和主内存。这时候的X值入下图所示。 那么线程A如果再想获取变量X的值,因为L1缓存曾经有x=1了,所以这时候变量内存不可见问题就产生了,B批改为2的值对A来说没有感知。

那么,如果X变量用volatile润饰的话,当线程A再次读取变量X的话,CPU就会依据缓存一致性协定强制线程A从新从主内存加载最新的值到本人的工作内存,而不是间接用缓存中的值。

再来说内存屏障的问题,volatile润饰之后会退出不同的内存屏障来保障可见性的问题能正确执行。这里写的屏障基于书中提供的内容,然而实际上因为CPU架构不同,重排序的策略不同,提供的内存屏障也不一样,比方x86平台上,只有StoreLoad一种内存屏障。

  1. StoreStore屏障,保障下面的一般写不和volatile写产生重排序
  2. StoreLoad屏障,保障volatile写与前面可能的volatile读写不产生重排序
  3. LoadLoad屏障,禁止volatile读与前面的一般读重排序
  4. LoadStore屏障,禁止volatile读和前面的一般写重排序

那么说说你对JMM内存模型的了解?为什么须要JMM?

自身随着CPU和内存的倒退速度差别的问题,导致CPU的速度远快于内存,所以当初的CPU退出了高速缓存,高速缓存个别能够分为L1、L2、L3三级缓存。基于下面的例子咱们晓得了这导致了缓存一致性的问题,所以退出了缓存一致性协定,同时导致了内存可见性的问题,而编译器和CPU的重排序导致了原子性和有序性的问题,JMM内存模型正是对多线程操作下的一系列标准束缚,因为不可能让陈雇员的代码去兼容所有的CPU,通过JMM咱们才屏蔽了不同硬件和操作系统内存的拜访差别,这样保障了Java程序在不同的平台下达到统一的内存拜访成果,同时也是保障在高效并发的时候程序可能正确执行。

原子性:Java内存模型通过read、load、assign、use、store、write来保障原子性操作,此外还有lock和unlock,间接对应着synchronized关键字的monitorenter和monitorexit字节码指令。

可见性:可见性的问题在下面的答复曾经说过,Java保障可见性能够认为通过volatile、synchronized、final来实现。

有序性:因为处理器和编译器的重排序导致的有序性问题,Java通过volatile、synchronized来保障。

happen-before规定

尽管指令重排进步了并发的性能,然而Java虚构机会对指令重排做出一些规定限度,并不能让所有的指令都随便的扭转执行地位,次要有以下几点:

  1. 单线程每个操作,happen-before于该线程中任意后续操作
  2. volatile写happen-before与后续对这个变量的读
  3. synchronized解锁happen-before后续对这个锁的加锁
  4. final变量的写happen-before于final域对象的读,happen-before后续对final变量的读
  5. 传递性规定,A先于B,B先于C,那么A肯定先于C产生

说了半天,到底工作内存和主内存是什么?

主内存能够认为就是物理内存,Java内存模型中理论就是虚拟机内存的一部分。而工作内存就是CPU缓存,他有可能是寄存器也有可能是L1L2L3缓存,都是有可能的。

说说ThreadLocal原理?

ThreadLocal能够了解为线程本地变量,他会在每个线程都创立一个正本,那么在线程之间拜访外部正本变量就行了,做到了线程之间相互隔离,相比于synchronized的做法是用空间来换工夫。

ThreadLocal有一个动态外部类ThreadLocalMap,ThreadLocalMap又蕴含了一个Entry数组,Entry自身是一个弱援用,他的key是指向ThreadLocal的弱援用,Entry具备了保留key value键值对的能力。

弱援用的目标是为了避免内存泄露,如果是强援用那么ThreadLocal对象除非线程完结否则始终无奈被回收,弱援用则会在下一次GC的时候被回收。

然而这样还是会存在内存泄露的问题,如果key和ThreadLocal对象被回收之后,entry中就存在key为null,然而value有值的entry对象,然而永远没方法被拜访到,同样除非线程完结运行。

然而只有ThreadLocal应用失当,在应用完之后调用remove办法删除Entry对象,实际上是不会呈现这个问题的。

那援用类型有哪些?有什么区别?

援用类型次要分为强脆弱虚四种:

  1. 强援用指的就是代码中普遍存在的赋值形式,比方A a = new A()这种。强援用关联的对象,永远不会被GC回收。
  2. 软援用能够用SoftReference来形容,指的是那些有用然而不是必须要的对象。零碎在产生内存溢出前会对这类援用的对象进行回收。
  3. 弱援用能够用WeakReference来形容,他的强度比软援用更低一点,弱援用的对象下一次GC的时候肯定会被回收,而不论内存是否足够。
  4. 虚援用也被称作幻影援用,是最弱的援用关系,能够用PhantomReference来形容,他必须和ReferenceQueue一起应用,同样的当产生GC的时候,虚援用也会被回收。能够用虚援用来治理堆外内存。

线程池原理晓得吗?

首先线程池有几个外围的参数概念:

  1. 最大线程数maximumPoolSize
  2. 外围线程数corePoolSize
  3. 沉闷工夫keepAliveTime
  4. 阻塞队列workQueue
  5. 回绝策略RejectedExecutionHandler

当提交一个新工作到线程池时,具体的执行流程如下:

  1. 当咱们提交工作,线程池会依据corePoolSize大小创立若干工作数量线程执行工作
  2. 当工作的数量超过corePoolSize数量,后续的工作将会进入阻塞队列阻塞排队
  3. 当阻塞队列也满了之后,那么将会持续创立(maximumPoolSize-corePoolSize)个数量的线程来执行工作,如果工作解决实现,maximumPoolSize-corePoolSize额定创立的线程期待keepAliveTime之后被主动销毁
  4. 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将依据不同的回绝策略对应解决

回绝策略有哪些?

次要有4种回绝策略:

  1. AbortPolicy:间接抛弃工作,抛出异样,这是默认策略
  2. CallerRunsPolicy:只用调用者所在的线程来解决工作
  3. DiscardOldestPolicy:抛弃期待队列中最旧的工作,并执行当前任务
  4. DiscardPolicy:间接抛弃工作,也不抛出异样
  • END -

举荐浏览:

======

为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了

刷Github时发现了一本阿里大神的算法笔记!标星70.5K

程序员50W年薪的常识体系与成长路线。

月薪在30K以下的程序员,听不懂这个我的项目;

字节跳动总结的设计模式 PDF 火了,完整版凋谢分享

对于【暴力递归算法】你所不晓得的思路

开拓鸿蒙,谁做零碎,聊聊华为微内核

看完三件事❤️

如果你感觉这篇内容对你还蛮有帮忙,我想邀请你帮我三个小忙:

点赞,转发,有你们的 『点赞和评论』,才是我发明的能源。

关注公众号 『 Java斗帝 』,不定期分享原创常识。

同时能够期待后续文章ing????