2020字节跳动最新春招面经分享(附增最新整理面试文档PDF)

前言:
java

愈来愈多的人都想去字节跳动了,小编也不例外,此次面试字节跳动也是作了不少的准备,还好顺利拿到了offer,特分享一下此次的面试题,可能有些记不全了,但多少也可以给一些正在面试字节或计划面试字节的朋友提供帮助。面试


f58efe46-c1e3-4985-a6fb-fdf2166dfd49



hashmap和hashTable的区别?为什么一个线程安全一个线程不安全?编程

区别:数组

Hashtable 是早期Java类库提供的一个哈希表实现,自己是同步(synchronized)的,不支持 null 键和值,因为同步致使的性能开销,因此已经不多被推荐使用。安全

HashMap与 HashTable主要区别在于 HashMap 不是同步的,支持 null 键和值等。一般状况下,HashMap 进行 put 或者 get 操做,能够达到常数时间的性能,因此它是绝大部分利用键值对存取场景的首选。数据结构

底层实现:多线程

JDK1.8 以前 HashMap 底层是 数组和链表 结合在一块儿使用也就是 链表散列。HashMap 经过 key 的 hashCode 通过扰动函数处理事后获得 hash 值,而后经过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),若是当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,若是相同的话,直接覆盖,不相同就经过拉链法解决冲突。JDK1.8 之后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减小搜索时间。Hashtable 没有这样的机制。并发

而HashTable底层与HashMap基本相似,主要区别在HashTable为了实现同步,全部方法都加了synchronizedide

线程安全:函数

HashMap 是非线程安全的,HashTable 是线程安全的;由于HashTable 内部的方法基本都通过synchronized 修饰。但HashTable效率低于HashMap,虽然能保证多线程下同步,但也会大大下降程序的性能(若是你要保证线程安全的话就使用 ConcurrentHashMap 吧,下面会有说到!);


- java中的hashmap 和 concurrenHashmap的区别?

区别:

HashMap是非同步的,这也意味着,HashMap在进行插入、删除等操做的时候,是线程不安全的,若是本身没有在程序上对HashMap进行同步的处理,则不能让多个线程共享一个变量。

concurrenHashmap

底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构同样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 以前的 HashMap 的底层数据结构相似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;

实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不一样数据段的数据,就不会存在锁竞争,提升并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操做。(JDK1.6之后 对 synchronized锁作了不少优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,可是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率很是低下。当一个线程访问同步方法时,其余线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另外一个线程不能使用 put 添加元素,也不能使用 get,竞争会愈来愈激烈效率越低。


- concurrenHashMap如何实现线程安全且并发性能比hashTable高?

锁分段:

在多线程状况下,既然不能全锁(HashTable)又不能不锁(HashMap),因此就搞个部分锁,只锁部分,用到哪部分就锁哪部分。一个大仓库,里面有若干个隔间,每一个隔间都有锁,同时只容许一我的进隔间存取东西。可是,在存取东西以前,须要有一个全局索引,告诉你要操做的资源在哪一个隔间里,而后当你看到隔间空闲时,就能够进去存取,若是隔间正在占用,那你就得等着。

解释下锁分段?

首先将数据分为一段一段的存储,而后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其余段的数据也能被其余线程访问。

JDK1.7的ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。

Segment 实现了 ReentrantLock,因此 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。

static class Segment<K,V> extends ReentrantLock implements Serializable {

}

一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap相似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每一个 HashEntry 是一个链表结构的元素,每一个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先得到对应的 Segment的锁。


JDK1.8的ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构相似,数组+链表/红黑二叉树。Java 8在链表长度超过必定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)))

synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提高N倍。


-聊一下volatile做用?

做用:

(1)保证可见性,不保证原子性

(2)禁止指令重排

谈谈原子性:

定义: 即一个操做或者多个操做 要么所有执行而且执行的过程不会被任何因素打断,要么就都不执行。

原子性是拒绝多线程操做的,不管是多核仍是单核,具备原子性的量,同一时刻只能有一个线程来对它进行操做。简而言之,在整个操做过程当中不会被线程调度器中断的操做,均可认为是原子性。例如 a=1是原子性操做,可是a++和a +=1就不是原子性操做。Java中的原子性操做包括:

a. 基本类型的读取和赋值操做,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性操做。

b.全部引用reference的赋值操做

c.java.concurrent.Atomic.* 包中全部类的一切操做


谈谈可见性:

定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其余线程可以当即看获得修改的值。

在多线程环境下,一个线程对共享变量的操做对其余线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会当即被更新到主内存中,其余线程读取共享变量时,会直接从主内存中读取。固然,synchronize和Lock均可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁而后执行同步代码,而且在释放锁以前会将对变量的修改刷新到主存当中。所以能够保证可见性。

谈谈有序性(指令重排):

定义:即程序执行的顺序按照代码的前后顺序执行。

Java内存模型中的有序性能够总结为:若是在本线程内观察,全部操做都是有序的;若是在一个线程中观察另外一个线程,全部操做都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工做内存主主内存同步延迟”现象。

在Java内存模型中,为了效率是容许编译器和处理器对指令进行重排序,固然重排序不会影响单线程的运行结果,可是对多线程会有影响。Java提供volatile来保证必定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,能够经过synchronized和Lock来保证有序性,synchronized和Lock保证每一个时刻是有一个线程执行同步代码,至关因而让线程顺序执行同步代码,天然就保证了有序性。


volatile使用场景

单例模式的双重锁

共享变量读多写少的状况


聊一下synchronize关键字实现原理?

使用方式:

同步普通方法,锁的是当前对象。

同步静态方法,锁的是当前 Class 对象。

同步块,锁的是 {} 中的对象。

实现原理:

JVM 是经过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。具体实现是在编译以后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和异常处插入 monitor.exit 的指令。其本质就是对一个对象监视器( Monitor )进行获取,而这个获取过程具备排他性从而达到了同一时刻只能一个线程访问的目的。而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit 以后才能尝试继续获取锁。

synchronize的锁升级?

- 无锁 -> 轻量锁 -> 解锁 -> 偏向锁 -> 释放锁

谈谈轻量锁:

当代码进入同步块时,若是同步对象为无锁状态时,当前线程会在栈帧中建立一个锁记录(Lock Record)区域,同时将锁对象的对象头中 Mark Word 拷贝到锁记录中,再尝试使用 CAS 将 Mark Word 更新为指向锁记录的指针。

若是更新成功,当前线程就得到了锁。

若是更新失败 JVM 会先检查锁对象的 Mark Word 是否指向当前线程的锁记录。

若是是则说明当前线程拥有锁对象的锁,能够直接进入同步块。

不是则说明有其余线程抢占了锁,若是存在多个线程同时竞争一把锁,轻量锁就会膨胀为重量锁。

解锁:

轻量锁的解锁过程也是利用 CAS 来实现的,会尝试锁记录替换回锁对象的 Mark Word 。若是替换成功则说明整个同步操做完成,失败则说明有其余线程尝试获取锁,这时就会唤醒被挂起的线程(此时已经膨胀为重量锁)

轻量锁能提高性能的缘由是:认为大多数锁在整个同步周期都不存在竞争,因此使用 CAS 比使用互斥开销更少。但若是锁竞争激烈,轻量锁就不但有互斥的开销,还有 CAS 的开销,甚至比重量锁更慢。


偏向锁:

为了进一步的下降获取锁的代价,JDK1.6 以后还引入了偏向锁。

偏向锁的特征是:锁不存在多线程竞争,而且应由一个线程屡次得到锁。

当线程访问同步块时,会使用 CAS 将线程 ID 更新到锁对象的 Mark Word 中,若是更新成功则得到偏向锁,而且以后每次进入这个对象锁相关的同步块时都不须要再次获取锁了。


释放锁:

当有另一个线程获取这个锁时,持有偏向锁的线程就会释放锁,释放时会等待全局安全点(这一时刻没有字节码运行),接着会暂停拥有偏向锁的线程,根据锁对象目前是否被锁来断定将对象头中的 Mark Word 设置为无锁或者是轻量锁状态。

轻量锁能够提升带有同步却没有竞争的程序性能,但若是程序中大多数锁都存在竞争时,那偏向锁就起不到太大做用。可使用 -XX:-userBiasedLocking=false 来关闭偏向锁,并默认进入轻量锁。


有用过java中的队列吗?

队列是一种特殊的线性表,遵循的原则就是“先入先出”。在咱们平常使用中,常常会用来并发操做数据。在并发编程中,有时候须要使用线程安全的队列。若是要实现一个线程安全的队列一般有两种方式:一种是使用阻塞队列,另外一种是使用线程同步锁。

阻塞队列有哪些:


09b0c4e360be4ca2bad224149c9e61cb



ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)对元素进行排序。

LinkedBlokcingQueue:是一个基于链表结构的阻塞队列,此队列按 FIFO(先进先出)对元素进行排序,吞吐量一般要高于 ArrayBlockingQueue。

SynchronousQueue:是一个不存储元素的阻塞队列,每一个插入操做必须等到另外一个线程调用移除操做,不然插入操做一直处于阻塞状态,吞吐量一般要高于 LinkedBlokcingQueue。


什么是阻塞队列:


阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的做用大体如图所示:

当阻塞队列是空时,从队列中获取元素的操做将会被阻塞。

当阻塞队列是满时,往队列里添加元素的操做将会被阻塞。


后序更新……

读者福利:

今天就给你们分享了这么多,后续有机会在给你们分享更多的经验,今天给你们分享一份千道的面试题资料分享给你们,有答案,带详细的解析。

领取方式:关注个人供种号(Java周某人)便可领取

efb23102d6cd46808b25697487893696

相关文章
相关标签/搜索