2020年中高级Android大厂面试秘籍,为你保驾护航金三银四,直通大厂(Java篇)

转载:http://www.javashuo.com/article/p-hzjfunlz-hv.htmlhtml

前言

成为一名优秀的Android开发,须要一份完备的知识体系,在这里,让咱们一块儿成长为本身所想的那样~。

🔥 A awesome android expert interview questions and answers(continuous updating ...)java

从几十份顶级面试仓库和300多篇高质量面经中总结出一份全面成体系化的Android高级面试题集。python

欢迎来到2020年中高级Android大厂面试秘籍,为你保驾护航金三银四,直通大厂的Java。android

Java面试题

Java基础

1、面向对象 (⭐⭐⭐)

一、谈谈对java多态的理解?git

多态是指父类的某个方法被子类重写时,能够产生本身的功能行为,同一个操做做用于不一样对象,能够有不一样的解释,产生不一样的执行结果。程序员

多态的三个必要条件:github

  • 继承父类。
  • 重写父类的方法。
  • 父类的引用指向子类对象。

什么是多态面试

面向对象的三大特性:封装、继承、多态。从必定角度来看,封装和继承几乎都是为多态而准备的。这是咱们最后一个概念,也是最重要的知识点。算法

多态的定义:指容许不一样类的对象对同一消息作出响应。即同一消息能够根据发送对象的不一样而采用多种不一样的行为方式。(发送消息就是函数调用)数据库

实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

多态的做用:消除类型之间的耦合关系。

现实中,关于多态的例子不胜枚举。比方说按下 F1 键这个动做,若是当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;若是当前在 Word 下弹出的就是 Word 帮助;在 Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不一样的对象上会产生不一样的结果。

多态的好处:

1.可替换性(substitutability)。多态对已存在代码具备可替换性。例如,多态对圆Circle类工做,对其余任何圆形几何体,如圆环,也一样工做。

2.可扩充性(extensibility)。多态对代码具备可扩充性。增长新的子类不影响已存在类的多态性、继承性,以及其余特性的运行和操做。实际上新加子类更容易得到多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。

3.接口性(interface-ability)。多态是超类经过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。

4.灵活性(flexibility)。它在应用中体现了灵活多样的操做,提升了使用效率。

5.简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤为在处理大量对象的运算和操做时,这个特色尤其突出和重要。

Java中多态的实现方式:接口实现,继承父类进行方法重写,同一个类中进行方法重载。

二、你所知道的设计模式有哪些?

答:Java 中通常认为有23种设计模式,咱们不须要全部的都会,可是其中经常使用的种设计模式应该去掌握。下面列出了全部的设计模式。要掌握的设计模式我单独列出来了,固然能掌握的越多越好。

整体来讲设计模式分为三大类:

建立型模式,共五种:

工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共七种:

适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共十一种:

策略模式、模板方法模式、观者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

具体可见个人设计模式总结笔记

三、经过静态内部类实现单例模式有哪些优势?

  1. 不用 synchronized ,节省时间。
  2. 调用 getInstance() 的时候才会建立对象,不调用不建立,节省空间,这有点像传说中的懒汉式。

四、静态代理和动态代理的区别,什么场景使用?

静态代理与动态代理的区别在于代理类生成的时间不一样,即根据程序运行前代理类是否已经存在,能够将代理分为静态代理和动态代理。若是须要对多个类进行代理,而且代理的功能都是同样的,用静态代理重复编写代理类就很是的麻烦,能够用动态代理动态的生成代理类。

// 为目标对象生成代理对象
public Object getProxyInstance() {
    return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
            new InvocationHandler() {

                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("开启事务");

                    // 执行目标对象方法
                    Object returnValue = method.invoke(target, args);

                    System.out.println("提交事务");
                    return null;
                }
            });
}
复制代码
  • 静态代理使用场景:四大组件同AIDL与AMS进行跨进程通讯
  • 动态代理使用场景:Retrofit使用了动态代理极大地提高了扩展性和可维护性。

五、简单工厂、工厂方法、抽象工厂、Builder模式的区别?

  • 简单工厂模式:一个工厂方法建立不一样类型的对象。
  • 工厂方法模式:一个具体的工厂类负责建立一个具体对象类型。
  • 抽象工厂模式:一个具体的工厂类负责建立一系列相关的对象。
  • Builder模式:对象的构建与表示分离,它更注重对象的建立过程。

六、装饰模式和代理模式有哪些区别 ?与桥接模式相比呢?

  • 一、装饰模式是以客户端透明的方式扩展对象的功能,是继承关系的一个替代方案;而代理模式则是给一个对象提供一个代理对象,并由代理对象来控制对原有对象的引用。
  • 二、装饰模式应该为所装饰的对象加强功能;代理模式对代理的对象施加控制,但不对对象自己的功能进行增长。
  • 三、桥接模式的做用于代理、装饰大相径庭,它主要是为了应对某个类族有多个变化维度致使子类类型急剧增多的场景。经过桥接模式将多个变化维度隔离开,使得它们能够独立地变化,最后经过组合使它们应对多维变化,减小子类的数量和复杂度。

七、外观模式和中介模式的区别?

外观模式重点是对外封装统一的高层接口,便于用户使用;而中介模式则是避免多个互相协做的对象直接引用,它们之间的交互经过一个中介对象进行,从而使得它们耦合松散,可以易于应对变化。

八、策略模式和状态模式的区别?

虽然二者的类型结构是一致的,可是它们的本质倒是不同的。策略模式重在整个算法的替换,也就是策略的替换,而状态模式则是经过状态来改变行为。

九、适配器模式,装饰者模式,外观模式的异同?

这三个模式的相同之处是,它们都做用于用户与真实被使用的类或系统之间,做一个中间层,起到了让用户间接地调用真实的类的做用。它们的不一样以外在于,如上所述的应用场合不一样和本质的思想不一样。

代理与外观的主要区别在于,代理对象表明一个单一对象,而外观对象表明一个子系统,代理的客户对象没法直接访问对象,由代理提供单独的目标对象的访问,而一般外观对象提供对子系统各元件功能的简化的共同层次的调用接口。代理是一种原来对象的表明,其它须要与这个对象打交道的操做都是和这个表明交涉的。而适配器则不须要虚构出一个表明者,只须要为应付特定使用目的,将原来的类进行一些组合。

外观与适配器都是对现存系统的封装。外观定义的新的接口,而适配器则是复用一个原有的接口,适配器是使两个已有的接口协同工做,而外观则是为现存系统提供一个更为方便的访问接口。若是硬要说外观是适配,那么适配器有用来适配对象的,而外观是用来适配整个子系统的。也就是说,外观所针对的对象的粒度更大。

代理模式提供与真实的类一致的接口,意在用代理类来处理真实的类,实现一些特定的服务或真实类的部分功能,Facade(外观)模式注重简化接口,Adapter(适配器)模式注重转换接口。

十、代码的坏味道:

一、代码重复:

代码重复几乎是最多见的异味了。他也是Refactoring 的主要目标之一。代码重复每每来自于copy-and-paste 的编程风格。

二、方法过长:

一个方法应当具备自我独立的意图,不要把几个意图放在一块儿。

三、类提供的功能太多:

把太多的责任交给了一个类,一个类应该仅提供一个单一的功能。

四、数据泥团:

某些数据一般像孩子同样成群玩耍:一块儿出如今不少类的成员变量中,一块儿出如今许多方法的参数中…..,这些数据或许应该本身独立造成对象。 好比以单例的形式对外提供本身的实例。

五、冗赘类:

一个干活很少的类。类的维护须要额外的开销,若是一个类承担了太少的责任,应当消除它。

六、须要太多注释:

常常以为要写不少注释表示你的代码难以理解。若是这种感受太多,表示你须要Refactoring。

十一、是否能从Android中举几个例子说说用到了什么设计模式 ?

AlertDialog、Notification源码中使用了Bulider(建造者)模式完成参数的初始化:

在AlertDialog的Builder模式中并无看到Direcotr角色的出现,其实在不少场景中,Android并无彻底按照GOF的经典设计模式来实现,而是作了一些修改,使得这个模式更易于使用。这个的AlertDialog.Builder同时扮演了上下文中提到的builder、ConcreteBuilder、Director的角色,简化了Builder模式的设计。当模块比较稳定,不存在一些变化时,能够在经典模式实现的基础上作出一些精简,而不是照搬GOF上的经典实现,更不要生搬硬套,使程序失去架构之美。

定义:将一个复杂对象的构建与它的表示分离,使得一样的构建过程能够建立不一样的表示。即将配置从目标类中隔离出来,避免过多的setter方法。

优势:

  • 一、良好的封装性,使用建造者模式能够使客户端没必要知道产品内部组成的细节。
  • 二、建造者独立,容易扩展。

缺点:

  • 会产生多余的Builder对象以及Director对象,消耗内存。

平常开发的BaseActivity抽象工厂模式:

定义:为建立一组相关或者是相互依赖的对象提供一个接口,而不须要指定它们的具体类。

主题切换的应用:

好比咱们的应用中有两套主题,分别为亮色主题LightTheme和暗色主题DarkTheme,这两种主题咱们能够经过一个抽象的类或接口来定义,而在对应主题下咱们又有各种不一样的UI元素,好比Button、TextView、Dialog、ActionBar等,这些UI元素都会分别对应不一样的主题,这些UI元素咱们也能够经过抽象的类或接口定义,抽象的主题、具体的主题、抽象的UI元素和具体的UI元素之间的关系就是抽象工厂模式最好的体现。

优势:

  • 分离接口与实现,面向接口编程,使其从具体的产品实现中解耦,同时基于接口与实现的分离,使抽象该工厂方法模式在切换产品类时更加灵活、容易。

缺点:

  • 类文件的爆炸性增长。
  • 新的产品类不易扩展。

Okhttp内部使用了责任链模式来完成每一个Interceptor拦截器的调用:

定义:使多个对象都有机会处理请求,从而避免了请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。

ViewGroup事件传递的递归调用就相似一条责任链,一旦其寻找到责任者,那么将由责任者持有并消费掉该次事件,具体体如今View的onTouchEvent方法中返回值的设置,若是onTouchEvent返回false,那么意味着当前View不会是该次事件的责任人,将不会对其持有;若是为true则相反,此时View会持有该事件并再也不向下传递。

优势:

将请求者和处理者关系解耦,提供代码的灵活性。

缺点:

对链中请求处理者的遍历中,若是处理者太多,那么遍历一定会影响性能,特别是在一些递归调用中,要慎重。

RxJava的观察者模式:

定义:定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则全部依赖于它的对象都会获得通知并被自动更新。

ListView/RecyclerView的Adapter的notifyDataSetChanged方法、广播、事件总线机制。

观察者模式主要的做用就是对象解耦,将观察者与被观察者彻底隔离,只依赖于Observer和Observable抽象。

优势:

  • 观察者和被观察者之间是抽象耦合,应对业务变化。
  • 加强系统灵活性、可扩展性。

缺点:

  • 在Java中消息的通知默认是顺序执行,一个观察者卡顿,会影响总体的执行效率,在这种状况下,通常考虑采用异步的方式。

AIDL代理模式:

定义:为其余对象提供一种代理以控制对这个对象的访问。

静态代理:代码运行前代理类的class编译文件就已经存在。

动态代理:经过反射动态地生成代理者的对象。代理谁将会在执行阶段决定。将原来代理类所作的工做由InvocationHandler来处理。

使用场景:

  • 当没法或不想直接访问某个对象或访问某个对象存在困难时能够经过一个代理对象来间接访问,为了保证客户端使用的透明性,委托对象与代理对象须要实现相同的接口。

缺点:

  • 对类的增长。

ListView/RecyclerView/GridView的适配器模式:

适配器模式把一个类的接口变换成客户端所期待的另外一种接口,从而使本来因接口不匹配而没法在一块儿工做的两个类可以在一块儿工做。

使用场景:

  • 接口不兼容。
  • 想要创建一个能够重复使用的类。
  • 须要一个统一的输出接口,而输入端的类型不可预知。

优势:

  • 更好的复用性:复用现有的功能。
  • 更好的扩展性:扩展示有的功能。

缺点:

  • 过多地使用适配器,会让系统很是零乱,不易于总体把握。例如,明明看到调用的是A接口,其实内部被适配成了B接口的实现,一个系统若是出现太多这种状况,无异于一场灾难。

Context/ContextImpl外观模式:

要求一个子系统的外部与其内部的通讯必须经过一个统一的对象进行,门面模式提供一个高层次的接口,使得子系统更易于使用。

使用场景:

  • 为一个复杂子系统提供一个简单接口。

优势:

  • 对客户程序隐藏子系统细节,于是减小了客户对于子系统的耦合,可以拥抱变化。
  • 外观类对子系统的接口封装,使得系统更易用使用。

缺点:

  • 外观类接口膨胀。
  • 外观类没有遵循开闭原则,当业务出现变动时,可能须要直接修改外观类。

2、集合框架 (⭐⭐⭐)

一、集合框架,list,map,set都有哪些具体的实现类,区别都是什么?

Java集合里使用接口来定义功能,是一套完善的继承体系。Iterator是全部集合的总接口,其余全部接口都继承于它,该接口定义了集合的遍历操做,Collection接口继承于Iterator,是集合的次级接口(Map独立存在),定义了集合的一些通用操做。

Java集合的类结构图以下所示:

 

image

 

 

List:有序、可重复;索引查询速度快;插入、删除伴随数据移动,速度慢;

Set:无序,不可重复;

Map:键值对,键惟一,值多个;

1.List,Set都是继承自Collection接口,Map则不是;

2.List特色:元素有放入顺序,元素可重复;

Set特色:元素无放入顺序,元素不可重复,重复元素会盖掉,(注意:元素虽然无放入顺序,可是元素在set中位置是由该元素的HashCode决定的,其位置实际上是固定,加入Set 的Object必须定义equals()方法;

另外list支持for循环,也就是经过下标来遍历,也能够使用迭代器,可是set只能用迭代,由于他无序,没法用下标取得想要的值)。

3.Set和List对比:

Set:检索元素效率低下,删除和插入效率高,插入和删除不会引发元素位置改变。

List:和数组相似,List能够动态增加,查找元素效率高,插入删除元素效率低,由于会引发其余元素位置改变。

4.Map适合储存键值对的数据。

5.线程安全集合类与非线程安全集合类

LinkedList、ArrayList、HashSet是非线程安全的,Vector是线程安全的;

HashMap是非线程安全的,HashTable是线程安全的;

StringBuilder是非线程安全的,StringBuffer是线程安的。

下面是这些类具体的使用介绍:

ArrayList与LinkedList的区别和适用场景

Arraylist:

优势:ArrayList是实现了基于动态数组的数据结构,因地址连续,一旦数据存储好了,查询操做效率会比较高(在内存里是连着放的)。

缺点:由于地址连续,ArrayList要移动数据,因此插入和删除操做效率比较低。

LinkedList:

优势:LinkedList基于链表的数据结构,地址是任意的,其在开辟内存空间的时候不须要等一个连续的地址,对新增和删除操做add和remove,LinedList比较占优点。LikedList 适用于要头尾操做或插入指定位置的场景。

缺点:由于LinkedList要移动指针,因此查询操做性能比较低。

适用场景分析:

当须要对数据进行对此访问的状况下选用ArrayList,当要对数据进行屡次增长删除修改时采用LinkedList。

ArrayList和LinkedList怎么动态扩容的吗?

ArrayList:

ArrayList 初始化大小是 10 (若是你知道你的arrayList 会达到多少容量,能够在初始化的时候就指定,能节省扩容的性能开支) 扩容点规则是,新增的时候发现容量不够用了,就去扩容 扩容大小规则是,扩容后的大小= 原始大小+原始大小/2 + 1。(例如:原始大小是 10 ,扩容后的大小就是 10 + 5+1 = 16)

LinkedList:

linkedList 是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好。

ArrayList与Vector的区别和适用场景

ArrayList有三个构造方法:

public ArrayList(intinitialCapacity)// 构造一个具备指定初始容量的空列表。   
public ArrayList()// 构造一个初始容量为10的空列表。
public ArrayList(Collection<? extends E> c)// 构造一个包含指定 collection 的元素的列表  
复制代码

Vector有四个构造方法:

public Vector() // 使用指定的初始容量和等于零的容量增量构造一个空向量。    
public Vector(int initialCapacity) // 构造一个空向量,使其内部数据数组的大小,其标准容量增量为零。    
public Vector(Collection<? extends E> c)// 构造一个包含指定 collection 中的元素的向量  
public Vector(int initialCapacity, int capacityIncrement)// 使用指定的初始容量和容量增量构造一个空的向量
复制代码

ArrayList和Vector都是用数组实现的,主要有这么四个区别:

1)Vector是多线程安全的,线程安全就是说多线程访问代码,不会产生不肯定的结果。而ArrayList不是,这能够从源码中看出,Vector类中的方法不少有synchronied进行修饰,这样就致使了Vector在效率上没法与ArrayLst相比;

2)两个都是采用的线性连续空间存储元素,可是当空间充足的时候,两个类的增长方式是不一样。

3)Vector能够设置增加因子,而ArrayList不能够。

4)Vector是一种老的动态数组,是线程同步的,效率很低,通常不同意使用。

适用场景:

1.Vector是线程同步的,因此它也是线程安全的,而ArraList是线程异步的,是不安全的。若是不考虑到线程的安全因素,通常用ArrayList效率比较高。

2.若是集合中的元素的数目大于目前集合数组的长度时,在集合中使用数据量比较大的数据,用Vector有必定的优点。

HashSet与TreeSet的区别和适用场景

1.TreeSet 是二叉树(红黑树的树据结构)实现的,Treest中的数据是自动排好序的,不容许放入null值。

2.HashSet 是哈希表实现的,HashSet中的数据是无序的能够放入null,但只能放入一个null,二者中的值都不重复,就如数据库中惟一约束。

3.HashSet要求放入的对象必须实现HashCode()方法,放的对象,是以hashcode码做为标识的,而具备相同内容的String对象,hashcode是同样,因此放入的内容不能重复可是同一个类的对象能够放入不一样的实例。

适用场景分析:

HashSet是基于Hash算法实现的,其性能一般都优于TreeSet。为快速查找而设计的Set,咱们一般都应该使用HashSet,在咱们须要排序的功能时,咱们才使用TreeSet。

HashMap与TreeMap、HashTable的区别及适用场景

HashMap 非线程安全

HashMap:基于哈希表(散列表)实现。使用HashMap要求的键类明肯定义了hashCode()和equals()[能够重写hasCode()和equals()],为了优化HashMap空间的使用,您能够调优初始容量和负载因子。其中散列表的冲突处理主分两种,一种是开放定址法,另外一种是链表法。HashMap实现中采用的是链表法。

TreeMap:非线程安全基于红黑树实现。TreeMap没有调优选项,由于该树总处于平衡状态。

适用场景分析:

HashMap和HashTable:HashMap去掉了HashTable的contain方法,可是加上了containsValue()和containsKey()方法。HashTable是同步的,而HashMap是非同步的,效率上比HashTable要高。HashMap容许空键值,而HashTable不容许。

HashMap:适用于Map中插入、删除和定位元素。

Treemap:适用于按天然顺序或自定义顺序遍历键(key)。 (ps:其实咱们工做的过程当中对集合的使用是很频繁的,稍注意并总结积累一下,在面试的时候应该会回答的很轻松)

二、set集合从原理上如何保证不重复?

1)在往set中添加元素时,若是指定元素不存在,则添加成功。

2)具体来说:当向HashSet中添加元素的时候,首先计算元素的hashcode值,而后用这个(元素的hashcode)%(HashMap集合的大小)+1计算出这个元素的存储位置,若是这个位置为空,就将元素添加进去;若是不为空,则用equals方法比较元素是否相等,相等就不添加,不然找一个空位添加。

三、HashMap和HashTable的主要区别是什么?,二者底层实现的数据结构是什么?

HashMap和HashTable的区别:

两者都实现了Map 接口,是将惟一的键映射到特定的值上,主要区别在于:

1)HashMap 没有排序,容许一个null 键和多个null 值,而Hashtable 不容许;

2)HashMap 把Hashtable 的contains 方法去掉了,改为containsvalue 和containsKey, 由于contains 方法容易让人引发误解;

3)Hashtable 继承自Dictionary 类,HashMap 是Java1.2 引进的Map 接口的实现;

4)Hashtable 的方法是Synchronized 的,而HashMap 不是,在多个线程访问Hashtable 时,不须要本身为它的方法实现同步,而HashMap 就必须为之提供额外的同步。Hashtable 和HashMap 采用的hash/rehash 算法大体同样,因此性能不会有很大的差别。

HashMap和HashTable的底层实现数据结构:

HashMap和Hashtable的底层实现都是数组 + 链表结构实现的(jdk8之前)

四、HashMap、ConcurrentHashMap、hash()相关原理解析?

HashMap 1.7的原理:

HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不一样。

负载因子:

  • 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程当中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就须要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操做,因此很是消耗性能。
  • 所以一般建议能提早预估 HashMap 的大小最好,尽可能的减小扩容带来的性能损耗。

其实真正存放数据的是 Entry<K,V>[] table,Entry 是 HashMap 中的一个静态内部类,它有key、value、next、hash(key的hashcode)成员变量。

put 方法:

  • 判断当前数组是否须要初始化。
  • 若是 key 为空,则 put 一个空值进去。
  • 根据 key 计算出 hashcode。
  • 根据计算出的 hashcode 定位出所在桶。
  • 若是桶是一个链表则须要遍历判断里面的 hashcode、key 是否和传入 key 相等,若是相等则进行覆盖,并返回原来的值。
  • 若是桶是空的,说明当前位置没有数据存入,新增一个 Entry 对象写入当前位置。(当调用 addEntry 写入 Entry 时须要判断是否须要扩容。若是须要就进行两倍扩充,并将当前的 key 从新 hash 并定位。而在 createEntry 中会将当前位置的桶传入到新建的桶中,若是当前桶有值就会在位置造成链表。)

get 方法:

  • 首先也是根据 key 计算出 hashcode,而后定位到具体的桶中。
  • 判断该位置是否为链表。
  • 不是链表就根据 key、key 的 hashcode 是否相等来返回值。
  • 为链表则须要遍历直到 key 及 hashcode 相等时候就返回值。
  • 啥都没取到就直接返回 null 。

HashMap 1.8的原理:

当 Hash 冲突严重时,在桶上造成的链表会变的愈来愈长,这样在查询时的效率就会愈来愈低;时间复杂度为 O(N),所以 1.8 中重点优化了这个查询效率。

TREEIFY_THRESHOLD 用于判断是否须要将链表转换为红黑树的阈值。

HashEntry 修改成 Node。

put 方法:

  • 判断当前桶是否为空,空的就须要初始化(在resize方法 中会判断是否进行初始化)。
  • 根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空代表没有 Hash 冲突就直接在当前位置建立一个新桶便可。
  • 若是当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。
  • 若是当前桶为红黑树,那就要按照红黑树的方式写入数据。
  • 若是是个链表,就须要将当前的 key、value 封装成一个新节点写入到当前桶的后面(造成链表)。
  • 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
  • 若是在遍历过程当中找到 key 相同时直接退出遍历。
  • 若是 e != null 就至关于存在相同的 key,那就须要将值覆盖。
  • 最后判断是否须要进行扩容。

get 方法:

  • 首先将 key hash 以后取得所定位的桶。
  • 若是桶为空则直接返回 null 。
  • 不然判断桶的第一个位置(有多是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
  • 若是第一个不匹配,则判断它的下一个是红黑树仍是链表。
  • 红黑树就按照树的查找方式返回值。
  • 否则就按照链表的方式遍历匹配返回值。

修改成红黑树以后查询效率直接提升到了 O(logn)。可是 HashMap 原有的问题也都存在,好比在并发场景下使用时容易出现死循环:

  • 在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操做容易在一个桶上造成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环:在 1.7 中 hash 冲突采用的头插法造成的链表,在并发条件下会造成循环链表,一旦有查询落到了这个链表上,当获取不到值时就会死循环。

ConcurrentHashMap 1.7原理:

ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样无论是 put 仍是 get 操做都须要作同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其余的 Segment。

put 方法:

首先是经过 key 定位到 Segment,以后在对应的 Segment 中进行具体的 put。

  • 虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,可是并不能保证并发的原子性,因此 put 操做时仍然须要加锁处理。

  • 首先第一步的时候会尝试获取锁,若是获取失败确定就有其余线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁:

    尝试自旋获取锁。 若是重试的次数达到了 MAX_SCAN_RETRIES 则改成阻塞锁获取,保证能获取成功。

  • 将当前 Segment 中的 table 经过 key 的 hashcode 定位到 HashEntry。

  • 遍历该 HashEntry,若是不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。

  • 为空则须要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否须要扩容。

  • 最后会使用unlock()解除当前 Segment 的锁。

get 方法:

  • 只须要将 Key 经过 Hash 以后定位到具体的 Segment ,再经过一次 Hash 定位到具体的元素上。
  • 因为 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,因此每次获取时都是最新值。
  • ConcurrentHashMap 的 get 方法是很是高效的,由于整个过程都不须要加锁。

ConcurrentHashMap 1.8原理:

1.7 已经解决了并发问题,而且能支持 N 个 Segment 这么屡次数的并发,但依然存在 HashMap 在 1.7 版本中的问题:那就是查询遍历链表效率过低。和 1.8 HashMap 结构相似:其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

CAS:

若是obj内的value和expect相等,就证实没有其余线程改变过这个变量,那么就更新它为update,若是这一步的CAS没有成功,那就采用自旋的方式继续进行CAS操做。

问题:

  • 目前在JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法做用是首先检查当前引用是否等于预期引用,而且当前标志是否等于预期标志,若是所有相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
  • 若是CAS不成功,则会原地自旋,若是长时间自旋会给CPU带来很是大的执行开销。

put 方法:

  • 根据 key 计算出 hashcode 。
  • 判断是否须要进行初始化。
  • 若是当前 key 定位出的 Node为空表示当前位置能够写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  • 若是当前位置的 hashcode == MOVED == -1,则须要进行扩容。
  • 若是都不知足,则利用 synchronized 锁写入数据。
  • 最后,若是数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

get 方法:

  • 根据计算出来的 hashcode 寻址,若是就在桶上那么直接返回值。
  • 若是是红黑树那就按照树的方式获取值。
  • 就不知足那就按照链表的方式遍历获取值。

1.8 在 1.7 的数据结构上作了大的改动,采用红黑树以后能够保证查询效率(O(logn)),甚至取消了 ReentrantLock 改成了 synchronized,这样能够看出在新版的 JDK 中对 synchronized 优化是很到位的。

HashMap、ConcurrentHashMap 1.7/1.8实现原理

hash()算法全解析

HashMap什么时候扩容:

当向容器添加元素的时候,会判断当前容器的元素个数,若是大于等于阈值---即大于当前数组的长度乘以加载因子的值的时候,就要自动扩容。

扩容的算法是什么:

扩容(resize)就是从新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组没法装载更多的元素时,对象就须要扩大数组的长度,以便能装入更多的元素。固然Java里的数组是没法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。

Hashmap如何解决散列碰撞(必问)?

Java中HashMap是利用“拉链法”处理HashCode的碰撞问题。在调用HashMap的put方法或get方法时,都会首先调用hashcode方法,去查找相关的key,当有冲突时,再调用equals方法。hashMap基于hasing原理,咱们经过put和get方法存取对象。当咱们将键值对传递给put方法时,他调用键对象的hashCode()方法来计算hashCode,而后找到bucket(哈希桶)位置来存储对象。当获取对象时,经过键对象的equals()方法找到正确的键值对,而后返回值对象。HashMap使用链表来解决碰撞问题,当碰撞发生了,对象将会存储在链表的下一个节点中。hashMap在每一个链表节点存储键值对对象。当两个不一样的键却有相同的hashCode时,他们会存储在同一个bucket位置的链表中。键对象的equals()来找到键值对。

Hashmap底层为何是线程不安全的?

  • 并发场景下使用时容易出现死循环,在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操做容易在一个桶上造成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环;
  • 在 1.7 中 hash 冲突采用的头插法造成的链表,在并发条件下会造成循环链表,一旦有查询落到了这个链表上,当获取不到值时就会死循环。

五、ArrayMap跟SparseArray在HashMap上面的改进?

HashMap要存储完这些数据将要不断的扩容,并且在此过程当中也须要不断的作hash运算,这将对咱们的内存空间形成很大消耗和浪费。

SparseArray:

SparseArray比HashMap更省内存,在某些条件下性能更好,主要是由于它避免了对key的自动装箱(int转为Integer类型),它内部则是经过两个数组来进行数据存储的,一个存储key,另一个存储value,为了优化性能,它内部对数据还采起了压缩的方式来表示稀疏数组的数据,从而节约内存空间,咱们从源码中能够看到key和value分别是用数组表示:

private int[] mKeys;
private Object[] mValues;
复制代码

同时,SparseArray在存储和读取数据时候,使用的是二分查找法。也就是在put添加数据的时候,会使用二分查找法和以前的key比较当前咱们添加的元素的key的大小,而后按照从小到大的顺序排列好,因此,SparseArray存储的元素都是按元素的key值从小到大排列好的。 而在获取数据的时候,也是使用二分查找法判断元素的位置,因此,在获取数据的时候很是快,比HashMap快的多。

ArrayMap:

ArrayMap利用两个数组,mHashes用来保存每个key的hash值,mArrray大小为mHashes的2倍,依次保存key和value。

mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
复制代码

当插入时,根据key的hashcode()方法获得hash值,计算出在mArrays的index位置,而后利用二分查找找到对应的位置进行插入,当出现哈希冲突时,会在index的相邻位置插入。

假设数据量都在千级之内的状况下:

一、若是key的类型已经肯定为int类型,那么使用SparseArray,由于它避免了自动装箱的过程,若是key为long类型,它还提供了一个LongSparseArray来确保key为long类型时的使用

二、若是key类型为其它的类型,则使用ArrayMap。

3、反射 (⭐⭐⭐)

一、说说你对Java反射的理解?

答:Java 中的反射首先是可以获取到Java中要反射类的字节码, 获取字节码有三种方法:

1.Class.forName(className)

2.类名.class

3.this.getClass()。

而后将字节码中的方法,变量,构造函数等映射成相应的Method、Filed、Constructor等类,这些类提供了丰富的方法能够被咱们所使用。

深刻解析Java反射(1) - 基础

Java基础之—反射(很是重要)

4、泛型 (⭐⭐)

一、简单介绍一下java中的泛型,泛型擦除以及相关的概念,解析与分派?

泛型是Java SE1.5的新特性,泛型的本质是参数化类型,也就是说所操的数据类型被指定为一个参数。这种参数类型能够用在类、接口和方法的建立中,分别称为泛型类、泛型接口、泛型方法。 Java语言引入泛型的好处是安全简单。

在Java SE 1.5以前,没有泛型的状况的下,经过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要作显式的强制类型转换,而这种转换是要求开发者实际参数类型能够预知的状况下进行的。对于强制类型换错误的状况,编译器可能不提示错误,在运行的时候出现异常,这是一个安全隐患。

泛型的好处是在编译的时候检查类型安全,而且全部的转换都是自动和隐式的,提升代码的重用率。

一、泛型的类型参数只能是类类型(包括自定义类),不是简单类型。

二、同一种泛型能够对应多个版本(由于参数类型是不确的),不一样版本的泛型类实例是不兼容的。

三、泛型的类型参数能够有多个。

四、泛型的参数类型能够使用extends语句,例如。习惯上称为“有界类型”。

五、泛型的参数类型还能够是通配符类型。例如Class<?> classType = Class.forName("java.lang.String");

泛型擦除以及相关的概念

泛型信息只存在代码编译阶段,在进入JVM以前,与泛型关的信息都会被擦除掉。

在类型擦除的时候,若是泛型类里的类型参数没有指定上限,则会被转成Object类型,若是指定了上限,则会被传转换成对应的类型上限。

Java中的泛型基本上都是在编译器这个层次来实现的。生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候擦除掉。这个过程就称为类型擦除。

类型擦除引发的问题及解决方法:

一、先检查,在编译,以及检查编译的对象和引用传递的题

二、自动类型转换

三、类型擦除与多态的冲突和解决方法

四、泛型类型变量不能是基本数据类型

五、运行时类型查询

六、异常中使用泛型的问题

七、数组(这个不属于类型擦除引发的问题)

九、类型擦除后的冲突

十、泛型在静态方法和静态类中的问题

5、注解 (⭐⭐)

一、说说你对Java注解的理解?

注解至关于一种标记,在程序中加了注解就等于为程序打上了某种标记。程序能够利用ava的反射机制来了解你的类及各类元素上有无何种标记,针对不一样的标记,就去作相应的事件。标记能够加在包,类,字段,方法,方法的参数以及局部变量上。

6、其它 (⭐⭐)

一、Java的char是两个字节,是怎么存Utf-8的字符的?

是否熟悉Java char和字符串(初级)

  • char是2个字节,utf-8是1~3个字节。
  • 字符集(字符集不是编码):ASCII码与Unicode码。
  • 字符 -> 0xd83dde00(码点)。

是否了解字符的映射和存储细节(中级)

人类认知:字符 => 字符集:0x4e2d(char) => 计算机存储(byte):01001110:4e、00101101:2d

编码:UTF-16

“中”.getBytes("utf-6"); -> fe ff 4e 2d:4个字节,其中前面的fe ff只是字节序标志。

是否能举一反三,横向对比其余语言(高级)

Python2的字符串:

  • byteString = "中"
  • unicodeString = u"中"

使人迷惑的字符串长度

emoij = u"表情"
print(len(emoji)
复制代码

Java与python 3.2及如下:2字节 python >= 3.3:1字节

注意:Java 9对latin字符的存储空间作了优化,但字符串长度仍是!= 字符数。

总结

  • Java char不存UTF-8的字节,而是UTF-16。
  • Unicode通用字符集占两个字节,例如“中”。
  • Unicode扩展字符集须要用一对char来表示,例如“表情”。
  • Unicode是字符集,不是编码,做用相似于ASCII码。
  • Java String的length不是字符数。

二、Java String能够有多长?

是否对字符串编解码有深刻了解(中级)

分配到栈:

String longString = "aaa...aaa";
复制代码

分配到堆:

byte[] bytes = loadFromFile(new File("superLongText.txt");
String superLongString = new String(bytes);
复制代码

是否对字符串在内存当中的存储形式有深刻了解(高级)

是否对Java虚拟机字节码有足够的了解(高级)

源文件:*.java

String longString = "aaa...aaa";
字节数 <= 65535
复制代码

字节码:*.class

CONSTANT_Utf8_info { 
    u1 tag; 
    u2 length;
    (0~65535) u1 bytes[length]; 
    最多65535个字节 
}
复制代码

javac的编译器有问题,< 65535应该改成< = 65535。

Java String 栈分配

  • 受字节码限制,字符串最终的MUTF-8字节数不超过65535。
  • Latin字符,受Javac代码限制,最多65534个。
  • 非Latin字符最终对应字节个数差别较大,最多字节个数是65535。
  • 若是运行时方法区设置较小,也会受到方法区大小的限制。

是否对java虚拟机指令有必定的认识(高级)

new String(bytes)内部是采用了一个字符数组,其对应的虚拟机指令是newarray [int] ,数组理论最大个数为Integer.MAX_VALUE,有些虚拟机须要一些头部信息,因此MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8。

Java String 堆分配

  • 受虚拟机指令限制,字符数理论上限为Integer.MAX_VALUE。
  • 受虚拟机实现限制,实际上限可能会小于Integer.MAX_VALUE。
  • 若是堆内存较小,也会受到堆内存的限制。

总结

Java String字面量形式

  • 字节码中CONSTANT_Utf8_info的限制
  • Javac源码逻辑的限制
  • 方法区大小的限制

Java String运行时建立在堆上的形式

  • Java虚拟机指令newarray的限制
  • Java虚拟机堆内存大小的限制

三、Java的匿名内部类有哪些限制?

考察匿名内部类的概念和用法(初级)

  • 匿名内部类的名字:没有人类认知意义上的名字
  • 只能继承一个父类或实现一个接口
  • 包名.OuterClass1,表示定位的第一个匿名内部类。外部类加N,N是匿名内部类的顺序。

考察语言规范以及语言的横向对比等(中级)

匿名内部类的继承结构:Java中的匿名内部类不能够继承,只有内部类才能够有实现继承、实现接口的特性。而Kotlin是的匿名内部类是支持继承的,如

val runnableFoo = object: Foo(),Runnable { 
        override fun run() { 
        
        } 
}
复制代码

做为考察内存泄漏的切入点(高级)

匿名内部类的构造方法(深刻源码字节码探索语言本质的能力):

  • 匿名内部类会默认持有外部类的引用,可能会致使内存泄漏。
  • 由编译器生成的。

其参数列表包括

  • 外部对象(定义在非静态域内)
  • 父类的外部对象(父类非静态)
  • 父类的构造方法参数(父类有构造方法且参数列表不为空)
  • 外部捕获的变量(方法体内有引用外部final变量)

Lambda转换(SAM类型,仅支持单一接口类型):

若是CallBack是一个interface,不是抽象类,则能够转换为Lambda表达式。

CallBack callBack = () -> { 
        ... 
};
复制代码

总结

  • 没有人类认知意义上的名字。
  • 只能继承一个父类或实现一个接口。
  • 父类是非静态的类型,则需父类外部实例来初始化。
  • 若是定义在非静态做用域内,会引用外部类实例。
  • 只能捕获外部做用域内的final变量。
  • 建立时只有单一方法的接口能够用Lambda转换。

技巧点拨

关注语言版本的变化:

  • 体现对技术的热情
  • 体现好学的品质
  • 显得专业

四、Java中对异常是如何进行分类的?

异常总体分类:

Java异常结构中定义有Throwable类。 Exception和Error为其子类。

Error是程序没法处理的错误,好比OutOfMemoryError、StackOverflowError。这些异常发生时, Java虚拟机(JVM)通常会选择线程终止。

Exception是程序自己能够处理的异常,这种异常分两大类运行时异常和非运行时异常,程序中应当尽量去处理这些异常。

运行时异常都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等, 这些异常是不检查异常,程序中能够选择捕获处理,也能够不处理。这些异常通常是由程序逻辑错误引发的, 程序应该从逻辑角度尽量避免这类异常的发生。

异常处理的两个基本原则:

一、尽可能不要捕获相似 Exception 这样的通用异常,而是应该捕获特定异常。

二、不要生吞异常。

NoClassDefFoundError 和 ClassNotFoundException 有什么区别?

ClassNotFoundException的产生缘由主要是: Java支持使用反射方式在运行时动态加载类,例如使用Class.forName方法来动态地加载类时,能够将类名做为参数传递给上述方法从而将指定类加载到JVM内存中,若是这个类在类路径中没有被找到,那么此时就会在运行时抛出ClassNotFoundException异常。 解决该问题须要确保所需的类连同它依赖的包存在于类路径中,常见问题在于类名书写错误。 另外还有一个致使ClassNotFoundException的缘由就是:当一个类已经某个类加载器加载到内存中了,此时另外一个类加载器又尝试着动态地从同一个包中加载这个类。经过控制动态类加载过程,能够避免上述状况发生。

NoClassDefFoundError产生的缘由在于: 若是JVM或者ClassLoader实例尝试加载(能够经过正常的方法调用,也多是使用new来建立新的对象)类的时候却找不到类的定义。要查找的类在编译的时候是存在的,运行的时候却找不到了。这个时候就会致使NoClassDefFoundError. 形成该问题的缘由多是打包过程漏掉了部分类,或者jar包出现损坏或者篡改。解决这个问题的办法是查找那些在开发期间存在于类路径下但在运行期间却不在类路径下的类。

五、String 为何要设计成不可变的?

String是不可变的(修改String时,不会在原有的内存地址修改,而是从新指向一个新对象),String用final修饰,不可继承,String本质上是个final的char[]数组,因此char[]数组的内存地址不会被修改,并且String 也没有对外暴露修改char[]数组的方法。不可变性能够保证线程安全以及字符串串常量池的实现。

六、Java里的幂等性了解吗?

幂等性本来是数学上的一个概念,即:f(x) = f(f(x)),对同一个系统,使用一样的条件,一次请求和重复的屡次请求对系统资源的影响是一致的。

幂等性最为常见的应用就是电商的客户付款,试想一下若是你在付款的时候由于网络等各类问题失败了,而后去重复的付了一次,是一种多么糟糕的体验。幂等性就是为了解决这样的问题。

实现幂等性能够使用Token机制。

核心思想是为每一次操做生成一个惟一性的凭证,也就是token。一个token在操做的每个阶段只有一次执行权,一旦执行成功则保存执行结果。对重复的请求,返回同一个结果。

例如:电商平台上的订单id就是最适合的token。当用户下单时,会经历多个环节,好比生成订单,减库存,减优惠券等等。每个环节执行时都先检测一下该订单id是否已经执行过这一步骤,对未执行的请求,执行操做并缓存结果,而对已经执行过的id,则直接返回以前的执行结果,不作任何操 做。这样能够在最大程度上避免操做的重复执行问题,缓存起来的执行结果也能用于事务的控制等。

七、为何Java里的匿名内部类只能访问final修饰的外部变量?

匿名内部类用法:

public class TryUsingAnonymousClass {
    public void useMyInterface() {
        final Integer number = 123;
        System.out.println(number);

        MyInterface myInterface = new MyInterface() {
            @Override
            public void doSomething() {
                System.out.println(number);
            }
        };
        myInterface.doSomething();

        System.out.println(number);
    }
}
复制代码

编译后的结果

class TryUsingAnonymousClass$1
        implements MyInterface {
    private final TryUsingAnonymousClass this$0;
    private final Integer paramInteger;

    TryUsingAnonymousClass$1(TryUsingAnonymousClass this$0, Integer paramInteger) {
        this.this$0 = this$0;
        this.paramInteger = paramInteger;
    }

    public void doSomething() {
        System.out.println(this.paramInteger);
    }
}
复制代码

由于匿名内部类最终会编译成一个单独的类,而被该类使用的变量会以构造函数参数的形式传递给该类,例如:Integer paramInteger,若是变量不定义成final的,paramInteger在匿名内部类被能够被修改,进而形成和外部的paramInteger不一致的问题,为了不这种不一致的状况,因次Java规定匿名内部类只能访问final修饰的外部变量。

八、讲一下Java的编码方式?

为何须要编码

计算机存储信息的最小单元是一个字节即8bit,因此能示的范围是0~255,这个范围没法保存全部的字符,因此要一个新的数据结构char来表示这些字符,从char到byte须要编码。

常见的编码方式有如下几种:

ASCII:总共有 128 个,用一个字节的低 7 位表示,031 是控制字符如换行回车删除等;32126 是打印字符,能够经过键盘输入而且可以显示出来。

GBK:码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示 21003 个汉字,它的编码是和 GB2312 兼容的,也就是说用 GB2312 编码的汉字能够用 GBK 来解码,而且不会有乱码。

UTF-16:UTF-16 具体定义了 Unicode 字符在计算机中存取方法。UTF-16 用两个字节来表示 Unicode 转化格式,这个是定长的表示方法,不论什么字符均可以用两个字节表示,两个字节是 16 个 bit,因此叫 UTF-16。UTF-16 表示字符很是方便,每两个字节表示一个字符,这个在字符串操做时就大大简化了操做,这也是 Java 以 UTF-16 做为内存的字符存储格式的一个很重要的缘由。

UTF-8:统一采用两个字节表示一个字符,虽然在表示上很是简单方便,可是也有其缺点,有很大一部分字符用一个字节就能够表示的如今要两个字节表示,存储空间放大了一倍,在如今的网络带宽还很是有限的今天,这样会增大网络传输的流量,并且也不必。而 UTF-8 采用了一种变长技术,每一个编码区域有不一样的字码长度。不一样类型的字符能够是由 1~6 个字节组成。

Java中须要编码的地方通常都在字符到字节的转换上,这个通常包括磁盘IO和网络IO。

Reader 类是 Java 的 I/O 中读字符的父类,而InputStream 类是读字节的父类,InputStreamReader类就是关联字节到字符的桥梁,它负责在 I/O 过程当中处理读取字节到字符的转换,而具体字节到字符解码实现由 StreamDecoder 去实现,在 StreamDecoder 解码过程当中必须由用户指定 Charset 编码格式。

九、String,StringBuffer,StringBuilder有哪些不一样?

三者在执行速度方面的比较:StringBuilder >  StringBuffer  >  String

String每次变化一个值就会开辟一个新的内存空间

StringBuilder:线程非安全的

StringBuffer:线程安全的

对于三者使用的总结:

1.若是要操做少许的数据用 String。

2.单线程操做字符串缓冲区下操做大量数据用 StringBuilder。

3.多线程操做字符串缓冲区下操做大量数据用 StringBuffer。

String 是 Java 语言很是基础和重要的类,提供了构造和管理字符串的各类基本逻辑。它是典型的 Immutable 类,被声明成为 final class,全部属性也都是 final 的。也因为它的不可变性,相似拼接、裁剪字符串等动做,都会产生新的 String 对象。因为字符串操做的广泛性,因此相关操做的效率每每对应用性能有明显影响。

StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,咱们能够用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,因此除非有线程安全的须要,否则仍是推荐使用它的后继者,也就是 StringBuilder。

StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,可是它去掉了线程安全的部分,有效减少了开销,是绝大部分状况下进行字符串拼接的首选。

十、什么是内部类?内部类的做用。

内部类能够有多个实例,每一个实例都有本身的状态信息,而且与其余外围对象的信息相互独立。

在单个外围类中,可让多个内部类以不一样的方式实现同一个接口,或者继承同一个类。

建立内部类对象并不依赖于外围类对象的建立。

内部类并无使人迷惑的“is-a”关系,他就是一个独立的实体。

内部类提供了更好的封装,除了该外围类,其余类都不能访问。。

十一、抽象类和接口区别?

共同点

  • 是上层的抽象层。
  • 都不能被实例化。
  • 都能包含抽象的方法,这些抽象的方法用于描述类具有的功能,可是不提供具体的实现。

区别:

  • 一、在抽象类中能够写非抽象的方法,从而避免在子类中重复书写他们,这样能够提升代码的复用性,这是抽象类的优点,接口中只能有抽象的方法。
  • 二、多继承:一个类只能继承一个直接父类,这个父类能够是具体的类也但是抽象类,可是一个类能够实现多个接口。
  • 三、抽象类能够有默认的方法实现,接口根本不存在方法的实现。
  • 四、子类使用extends关键字来继承抽象类。若是子类不是抽象类的话,它须要提供抽象类中全部声明方法的实现。子类使用关键字implements来实现接口。它须要提供接口中全部声明方法的实现。
  • 五、构造器:抽象类能够有构造器,接口不能有构造器。
  • 六、和普通Java类的区别:除了你不能实例化抽象类以外,抽象类和普通Java类没有任何区别,接口是彻底不一样的类型。
  • 七、访问修饰符:抽象方法能够有public、protected和default修饰符,接口方法默认修饰符是public。你不能够使用其它修饰符。
  • 八、main方法:抽象方法能够有main方法而且咱们能够运行它接口没有main方法,所以咱们不能运行它。
  • 九、速度:抽象类比接口速度要快,接口是稍微有点慢的,由于它须要时间去寻找在类中实现的方法。
  • 十、添加新方法:若是你往抽象类中添加新的方法,你能够给它提供默认的实现。所以你不须要改变你如今的代码。若是你往接口中添加方法,那么你必须改变实现该接口的类。

十二、接口的意义?

规范、扩展、回调。

1三、父类的静态方法可否被子类重写?

不能。子类继承父类后,用相同的静态方法和非静态方法,这时非静态方法覆盖父类中的方法(即方法重写),父类的该静态方法被隐藏(若是对象是父类则调用该隐藏的方法),另外子类可继承父类的静态与非静态方法,至于方法重载我以为它其中一要素就是在同一类中,不能说父类中的什么方法与子类里的什么方法是方法重载的体现。

1四、抽象类的意义?

为其子类提供一个公共的类型,封装子类中的重复内容,定义抽象方法,子类虽然有不一样的实现 可是定义是一致的。

1五、静态内部类、非静态内部类的理解?

静态内部类:只是为了下降包的深度,方便类的使用,静态内部类适用于包含在类当中,但又不依赖与外在的类,不用使用外在类的非静态属性和方法,只是为了方便管理类结构而定义。在建立静态内部类的时候,不须要外部类对象的引用。

非静态内部类:持有外部类的引用,能够自由使用外部类的全部变量和方法。

1六、为何复写equals方法的同时须要复写hashcode方法,前者相同后者是否相同,反过来呢?为何?

要考虑到相似HashMap、HashTable、HashSet的这种散列的数据类型的运用,当咱们重写equals时,是为了用自身的方式去判断两个自定义对象是否相等,然而若是此时恰好须要咱们用自定义的对象去充当hashmap的键值使用时,就会出现咱们认为的同一对象,却由于hash值不一样而致使hashmap中存了两个对象,从而才须要进行hashcode方法的覆盖。

1七、equals 和 hashcode 的关系?

hashcode和equals的约定关系以下:

  • 一、若是两个对象相等,那么他们必定有相同的哈希值(hashcode)。

  • 二、若是两个对象的哈希值相等,那么这两个对象有可能相等也有可能不相等。(须要再经过equals来判断)

1八、java为何跨平台?

由于Java程序编译以后的代码不是能被硬件系统直接运行的代码,而是一种“中间码”——字节码。而后不一样的硬件平台上安装有不一样的Java虚拟机(JVM),由JVM来把字节码再“翻译”成所对应的硬件平台可以执行的代码。所以对于Java编程者来讲,不须要考虑硬件平台是什么。因此Java能够跨平台。

1九、浮点数的精准计算

BigDecimal类进行商业计算,Float和Double只能用来作科学计算或者是工程计算。

20、final,finally,finalize的区别?

final 能够用来修饰类、方法、变量,分别有不一样的意义,final 修饰的 class 表明不能够继承扩展,final 的变量是不能够修改的,而 final 的方法也是不能够重写的(override)。

finally 则是 Java 保证重点代码必定要被执行的一种机制。咱们能够使用 try-finally 或者 try-catch-finally 来进行相似关闭 JDBC 链接、保证 unlock 锁等动做。

finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制如今已经不推荐使用,而且在 JDK 9 开始被标记为 deprecated。Java 平台目前在逐步使用 java.lang.ref.Cleaner 来替换掉原有的 finalize 实现。Cleaner 的实现利用了幻象引用(PhantomReference),这是一种常见的所谓 post-mortem 清理机制。利用幻象引用和引用队列,咱们能够保证对象被完全销毁前作一些相似资源回收的工做,好比关闭文件描述符(操做系统有限的资源),它比 finalize 更加轻量、更加可靠。

2一、静态内部类的设计意图

静态内部类与非静态内部类之间存在一个最大的区别:非静态内部类在编译完成以后会隐含地保存着一个引用,该引用是指向建立它的外围内,可是静态内部类却没有。

没有这个引用就意味着:

它的建立是不须要依赖于外围类的。 它不能使用任何外围类的非static成员变量和方法。

2二、Java中对象的生命周期

在Java中,对象的生命周期包括如下几个阶段:

1.建立阶段(Created)

JVM 加载类的class文件 此时全部的static变量和static代码块将被执行 加载完成后,对局部变量进行赋值(先父后子的顺序) 再执行new方法 调用构造函数 一旦对象被建立,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段。

2.应用阶段(In Use)

对象至少被一个强引用持有着。

3.不可见阶段(Invisible)

当一个对象处于不可见阶段时,说明程序自己再也不持有该对象的任何强引用,虽然该这些引用仍然是存在着的。 简单说就是程序的执行已经超出了该对象的做用域了。

4.不可达阶段(Unreachable)

对象处于不可达阶段是指该对象再也不被任何强引用所持有。 与“不可见阶段”相比,“不可见阶段”是指程序再也不持有该对象的任何强引用,这种状况下,该对象仍可能被JVM等系统下的某些已装载的静态变量或线程或JNI等强引用持有着,这些特殊的强引用被称为”GC root”。存在着这些GC root会致使对象的内存泄露状况,没法被回收。

5.收集阶段(Collected)

当垃圾回收器发现该对象已经处于“不可达阶段”而且垃圾回收器已经对该对象的内存空间从新分配作好准备时,则对象进入了“收集阶段”。若是该对象已经重写了finalize()方法,则会去执行该方法的终端操做。

6.终结阶段(Finalized)

当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。

7.对象空间重分配阶段(De-allocated)

垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象完全消失了,称之为“对象空间从新分配阶段。

2三、静态属性和静态方法是否能够被继承?是否能够被重写?以及缘由?

结论:java中静态属性和静态方法能够被继承,可是不能够被重写而是被隐藏。

缘由:

1). 静态方法和属性是属于类的,调用的时候直接经过类名.方法名完成,不须要继承机制便可以调用。若是子类里面定义了静态方法和属性,那么这时候父类的静态方法或属性称之为"隐藏"。若是你想要调用父类的静态方法和属性,直接经过父类名.方法或变量名完成,至因而否继承一说,子类是有继承静态方法和属性,可是跟实例方法和属性不太同样,存在"隐藏"的这种状况。

2). 多态之因此可以实现依赖于继承、接口和重写、重载(继承和重写最为关键)。有了继承和重写就能够实现父类的引用指向不一样子类的对象。重写的功能是:"重写"后子类的优先级要高于父类的优先级,可是“隐藏”是没有这个优先级之分的。

3). 静态属性、静态方法和非静态的属性均可以被继承和隐藏而不能被重写,所以不能实现多态,不能实现父类的引用能够指向不一样子类的对象。非静态方法能够被继承和重写,所以能够实现多态。

2四、object类的equal 和hashcode 方法重写,为何?

在Java API文档中关于hashCode方法有如下几点规定(原文来自java深刻解析一书):

一、在java应用程序执行期间,若是在equals方法比较中所用的信息没有被修改,那么在同一个对象上屡次调用hashCode方法时必须一致地返回相同的整数。若是屡次执行同一个应用时,不要求该整数必须相同。

二、若是两个对象经过调用equals方法是相等的,那么这两个对象调用hashCode方法必须返回相同的整数。

三、若是两个对象经过调用equals方法是不相等的,不要求这两个对象调用hashCode方法必须返回不一样的整数。可是程序员应该意识到对不一样的对象产生不一样的hash值能够提供哈希表的性能。

2五、java中==和equals和hashCode的区别?

默认状况下也就是从超类Object继承而来的equals方法与‘==’是彻底等价的,比较的都是对象的内存地址,但咱们能够重写equals方法,使其按照咱们的需求的方式进行比较,如String类重写了equals方法,使其比较的是字符的序列,而再也不是内存地址。在java的集合中,判断两个对象是否相等的规则是:

1.判断两个对象的hashCode是否相等。
  2.判断两个对象用equals运算是否相等。
复制代码

2六、Java的四种引用及使用场景?

  • 强引用(FinalReference):在内存不足时不会被回收。日常用的最多的对象,如新建立的对象。
  • 软引用(SoftReference):在内存不足时会被回收。用于实现内存敏感的高速缓存。
  • 弱引用(WeakReferenc):只要GC回收器发现了它,就会将之回收。用于Map数据结构中,引用占用内存空间较大的对象。
  • 虚引用(PhantomReference):在回收以前,会被放入ReferenceQueue,JVM不会自动将该referent字段值设置成null。其它引用被JVM回收以后才会被放入ReferenceQueue中。用于实现一个对象被回收以前作一些清理工做。

2七、类的加载过程,Person person = new Person();为例进行说明。

1).由于new用到了Person.class,因此会先找到Person.class文件,并加载到内存中;

2).执行该类中的static代码块,若是有的话,给Person.class类进行初始化;

3).在堆内存中开辟空间分配内存地址;

4).在堆内存中创建对象的特有属性,并进行默认初始化;

5).对属性进行显示初始化;

6).对对象进行构造代码块初始化;

7).对对象进行与之对应的构造函数进行初始化;

8).将内存地址付给栈内存中的p变量。

2八、JAVA常量池

Interger中的128(-128~127)

a.当数值范围为-128~127时:若是两个new出来的Integer对象,即便值相同,经过“==”比较结果为false,但两个对直接赋值,则经过“==”比较结果为“true,这一点与String很是类似。

b.当数值不在-128~127时,不管经过哪一种方式,即便两对象的值相等,经过“==”比较,其结果为false;

c.当一个Integer对象直接与一个int基本数据类型经过“==”比较,其结果与第一点相同;

d.Integer对象的hash值为数值自己;

为何是-128-127?

在Integer类中有一个静态内部类IntegerCache,在IntegrCache类中有一个Integer数组,用以缓存当前数值范围为-128~127时的Integer对象。

2九、在重写equals方法时,须要遵循哪些约定,具体介绍一下?

重写equals方法时须要遵循通用约定:自反性、对称性、传递性、一致性、非空性

1)自反性

对于任何非null的引用值x,x.equals(x)必须返回true。---这一点基本上不会有啥问题

2)对称性

对于任何非null的引用值x和y,当且仅当x.equals(y)为true时,y.equals(x)也为true。

3)传递性

对于任何非null的引用值x、y、z。若是x.equals(y)==true,y.equals(z)==true,那么x.equals(z)==true。

4) 一致性

对于任何非null的引用值x和y,只要equals的比较操做在对象所用的信息没有被修改,那么屡次调用x.equals(y)就会一致性地返回true,或者一致性的返回false。

5)非空性

全部比较的对象都不能为空。

30、深拷贝和浅拷贝的区别

3一、Integer类对int的优化

Java并发

1、线程池相关 (⭐⭐⭐)

一、什么是线程池,如何使用?为何要使用线程池?

答:线程池就是事先将多个线程对象放到一个容器中,使用的时候就不用new线程而是直接去池中拿线程便可,节 省了开辟子线程的时间,提升了代码执行效率。

二、Java中的线程池共有几种?

Java有四种线程池:

第一种:newCachedThreadPool

不固定线程数量,且支持最大为Integer.MAX_VALUE的线程数量:

public static ExecutorService newCachedThreadPool() {
    // 这个线程池corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE
    // 意思也就是说来一个任务就建立一个woker,回收时间是60s
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                60L, TimeUnit.SECONDS,
                                new SynchronousQueue<Runnable>());
}
复制代码

可缓存线程池:

一、线程数无限制。 二、有空闲线程则复用空闲线程,若无空闲线程则新建线程。 三、必定程序减小频繁建立/销毁线程,减小系统开销。

第二种:newFixedThreadPool

一个固定线程数量的线程池:

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    // corePoolSize跟maximumPoolSize值同样,同时传入一个无界阻塞队列
    // 该线程池的线程会维持在指定线程数,不会进行回收
    return new ThreadPoolExecutor(nThreads, nThreads,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory);
}
复制代码

定长线程池:

一、可控制线程最大并发数(同时执行的线程数)。 二、超出的线程会在队列中等待。

第三种:newSingleThreadExecutor

能够理解为线程数量为1的FixedThreadPool:

public static ExecutorService newSingleThreadExecutor() {
    // 线程池中只有一个线程进行任务执行,其余的都放入阻塞队列
    // 外面包装的FinalizableDelegatedExecutorService类实现了finalize方法,在JVM垃圾回收的时候会关闭线程池
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
复制代码

单线程化的线程池:

一、有且仅有一个工做线程执行任务。 二、全部任务按照指定顺序执行,即遵循队列的入队出队规则。

第四种:newScheduledThreadPool。

支持定时以指定周期循环执行任务:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
复制代码

注意:前三种线程池是ThreadPoolExecutor不一样配置的实例,最后一种是ScheduledThreadPoolExecutor的实例。

三、线程池原理?

从数据结构的角度来看,线程池主要使用了阻塞队列(BlockingQueue)和HashSet集合构成。 从任务提交的流程角度来看,对于使用线程池的外部来讲,线程池的机制是这样的:

一、若是正在运行的线程数 < coreSize,立刻建立核心线程执行该task,不排队等待;
二、若是正在运行的线程数 >= coreSize,把该task放入阻塞队列;
三、若是队列已满 && 正在运行的线程数 < maximumPoolSize,建立新的非核心线程执行该task;
四、若是队列已满 && 正在运行的线程数 >= maximumPoolSize,线程池调用handler的reject方法拒绝本次提交。
复制代码

理解记忆:1-2-3-4对应(核心线程->阻塞队列->非核心线程->handler拒绝提交)。

线程池的线程复用:

这里就须要深刻到源码addWorker():它是建立新线程的关键,也是线程复用的关键入口。最终会执行到runWoker,它取任务有两个方式:

  • firstTask:这是指定的第一个runnable可执行任务,它会在Woker这个工做线程中运行执行任务run。而且置空表示这个任务已经被执行。
  • getTask():这首先是一个死循环过程,工做线程循环直到可以取出Runnable对象或超时返回,这里的取的目标就是任务队列workQueue,对应刚才入队的操做,有入有出。

其实就是任务在并不仅执行建立时指定的firstTask第一任务,还会从任务队列的中经过getTask()方法本身主动去取任务执行,并且是有/无时间限定的阻塞等待,保证线程的存活。

信号量

semaphore 可用于进程间同步也可用于同一个进程间的线程同步。

能够用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段以前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。

四、线程池都有哪几种工做队列?

一、ArrayBlockingQueue

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

二、LinkedBlockingQueue

一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量一般要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor使用了这个队列。

三、SynchronousQueue

一个不存储元素的阻塞队列。每一个插入操做必须等到另外一个线程调用移除操做,不然插入操做一直处于阻塞状态,吞吐量一般要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

四、PriorityBlockingQueue

一个具备优先级的无限阻塞队列。

五、怎么理解无界队列和有界队列?

有界队列

1.初始的poolSize < corePoolSize,提交的runnable任务,会直接作为new一个Thread的参数,立马执行 。 2.当提交的任务数超过了corePoolSize,会将当前的runable提交到一个block queue中。 3.有界队列满了以后,若是poolSize < maximumPoolsize时,会尝试new 一个Thread的进行救急处理,立马执行对应的runnable任务。 4.若是3中也没法处理了,就会走到第四步执行reject操做。

无界队列

与有界队列相比,除非系统资源耗尽,不然无界的任务队列不存在任务入队失败的状况。当有新的任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续增长,若后续仍有新的任务加入,而没有空闲的线程资源,则任务直接进入队列等待。若任务建立和处理的速度差别很大,无界队列会保持快速增加,直到耗尽系统内存。 当线程池的任务缓存队列已满而且线程池中的线程数目达到maximumPoolSize,若是还有任务到来就会采起任务拒绝策略。

六、多线程中的安全队列通常经过什么实现?

Java提供的线程安全的Queue能够分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非阻塞队列的典型例子是ConcurrentLinkedQueue.

对于BlockingQueue,想要实现阻塞功能,须要调用put(e) take() 方法。而ConcurrentLinkedQueue是基于连接节点的、无界的、线程安全的非阻塞队列。

2、Synchronized、volatile、Lock(ReentrantLock)相关 (⭐⭐⭐)

一、synchronized的原理?

synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现,而 synchronized 同步方法使用了ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

在 Java 6 以前,Monitor 的实现彻底是依靠操做系统内部的互斥锁,由于须要进行用户态到内核态的切换,因此同步操做是一个无差异的重量级操做。

现代的(Oracle)JDK 中,JVM 对此进行了大刀阔斧地改进,提供了三种不一样的 Monitor 实现,也就是常说的三种不一样的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不一样的竞争情况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。

当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操做,在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,因此并不涉及真正的互斥锁。这样作的假设是基于在不少应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁能够下降无竞争开销。

若是有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就须要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操做 Mark Word 来试图获取锁,若是重试成功,就使用普通的轻量级锁;不然,进一步升级为重量级锁(可能会先进行自旋锁升级,若是失败再尝试重量级锁升级)。

我注意到有的观点认为 Java 不会进行锁降级。实际上据我所知,锁降级确实是会发生的,当 JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,而后试图进行降级。

二、Synchronized优化后的锁机制简单介绍一下,包括自旋锁、偏向锁、轻量级锁、重量级锁?

自旋锁:

线程自旋说白了就是让cpu在作无用功,好比:能够执行几回for循环,能够执行几条空的汇编指令,目的是占着CPU不放,等待获取锁的机会。若是旋的时间过长会影响总体性能,时间太短又达不到延迟阻塞的目的。

偏向锁

偏向锁就是一旦线程第一次得到了监视对象,以后让监视对象“偏向”这个线程,以后的屡次调用则能够避免CAS操做,说白了就是置个变量,若是发现为true则无需再走各类加锁/解锁流程。

轻量级锁:

轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的状况下,当第二个线程加入锁竞争用的时候,偏向锁就会升级为轻量级锁;

重量级锁

重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具有Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责作互斥,后一个用于作线程同步。

三、谈谈对Synchronized关键字涉及到的类锁,方法锁,重入锁的理解?

synchronized修饰静态方法获取的是类锁(类的字节码文件对象)。

synchronized修饰普通方法或代码块获取的是对象锁。这种机制确保了同一时刻对于每个类实例,其全部声明为 synchronized 的成员函数中至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。

它俩是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的!

public class Widget {

    // 锁住了
    public synchronized void doSomething() {
        ...
    }
}

public class LoggingWidget extends Widget {

    // 锁住了
    public synchronized void doSomething() {
        System.out.println(toString() + ": calling doSomething");
        super.doSomething();
    }
}
复制代码

由于锁的持有者是“线程”,而不是“调用”。

线程A已是有了LoggingWidget实例对象的锁了,当再须要的时候能够继续**“开锁”**进去的!

这就是内置锁的可重入性。

四、wait、sleep的区别和notify运行过程。

wait、sleep的区别

最大的不一样是在等待时 wait 会释放锁,而 sleep 一直持有锁。wait 一般被用于线程间交互,sleep 一般被用于暂停执行。

  • 首先,要记住这个差异,“sleep是Thread类的方法,wait是Object类中定义的方法”。尽管这两个方法都会影响线程的执行行为,可是本质上是有区别的。
  • Thread.sleep不会致使锁行为的改变,若是当前线程是拥有锁的,那么Thread.sleep不会让线程释放锁。若是可以帮助你记忆的话,能够简单认为和锁相关的方法都定义在Object类中,所以调用Thread.sleep是不会影响锁的相关行为。
  • Thread.sleep和Object.wait都会暂停当前的线程,对于CPU资源来讲,无论是哪一种方式暂停的线程,都表示它暂时再也不须要CPU的执行时间。OS会将执行时间分配给其它线程。区别是,调用wait后,须要别的线程执行notify/notifyAll才可以从新得到CPU执行时间。
  • 线程的状态参考 Thread.State的定义。新建立的可是没有执行(尚未调用start())的线程处于“就绪”,或者说Thread.State.NEW状态。
  • Thread.State.BLOCKED(阻塞)表示线程正在获取锁时,由于锁不能获取到而被迫暂停执行下面的指令,一直等到这个锁被别的线程释放。BLOCKED状态下线程,OS调度机制须要决定下一个可以获取锁的线程是哪一个,这种状况下,就是产生锁的争用,不管如何这都是很耗时的操做。

notify运行过程

当线程A(消费者)调用wait()方法后,线程A让出锁,本身进入等待状态,同时加入锁对象的等待队列。 线程B(生产者)获取锁后,调用notify方法通知锁对象的等待队列,使得线程A从等待队列进入阻塞队列。 线程A进入阻塞队列后,直至线程B释放锁后,线程A竞争获得锁继续从wait()方法后执行。

五、synchronized关键字和Lock的区别你知道吗?为何Lock的性能好一些?

类别 synchronized Lock(底层实现主要是Volatile + CAS)
存在层次 Java的关键字,在jvm层面上 是一个类
锁的释放 一、已获取锁的线程执行完同步代码,释放锁 二、线程执行发生异常,jvm会让线程释放锁。 在finally中必须释放锁,否则容易形成线程死锁。
锁的获取 假设A线程得到锁,B线程等待。若是A线程阻塞,B线程会一直等待。 分状况而定,Lock有多个锁获取的方式,大体就是能够尝试得到锁,线程能够不用一直等待
锁状态 没法判断 能够判断
锁类型 可重入 不可中断 非公平 可重入 可判断 可公平(二者皆可)
性能 少许同步 大量同步

Lock(ReentrantLock)的底层实现主要是Volatile + CAS(乐观锁),而Synchronized是一种悲观锁,比较耗性能。可是在JDK1.6之后对Synchronized的锁机制进行了优化,加入了偏向锁、轻量级锁、自旋锁、重量级锁,在并发量不大的状况下,性能可能优于Lock机制。因此建议通常请求并发量不大的状况下使用synchronized关键字。

六、volatile原理。

在《Java并发编程:核心理论》一文中,咱们已经提到可见性、有序性及原子性问题,一般状况下咱们能够经过Synchronized关键字来解决这些个问题,不过若是对Synchonized原理有了解的话,应该知道Synchronized是一个较重量级的操做,对系统的性能有比较大的影响,因此若是有其余解决方案,咱们一般都避免使用Synchronized来解决问题。

而volatile关键字就是Java中提供的另外一种解决可见性有序性问题的方案。对于原子性,须要强调一点,也是你们容易误解的一点:对volatile变量的单次读/写操做可保证原子性的,如long和double类型变量,可是并不能保证i++这种操做的原子性,由于本质上i++是读、写两次操做。

volatile也是互斥同步的一种实现,不过它很是的轻量级。

volatile 的意义?

  • 防止CPU指令重排序

volatile有两条关键的语义:

保证被volatile修饰的变量对全部线程都是可见的

禁止进行指令重排序

要理解volatile关键字,咱们得先从Java的线程模型开始提及。如图所示:

 

image

 

 

Java内存模型规定了全部字段(这些字段包括实例字段、静态字段等,不包括局部变量、方法参数等,由于这些是线程私有的,并不存在竞争)都存在主内存中,每一个线程会 有本身的工做内存,工做内存里保存了线程所使用到的变量在主内存里的副本拷贝,线程对变量的操做只能在工做内存里进行,而不能直接读写主内存,固然不一样内存之间也 没法直接访问对方的工做内存,也就是说主内存是线程传值的媒介。

咱们来理解第一句话:

保证被volatile修饰的变量对全部线程都是可见的
复制代码

如何保证可见性?

被volatile修饰的变量在工做内存修改后会被强制写回主内存,其余线程在使用时也会强制从主内存刷新,这样就保证了一致性。

关于“保证被volatile修饰的变量对全部线程都是可见的”,有种常见的错误理解:

  • 因为volatile修饰的变量在各个线程里都是一致的,因此基于volatile变量的运算在多线程并发的状况下是安全的。

这句话的前半部分是对的,后半部分却错了,所以它忘记考虑变量的操做是否具备原子性这一问题。

举个例子:

private volatile int start = 0;

private void volatile Keyword() {

    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                start++;
            }
        }
    };

    for (int i = 0; i < 10; i++) {
        Thread thread = new Thread(runnable);
        thread.start();
    }
    Log.d(TAG, "start = " + start);
}
复制代码

 

image

 

 

这段代码启动了10个线程,每次10次自增,按道理最终结果应该是100,可是结果并不是如此。

为何会这样?

仔细看一下start++,它其实并不是一个原子操做,简单来看,它有两步:

一、取出start的值,由于有volatile的修饰,这时候的值是正确的。

二、自增,可是自增的时候,别的线程可能已经把start加大了,这种状况下就有可能把较小的start写回主内存中。 因此volatile只能保证可见性,在不符合如下场景下咱们依然须要经过加锁来保证原子性:

  • 运算结果并不依赖变量当前的值,或者只有单一线程修改变量的值。(要么结果不依赖当前值,要么操做是原子性的,要么只要一个线程修改变量的值)
  • 变量不须要与其余状态变量共同参与不变约束 比方说咱们会在线程里加个boolean变量,来判断线程是否中止,这种状况就很是适合使用volatile。

咱们再来理解第二句话。

禁止进行指令重排序

什么是指令重排序?

  • 指令重排序是指指令乱序执行,即在条件容许的状况下直接运行当前有能力当即执行的后续指令,避开为获取一条指令所需数据而形成的等待,经过乱序执行的技术提供执行效率。

  • 指令重排序会在被volatile修饰的变量的赋值操做前,添加一个内存屏障,指令重排序时不能把后面的指令重排序移到内存屏障以前的位置。

七、synchronized 和 volatile 关键字的做用和区别。

Volatile

1)保证了不一样线程对这个变量进行操做时的可见性即一个线程修改了某个变量的值,这新值对其余线程来是当即可见的。

2)禁止进行指令重排序。

做用

volatile 本质是在告诉jvm当前变量在寄存器(工做内存)中的值是不肯定的,需从主存中读取;synchronized则是锁定当前变量,只有当前线程能够访问该变量,其它线程被阻塞住。

区别

1.volatile 仅能使用在变量级别;synchronized则能够使用在变量、方法、和类级别的。

2.volatile 仅能实现变量的修改可见性,并不能保证原子性;synchronized 则能够保证变量的修改可见性和原子性。

3.volatile 不会形成线程的阻塞;synchronized 可能会形成线程的阻塞。

4.volatile 标记的变量不会被编译器优化;synchronized标记的变量能够被编译器优化。

八、ReentrantLock的内部实现

ReentrantLock实现的前提就是AbstractQueuedSynchronizer,简称AQS,是java.util.concurrent的核心,CountDownLatch、FutureTask、Semaphore、ReentrantLock等都有一个内部类是这个抽象类的子类。因为AQS是基于FIFO队列的实现,所以必然存在一个个节点,Node就是一个节点,Node有两种模式:共享模式和独占模式。ReentrantLock是基于AQS的,AQS是Java并发包中众多同步组件的构建基础,它经过一个int类型的状态变量state和一个FIFO队列来完成共享资源的获取,线程的排队等待等。AQS是个底层框架,采用模板方法模式,它定义了通用的较为复杂的逻辑骨架,好比线程的排队,阻塞,唤醒等,将这些复杂但实质通用的部分抽取出来,这些都是须要构建同步组件的使用者无需关心的,使用者仅需重写一些简单的指定的方法便可(其实就是对于共享变量state的一些简单的获取释放的操做)。AQS的子类通常只须要重写tryAcquire(int arg)和tryRelease(int arg)两个方法便可。

ReentrantLock的处理逻辑:

其内部定义了三个重要的静态内部类,Sync,NonFairSync,FairSync。Sync做为ReentrantLock中公用的同步组件,继承了AQS(要利用AQS复杂的顶层逻辑嘛,线程排队,阻塞,唤醒等等);NonFairSync和FairSync则都继承Sync,调用Sync的公用逻辑,而后再在各自内部完成本身特定的逻辑(公平或非公平)。

接着说下这二者的lock()方法实现原理:

NonFairSync(非公平可重入锁)

1.先获取state值,若为0,意味着此时没有线程获取到资源,CAS将其设置为1,设置成功则表明获取到排他锁了;

2.若state大于0,确定有线程已经抢占到资源了,此时再去判断是否就是本身抢占的,是的话,state累加,返回true,重入成功,state的值便是线程重入的次数;

3.其余状况,则获取锁失败。

FairSync(公平可重入锁)

能够看到,公平锁的大体逻辑与非公平锁是一致的,不一样的地方在于有了!hasQueuedPredecessors()这个判断逻辑,即使state为0,也不能贸然直接去获取,要先去看有没有还在排队的线程,若没有,才能尝试去获取,作后面的处理。反之,返回false,获取失败。

最后,说下ReentrantLock的tryRelease()方法实现原理:

若state值为0,表示当前线程已彻底释放干净,返回true,上层的AQS会意识到资源已空出。若不为0,则表示线程还占有资源,只不过将这次重入的资源的释放了而已,返回false。

ReentrantLock是一种可重入的,可实现公平性的互斥锁,它的设计基于AQS框架,可重入和公平性的实现逻辑都不难理解,每重入一次,state就加1,固然在释放的时候,也得一层一层释放。至于公平性,在尝试获取锁的时候多了一个判断:是否有比本身申请早的线程在同步队列中等待,如有,去等待;若没有,才容许去抢占。  

九、ReentrantLock 、synchronized 和 volatile 比较?

synchronized是互斥同步的一种实现。

synchronized:当某个线程访问被synchronized标记的方法或代码块时,这个线程便得到了该对象的锁,其余线暂时没法访问这个方法,只有等待这个方法执行完毕或代码块执行完毕,这个线程才会释放该对象的锁,其余线程才能执行这个方法代码块。

前面咱们已经说了volatile关键字,这里咱们举个例子来综合分析volatile与synchronized关键字的使用。

举个例子:

public class Singleton {

    // volatile保证了:1 instance在多线程并发的可见性 2 禁止instance在操做是的指令重排序
    private volatile static Singleton instance;

    private Singleton(){}

    public static Singleton getInstance() {
        // 第一次判空,保证没必要要的同步
        if (instance == null) {
            // synchronized对Singleton加全局锁,保证每次只要一个线程建立实例
            synchronized (Singleton.class) {
                // 第二次判空时为了在null的状况下建立实例
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
复制代码

这是一个经典的DCL单例。

它的字节码以下:

image

 

 

能够看到被synchronized同步的代码块,会在先后分别加上monitorenter和monitorexit,这两个字节码都须要指定加锁和解锁的对象。

关于加锁和解锁的对象:

synchronized代码块 :同步代码块,做用范围是整个代码块,做用对象是调用这个代码块的对象。

synchronized方法 :同步方法,做用范围是整个方法,做用对象是调用这个方法的对象。

synchronized静态方法 :同步静态方法,做用范围是整个静态方法,做用对象是调用这个类的全部对象。

synchronized(this):做用范围是该对象中全部被synchronized标记的变量、方法或代码块,做用对象是对象自己。

synchronized(ClassName.class) :做用范围是静态的方法或者静态变量,做用对象是Class对象。

synchronized(this)添加的是对象锁,synchronized(ClassName.class)添加的是类锁,它们的区别以下:

  • 对象锁:Java的全部对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,固然若是已经有线程获取了这个对象的锁那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的好处,方法抛异常的时候,锁仍然能够由JVM来自动释放。

  • 类锁:对象锁是用来控制实例方法之间的同步,类锁是来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并非真实存在的,它只用来帮助咱们理解锁定实例方法和静态方法的区别的。咱们都知道,java类可能会有不少个对象,可是只有1个Class对象,也就说类的不一样实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。因为每一个java对象都有个互斥锁,而类的静态方法是须要Class对象。因此所谓类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简单的就是MyClass.class的方式。类锁和对象锁不是同一个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:一个线程访问静态sychronized的时候,容许另外一个线程访问对象的实例synchronized方法。反过来也是成立的,为他们须要的锁是不一样的。

3、其它 (⭐⭐⭐)

一、多线程的使用场景?

使用多线程就必定效率高吗?有时候使用多线程并非为了提升效率,而是使得CPU能同时处理多个事件。

  • 为了避免阻塞主线程,启动其余线程来作事情,好比APP中的耗时操做都不在UI线程中作。

  • 实现更快的应用程序,即主线程专门监听用户请求,子线程用来处理用户请求,以得到大的吞吐量.感受这种状况,多线程的效率未必高。这种状况下的多线程是为了避免必等待,能够并行处理多条数据。好比JavaWeb的就是主线程专门监听用户的HTTP请求,然启动子线程去处理用户的HTTP请求。

  • 某种虽然优先级很低的服务,可是却要不定时去作。好比Jvm的垃圾回收。

  • 某种任务,虽然耗时,可是不消耗CPU的操做时间,开启个线程,效率会有显著提升。好比读取文件,而后处理。磁盘IO是个很耗费时间,可是不耗CPU计算的工做。因此能够一个线程读取数据,一个线程处理数据。确定比一个线程读取数据,而后处理效率高。由于两个线程的时候充分利用了CPU等待磁盘IO的空闲时间。

二、CopyOnWriteArrayList的了解。

Copy-On-Write 是什么?

在计算机中就是当你想要对一块内存进行修改时,咱们不在原有内存块中进行写操做,而是将内存拷贝一份,在新的内存中进行写操做,写完以后呢,就将指向原来内存指针指向新的内存,原来的内存就能够被回收掉。

原理:

CopyOnWriteArrayList这是一个ArrayList的线程安全的变体,CopyOnWriteArrayList 底层实现添加的原理是先copy出一个容器(能够简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了以前那个旧的的容器地址,可是在添加这个数据的期间,其余线程若是要去读取数据,仍然是读取到旧的容器里的数据。

优势和缺点:

优势:

1.据一致性完整,为何?由于加锁了,并发数据不会乱。

2.解决了像ArrayList、Vector这种集合多线程遍历迭代问题,记住,Vector虽然线程安全,只不过是加了synchronized关键字,迭代问题彻底没有解决!

缺点:

1.内存占有问题:很明显,两个数组同时驻扎在内存中,若是实际应用中,数据比较多,并且比较大的状况下,占用内存会比较大,针对这个其实能够用ConcurrentHashMap来代替。

2.数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。因此若是你但愿写入的的数据,立刻能读到,请不要使用CopyOnWrite容器。

使用场景:

一、读多写少(白名单,黑名单,商品类目的访问和更新场景),为何?由于写的时候会复制新集合。

二、集合不大,为何?由于写的时候会复制新集合。

三、实时性要求不高,为何,由于有可能会读取到旧的集合数据。

三、ConcurrentHashMap加锁机制是什么,详细说一下?

Java7 ConcurrentHashMap

ConcurrentHashMap做为一种线程安全且高效的哈希表的解决方案,尤为其中的"分段锁"的方案,相比HashTable的表锁在性能上的提高很是之大。HashTable容器在竞争激烈的并发环境下表现出效率低下的缘由,是由于全部访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不一样数据段的数据时,线程间就不会存在锁竞争,从而能够有效的提升并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分红一段一段的存储,而后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其余段的数据也能被其余线程访问。

ConcurrentHashMap 是一个 Segment 数组,Segment 经过继承 ReentrantLock 来进行加锁,因此每次须要加锁的操做锁住的是一个 segment,这样只要保证每一个 Segment 是线程安全的,也就实现了全局的线程安全。

concurrencyLevel:并行级别、并发数、Segment 数。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,因此理论上,这个时候,最多能够同时支持 16 个线程并发写,只要它们的操做分别分布在不一样的 Segment 上。这个值能够在初始化的时候设置为其余值,可是一旦初始化之后,它是不能够扩容的。其中的每一个 Segment 很像 HashMap,不过它要保证线程安全,因此处理起来要麻烦些。

初始化槽: ensureSegment

ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0],对于其余槽来讲,在插入第一个值的时候进行初始化。对于并发操做使用 CAS 进行控制。

Java8 ConcurrentHashMap

抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。结构上和 Java8 的 HashMap(数组+链表+红黑树) 基本上同样,不过它要保证线程安全性,因此在源码上确实要复杂一些。1.8 在 1.7 的数据结构上作了大的改动,采用红黑树以后能够保证查询效率(O(logn)),甚至取消了 ReentrantLock 改成了 synchronized,这样能够看出在新版的 JDK 中对 synchronized 优化是很到位的。

四、线程死锁的4个条件?

死锁是如何发生的,如何避免死锁?

当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的状况下,就会发生AB两个线程因为互相持有对方须要的锁,而发生的阻塞现象,咱们称为死锁。

public class DeadLockDemo {

    public static void main(String[] args) {
        // 线程a
        Thread td1 = new Thread(new Runnable() {
            public void run() {
                DeadLockDemo.method1();
            }
        });
        // 线程b
        Thread td2 = new Thread(new Runnable() {
            public void run() {
                DeadLockDemo.method2();
            }
        });

        td1.start();
        td2.start();
    }

    public static void method1() {
        synchronized (String.class) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程a尝试获取integer.class");
            synchronized (Integer.class) {

            }
        }
    }

    public static void method2() {
        synchronized (Integer.class) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程b尝试获取String.class");
            synchronized (String.class) {

            }
        }
    }
}

形成死锁的四个条件:

  • 互斥条件:一个资源每次只能被一个线程使用。
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已得到的资源保持不放。
  • 不剥夺条件:线程已得到的资源,在未使用完以前,不能强行剥夺。
  • 循环等待条件:若干线程之间造成一种头尾相接的循环等待资源关系。

在并发程序中,避免了逻辑中出现数个线程互相持有对方线程所须要的独占锁的的状况,就能够避免死锁,以下所示:

public class BreakDeadLockDemo {

    public static void main(String[] args) {
        // 线程a
        Thread td1 = new Thread(new Runnable() {
            public void run() {
                DeadLockDemo2.method1();
            }
        });
        // 线程b
        Thread td2 = new Thread(new Runnable() {
            public void run() {
                DeadLockDemo2.method2();
            }
        });

        td1.start();
        td2.start();
    }

    public static void method1() {
        synchronized (String.class) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程a尝试获取integer.class");
            synchronized (Integer.class) {
                System.out.println("线程a获取到integer.class");
            }

        }
    }

    public static void method2() {
        // 再也不获取线程a须要的Integer.class锁。
        synchronized (String.class) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程b尝试获取Integer.class");
            synchronized (Integer.class) {
                System.out.println("线程b获取到Integer.class");
            }
        }
    }
}
复制代码

五、CAS介绍?

Unsafe

Unsafe是CAS的核心类。由于Java没法直接访问底层操做系统,而是经过本地(native)方法来访问。不过尽管如此,JVM仍是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操做。

CAS

CAS,Compare and Swap即比较并交换,设计并发算法时经常使用到的一种技术,java.util.concurrent包全完创建在CAS之上,没有CAS也就没有此包,可见CAS的重要性。当前的处理器基本都支持CAS,只不过不一样的厂家的实现不同罢了。而且CAS也是经过Unsafe实现的,因为CAS都是硬件级别的操做,所以效率会比普通加锁高一些。

CAS的缺点

CAS看起来很美,但这种操做显然没法涵盖并发下的全部场景,而且CAS从语义上来讲也不是完美的,存在这样一个逻辑漏洞:若是一个变量V初次读取的时候是A值,而且在准备赋值的时候检查到它仍然是A值,那咱们就能说明它的值没有被其余线程修改过了吗?若是在这段期间它的值曾经被改为了B,而后又改回A,那CAS操做就会误认为它历来没有被修改过。这个漏洞称为CAS操做的"ABA"问题。java.util.concurrent包为了解决这个问题,提供了一个带有标记的原子引用类"AtomicStampedReference",它能够经过控制变量值的版原本保证CAS的正确性。不过目前来讲这个类比较"鸡肋",大部分状况下ABA问题并不会影响程序并发的正确性,若是须要解决ABA问题,使用传统的互斥同步可能回避原子类更加高效。

六、进程和线程的区别?

简而言之,一个程序至少有一个进程,一个进程至少有一个线程。

  • 一、线程的划分尺度小于进程,使得多线程程序的并发性高。

  • 二、进程在执行过程当中拥有独立的内存单元,而多个线程共享内存,从而极大地提升了程序的运行效率。

  • 三、线程在执行过程当中与进程仍是有区别的。每一个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。可是线程不可以独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

  • 四、从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分能够同时执行。但操做系统并无将多个线程看作多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

  • 五、进程是具备必定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程本身基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),可是它可与同属一个进程的其余的线程共享进程所拥有的所有资源。

  • 六、一个线程能够建立和撤销另外一个线程;同一个进程中的多个线程之间能够并发执行。

  • 七、进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不一样执行路径。线程有本身的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,因此多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。

七、什么致使线程阻塞?

线程的阻塞

为了解决对共享存储区的访问冲突,Java 引入了同步机制,如今让咱们来考察多个线程对共享资源的访问,显然同步机制已经不够了,由于在任意时刻所要求的资源不必定已经准备好了被访问,反过来,同一时刻准备好了的资源也可能不止一个。为了解决这种状况下的访问控制问题,Java 引入了对阻塞机制的支持.

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操做系统的同窗对它必定已经很熟悉了。Java 提供了大量方法来支持阻塞,下面让咱们逐一分析。

sleep() 方法:sleep() 容许 指定以毫秒为单位的一段时间做为参数,它使得线程在指定的时间内进入阻塞状态,不能获得CPU 时间,指定的时间一过,线程从新进入可执行状态。 典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不知足后,让线程阻塞一段时间后从新测试,直到条件知足为止。

suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,而且不会自动恢复,必须其对应的resume() 被调用,才能使得线程从新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另外一个线程产生的结果的情形:测试发现结果尚未产生后,让线程阻塞,另外一个线程产生告终果后,调用 resume() 使其恢复。

yield() 方法:yield() 使得线程放弃当前分得的 CPU 时间,可是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另外一个线程。

wait() 和 notify() 方法:两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种容许指定以毫秒为单位的一段时间做为参数,另外一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程从新进入可执行状态,后者则必须对应的 notify() 被调用。初看起来它们与 suspend() 和 resume() 方法对没有什么分别,可是事实上它们是大相径庭的。区别的核心在于,前面叙述的全部方法,阻塞时都不会释放占用的锁(若是占用了的话),而这一对方法则相反。

上述的核心区别致使了一系列的细节上的区别。

首先,前面叙述的全部方法都隶属于 Thread 类,可是这一对却直接隶属于 Object 类,也就是说,全部对象都拥有这一对方法。初看起来这十分难以想象,可是实际上倒是很天然的,由于这一对方法阻塞时要释放占用的锁,而锁是任何对象都具备的,调用任意对象的 wait() 方法致使线程阻塞,而且该对象上的锁被释放。而调用 任意对象的notify()方法则致使因调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到得到锁后才真正可执行)。

其次,前面叙述的全部方法均可在任何位置调用,可是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁能够释放。一样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁能够释放。所以,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不知足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。

wait() 和 notify() 方法的上述特性决定了它们常常和synchronized 方法或块一块儿使用,将它们和操做系统的进程间通讯机制做一个比较就会发现它们的类似性:synchronized方法或块提供了相似于操做系统原语的功能,它们的执行不会受到多线程机制的干扰,而这一对方法则至关于 block 和wakeup 原语(这一对方法均声明为 synchronized)。它们的结合使得咱们能够实现操做系统上一系列精妙的进程间通讯的算法(如信号量算法),并用于解决各类复杂的线程间通讯问题。(此外,线程间通讯的方式还有多个线程经过synchronized关键字这种方式来实现线程间的通讯、while轮询、使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通讯的管道通讯)。

关于 wait() 和 notify() 方法最后再说明两点:

第一:调用 notify() 方法致使解除阻塞的线程是从调用该对象的 wait() 方法而阻塞的线程中随机选取的,咱们没法预料哪个线程将会被选择,因此编程时要特别当心,避免因这种不肯定性而产生问题。

第二:除了 notify(),还有一个方法 notifyAll() 也可起到相似做用,惟一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的全部线程一次性所有解除阻塞。固然,只有得到锁的那一个线程才能进入可执行状态。

谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定超时期限的 wait() 方法的调用均可能产生死锁。遗憾的是,Java 并不在语言级别上支持死锁的避免,咱们在编程中必须当心地避免死锁。

以上咱们对 Java 中实现线程阻塞的各类方法做了一番分析,咱们重点分析了 wait() 和 notify() 方法,由于它们的功能最强大,使用也最灵活,可是这也致使了它们的效率较低,较容易出错。实际使用中咱们应该灵活使用各类方法,以便更好地达到咱们的目的。

八、线程的生命周期

线程状态流程图

 

image

 

 

  • NEW:建立状态,线程建立以后,可是还未启动。
  • RUNNABLE:运行状态,处于运行状态的线程,但有可能处于等待状态,例如等待CPU、IO等。
  • WAITING:等待状态,通常是调用了wait()、join()、LockSupport.spark()等方法。
  • TIMED_WAITING:超时等待状态,也就是带时间的等待状态。通常是调用了wait(time)、join(time)、LockSupport.sparkNanos()、LockSupport.sparkUnit()等方法。
  • BLOCKED:阻塞状态,等待锁的释放,例如调用了synchronized增长了锁。
  • TERMINATED:终止状态,通常是线程完成任务后退出或者异常终止。

NEW、WAITING、TIMED_WAITING都比较好理解,咱们重点说一说RUNNABLE运行态和BLOCKED阻塞态。

线程进入RUNNABLE运行态通常分为五种状况:

  • 线程调用sleep(time)后结束了休眠时间
  • 线程调用的阻塞IO已经返回,阻塞方法执行完毕
  • 线程成功的获取了资源锁
  • 线程正在等待某个通知,成功的得到了其余线程发出的通知
  • 线程处于挂起状态,而后调用了resume()恢复方法,解除了挂起。

线程进入BLOCKED阻塞态通常也分为五种状况:

  • 线程调用sleep()方法主动放弃占有的资源
  • 线程调用了阻塞式IO的方法,在该方法返回前,该线程被阻塞。
  • 线程视图得到一个资源锁,可是该资源锁正被其余线程锁持有。
  • 线程正在等待某个通知
  • 线程调度器调用suspend()方法将该线程挂起

咱们再来看看和线程状态相关的一些方法。

  • sleep()方法让当前正在执行的线程在指定时间内暂停执行,正在执行的线程能够经过Thread.currentThread()方法获取。

  • yield()方法放弃线程持有的CPU资源,将其让给其余任务去占用CPU执行时间。但放弃的时间不肯定,有可能刚刚放弃,立刻又得到CPU时间片。

  • wait()方法是当前执行代码的线程进行等待,将当前线程放入预执行队列,并在wait()所在的代码处中止执行,直到接到通知或者被中断为止。该方法能够使得调用该方法的线程释放共享资源的锁, 而后从运行状态退出,进入等待队列,直到再次被唤醒。该方法只能在同步代码块里调用,不然会抛出IllegalMonitorStateException异常。wait(long millis)方法等待某一段时间内是否有线程对锁进行唤醒,若是超过了这个时间则自动唤醒。

  • notify()方法用来通知那些可能等待该对象的对象锁的其余线程,该方法能够随机唤醒等待队列中等同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态。

  • notifyAll()方法能够使全部正在等待队列中等待同一共享资源的所有线程从等待状态退出,进入可运行状态,通常会是优先级高的线程先执行,可是根据虚拟机的实现不一样,也有多是随机执行。

  • join()方法可让调用它的线程正常执行完成后,再去执行该线程后面的代码,它具备让线程排队的做用。

九、乐观锁与悲观锁

悲观锁

老是假设最坏的状况,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

老是假设最好的状况,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,能够使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样能够提升吞吐量。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

使用场景

乐观锁适用于写比较少的状况下(多读场景),而通常多写的场景下用悲观锁就比较合适。

乐观锁常见的两种实现方式

一、版本号机制

通常是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加1。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,不然重试更新操做,直到更新成功。

二、CAS算法

即compare and swap(比较与交换),是一种有名的无锁算法。CAS有3个操做数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然什么都不作。 通常状况下是一个自旋操做,即不断的重试。

乐观锁的缺点

一、ABA 问题

若是一个变量V初次读取的时候是A值,而且在准备赋值的时候检查到它仍然是A值,那咱们就能说明它的值没有被其余线程修改过了吗?很明显是不能的,由于在这段时间它的值可能被改成其余值,而后又改回A,那CAS操做就会误认为它历来没有被修改过。这个问题被称为CAS操做的 "ABA"问题。

JDK 1.5 之后的 AtomicStampedReference 类必定程度上解决了这个问题,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,而且当前标志是否等于预期标志,若是所有相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

二、自旋CAS(也就是不成功就一直循环执行直到成功)若是长时间不成功,会给CPU带来很是大的执行开销。

三、CAS 只对单个共享变量有效,当操做涉及跨多个共享变量时 CAS 无效。可是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你能够把多个变量放在一个对象里来进行 CAS 操做.因此咱们能够使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操做。

十、run()和start()方法区别?

1.start()方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码:

经过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并无运行。 而后经过此Thread类调用方法run()来完成其运行操做的, 这里方法run()称为线程体, 它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止, 而CPU再运行其它线程,在Android中通常是主线程。

2.run()方法看成普通方法的方式调用,程序仍是要顺序执行,仍是要等待run方法体执行完毕后才可继续执行下面的代码:

而若是直接用Run方法, 这只是调用一个方法而已, 程序中依然只有主线程--这一个线程, 其程序执行路径仍是只有一条, 这样就没有达到写线程的目的。

十一、多线程断点续传原理。

在本地下载过程当中要使用数据库实时存储到底存储到文件的哪一个位置了,这样点击开始继续传递时,才能经过HTTP的GET请求中的setRequestProperty("Range","bytes=startIndex-endIndex");方法能够告诉服务器,数据从哪里开始,到哪里结束。同时在本地的文件写入时,RandomAccessFile的seek()方法也支持在文件中的任意位置进行写入操做。同时经过广播或事件总线机制将子线程的进度告诉Activity的进度条。关于断线续传的HTTP状态码是206,即HttpStatus.SC_PARTIAL_CONTENT。

十二、怎么安全中止一个线程任务?原理是什么?线程池里有相似机制吗?

终止线程

一、使用violate boolean变量退出标志,使线程正常退出,也就是当run方法完成后线程终止。(推荐)

二、使用interrupt()方法中断线程,可是线程不必定会终止。

三、使用stop方法强行终止线程。不安全主要是:thread.stop()调用以后,建立子线程的线程就会抛出ThreadDeatherror的错误,而且会释放子线程所持有的全部锁。

终止线程池

ExecutorService线程池就提供了shutdown和shutdownNow这样的生命周期方法来关闭线程池自身以及它拥有的全部线程。

一、shutdown关闭线程池

线程池不会马上退出,直到添加到线程池中的任务都已经处理完成,才会退出。

二、shutdownNow关闭线程池并中断任务

终止等待执行的线程,并返回它们的列表。试图中止全部正在执行的线程,试图终止的方法是调用Thread.interrupt(),可是你们知道,若是线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是没法中断当前的线程的。因此,ShutdownNow()并不表明线程池就必定当即就能退出,它可能必需要等待全部正在执行的任务都执行完成了才能退出。

1三、堆内存,栈内存理解,栈如何转换成堆?

  • 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。
  • 堆内存用于存放由new建立的对象和数组。JVM里的“堆”(heap)特指用于存放Java对象的内存区域。因此根据这个定义,Java对象所有都在堆上。JVM的堆被同一个JVM实例中的全部Java线程共享。它一般由某种自动内存管理机制所管理,这种机制一般叫作“垃圾回收”(garbage collection,GC)。
  • 堆主要用来存放对象的,栈主要是用来执行程序的。
  • 实际上,栈中的变量指向堆内存中的变量,这就是 Java 中的指针!

1四、如何控制某个方法容许并发访问线程的个数;

1五、多进程开发以及多进程应用场景;

1六、Java的线程模型;

1七、死锁的概念,怎么避免死锁?

1八、如何保证多线程读写文件的安全?

1九、线程如何关闭,以及如何防止线程的内存泄漏?

20、为何要有线程,而不是仅仅用进程?

2一、多个线程如何同时请求,返回的结果如何等待全部线程数据完成后合成一个数据?

2二、线程如何关闭?

2三、数据一致性如何保证?

2四、两个进程同时要求写或者读,能不能实现?如何防止进程的同步?

2五、谈谈对多线程的理解并举例说明

2六、线程的状态和优先级。

2七、ThreadLocal的使用

2八、Java中的并发工具(CountDownLatch,CyclicBarrier等)

2九、进程线程在操做系统中的实现

30、双线程经过线程同步的方式打印12121212.......

3一、java线程,场景实现,多个线程如何同时请求,返回的结果如何等待全部线程数据完成后合成一个数据

3二、服务器只提供数据接收接口,在多线程或多进程条件下,如何保证数据的有序到达?

3三、单机上一个线程池正在处理服务,若是突然断电了怎么办(正在处理和阻塞队列里的请求怎么处理)?

Java虚拟机面试题 (⭐⭐⭐)

一、JVM内存区域。

JVM基本构成

image

 

 

从上图可知,JVM主要包括四个部分:

1.类加载器(ClassLoader):在JVM启动时或者在类运行将须要的class加载到JVM中。(下图表示了从java源文件到JVM的整个过程,可配合理解。

 

image

 

 

2.执行引擎:负责执行class文件中包含的字节码指令;

3.内存区(也叫运行时数据区):是在JVM运行的时候操做所分配的内存区。运行时内存区主要能够划分为5个区域,如图:

 

image

 

 

方法区(MethodArea):用于存储类结构信息的地方,包括常量池、静态常量、构造函数等。虽然JVM规范把方法区描述为堆的一个辑部分, 但它却有个别名non-heap(非堆),因此你们不要搞混淆了。方法区还包含一个运行时常量池。

java堆(Heap):存储java实例或者对象的地方。这块是GC的主要区域。从存储的内容咱们能够很容易知道,方法和堆是被全部java线程共享的。

java栈(Stack):java栈老是和线程关联在一块儿,每当创一个线程时,JVM就会为这个线程建立一个对应的java栈在这个java栈中,其中又会包含多个栈帧,每运行一个方法就建一个栈帧,用于存储局部变量表、操做栈、方法返回等。每个方法从调用直至执行完成的过程,就对应一栈帧在java栈中入栈到出栈的过程。因此java栈是现成有的。

程序计数器(PCRegister):用于保存当前线程执行的内存地址。因为JVM程序是多线程执行的(线程轮流切换),因此为了保证程切换回来后,还能恢复到原先状态,就须要一个独立计数器,记录以前中断的地方,可见程序计数器也是线程私有的。

本地方法栈(Native MethodStack):和java栈的做用差很少,只不过是为JVM使用到native方法服务的。

4.本地方法接口:主要是调用C或C++实现的本地方法及回调结果。

开线程影响哪块内存?

每当有线程被建立的时候,JVM就须要为其在内存中分配虚拟机栈和本地方法栈来记录调用方法的内容,分配程序计数器记录指令执行的位置,这样的内存消耗就是建立线程的内存代价。

二、JVM的内存模型的理解?

Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工做方式。JVM是整个计算机虚拟模型,因此JMM是隶属于JVM的。

Java线程之间的通讯老是隐式进行,而且采用的是共享内存模型。这里提到的共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入什么时候对另外一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每一个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其余的硬件和编译器优化。

总之,JMM就是一组规则,这组规则意在解决在并发编程可能出现的线程安全问题,并提供了内置解决方案(happen-before原则)及其外部可以使用的同步手段(synchronized/volatile等),确保了程序执行在多线程环境中的应有的原子性,可视性及其有序性。

须要更全面理解建议阅读如下文章:

全面理解Java内存模型(JMM)及volatile关键字

全面理解Java内存模型

三、描述一下GC的原理和回收策略?

提到垃圾回收,咱们能够先思考一下,若是咱们去作垃圾回收须要解决哪些问题?

通常说来,咱们要解决三个问题:

一、回收哪些内存?

二、何时回收?

三、如何回收?

这些问题分别对应着引用管理和回收策略等方案。

提到引用,咱们都知道Java中有四种引用类型:

  • 强引用:代码中广泛存在的,只要强引用还存在,垃圾收集器就不会回收掉被引用的对象。
  • 软引用:SoftReference,用来描述还有用可是非必须的对象,当内存不足的时候会回收这类对象。
  • 弱引用:WeakReference,用来描述非必须对象,弱引用的对象只能生存到下一次GC发生时,当GC发生时,不管内存是否足够,都会回收该对象。
  • 虚引用:PhantomReference,一个对象是否有虚引用的存在,彻底不会对其生存时间产生影响,也没法经过虚引用取得一个对象的引用,它存在的惟一目的是在这个对象被回收时能够收到一个系统通知。

不一样的引用类型,在作GC时会区别对待,咱们平时生成的Java对象,默认都是强引用,也就是说只要强引用还在,GC就不会回收,那么如何判断强引用是否存在呢?

一个简单的思路就是:引用计数法,有对这个对象的引用就+1,再也不引用就-1,可是这种方式看起来简单美好,但它却不能解决循环引用计数的问题。

所以可达性分析算法登上历史舞台,用它来判断对象的引用是否存在。

可达性分析算法经过一系列称为GCRoots的对象做为起始点,从这些节点从上向下搜索,所走过的路径称为引用链,当一个对象没有任何引用链与GCRoots链接时就说明此对象不可用,也就是对象不可达。

GC Roots对象一般包括:

  • 虚拟机栈中引用的对象(栈帧中的本地变量表)
  • 方法中类的静态属性引用的对象
  • 方法区中常量引用的对象
  • Native方法引用的对象

可达性分析算法整个流程以下所示:

第一次标记:对象在通过可达性分析后发现没有与GC Roots有引用链,则进行第一次标记并进行一次筛选,筛选条件是:该对象是否有必要执行finalize()方法。没有覆盖finalize()方法或者finalize()方法已经被执行过都会被认为没有必要执行。 若是有必要执行:则该对象会被放在一个F-Queue队列,并稍后在由虚拟机创建的低优先级Finalizer线程中触发该对象的finalize()方法,但不保证必定等待它执行结束,由于若是这个对象的finalize()方法发生了死循环或者执行时间较长的状况,会阻塞F-Queue队列里的其余对象,影响GC。

第二次标记:GC对F-Queue队列里的对象进行第二次标记,若是在第二次标记时该对象又成功被引用,则会被移除即将回收的集合,不然会被回收。

总之,JVM在作垃圾回收的时候,会检查堆中的全部对象否会被这些根集对象引用,不可以被引用的对象就会被圾收集器回收。通常回收算法也有以下几种:

1).标记-清除(Mark-sweep)

标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不须要进行对象的移动,而且仅对不存活的对象进行处理,在存活对象比较多的状况下极为高效,但因为标记-清除算法直接回收不存活的对象,所以会形成内存碎片。

2).标记-整理(Mark-Compact)

标记-整理算法采用标记-清除算法同样的方式进行对象的标记,但在清除时不一样,在回收不存活的对象占用的空间后,会将全部的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,所以成本更高,可是却解决了内存碎片的问题。该垃圾回收算法适用于对象存活率高的场景(老年代)。

3).复制(Copying)

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,好比新生代。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂状况。

4).分代收集算法

不一样的对象的生命周期(存活状况)是不同的,而不一样生命周期的对象位于堆中不一样的区域,所以对堆内存不一样区域采用不一样的策略进行回收能够提升 JVM 的执行效率。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法。Java堆内存通常能够分为新生代、老年代和永久代三个模块:

新生代:

1.全部新生成的对象首先都是放在新生代的。新生代的目标就是尽量快速的收集掉那些生命周期短的对象。

2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,而后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另外一个survivor1区,而后清空eden和这个survivor0区,此时survivor0区是空的,而后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。

4.新生代发生的GC也叫作Minor GC,MinorGC发生频率比较高(不必定等Eden区满了才触发)。

老年代:

1.在老年代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。所以,能够认为老年代中存放的都是一些生命周期较长的对象。

2.内存比新生代也大不少(大概比例是1:2),当老年代内存满时触发Major GC,即Full GC。Full GC发生频率比较低,老年代对象存活时间比较长。

永久代:

永久代主要存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,可是有些应用可能动态生成或者调用一些class,例如使用反射、动态代理、CGLib等bytecode框架时,在这种时候须要设置一个比较大的永久代空间来存放这些运行过程当中新增的类。

垃圾收集器

垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现:

  • Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优势是简单高效;

  • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;

  • ParNew收集器 (复制算法): 新生代收并行集器,其实是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;

  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具备高并发、低停顿的特色,追求最短GC回收停顿时间。

  • Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;

  • Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量能够高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;

  • G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不一样于以前的收集器的一个重要特色是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

内存分配和回收策略

JAVA自动内存管理:给对象分配内存 以及 回收分配给对象的内存。

一、对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。

二、大对象直接进入老年代。如很长的字符串以及数组。很长的字符串以及数组。

三、长期存活的对象将进入老年代。当对象在新生代中经历过必定次数(默认为15)的Minor GC后,就会被晋升到老年代中。

四、动态对象年龄断定。为了更好地适应不一样程序的内存情况,虚拟机并非永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,若是在Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就能够直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

须要更全面的理解请点击这里

四、类的加载器,双亲机制,Android的类加载器。

类的加载器

你们都知道,一个Java程序都是由若干个.class文件组织而成的一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不一样的class文件当中,因此常常要从这个class文件中要调用另一个class文件中的方法,若是另一个文件不存在的话,则会引起系统异常。

而程序在启动的时候,并不会一次性加载程序所要用到的class文件,而是根据程序的须要,经过Java的类加载制(ClassLoader)来动态加载某个class文件到内存当的,从而只有class文件被载入到了内存以后,才能被其它class文件所引用。因此ClassLoader就是用来动态加载class件到内存当中用的。

双亲机制

类的加载就是虚拟机经过一个类的全限定名来获取描述此类的二进制字节流,而完成这个加载动做的就是类加载器。

类和类加载器息息相关,断定两个类是否相等,只有在这两个类被同一个类加载器加载的状况下才有意义,不然即使是两个类来自同一个Class文件,被不一样类加载器加载,它们也是不相等的。

注:这里的相等性保函Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果以及Instance关键字对对象所属关系的断定结果等。

类加载器能够分为三类:

  • 启动类加载器(Bootstrap ClassLoader):负责加载<JAVA_HOME>\lib目录下或者被-Xbootclasspath参数所指定的路径的,而且是被虚拟机所识别的库到内存中。

  • 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录下或者被java.ext.dirs系统变量所指定的路径的全部类库到内存中。

  • 应用类加载器(Application ClassLoader):负责加载用户类路径上的指定类库,若是应用程序中没有实现本身的类加载器,通常就是这个类加载器去加载应用程序中的类库。

一、原理介绍

ClassLoader使用的是双亲委托模型来搜索类的,每一个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)自己没有父类加载器,但能够用做其它lassLoader实例的的父类加载器。

当一个ClassLoader实例须要加载某个类时,它会在试图搜索某个类以前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,若是没加载到,则把任务转交给Extension ClassLoader试图加载,若是也没加载到,则转交给App ClassLoader 进行加载,若是它也没有加载获得的话,则返回给委托的发起者,由它到指定的文件系统或网络等待URL中加载该类。

若是它们都没有加载到这个类时,则抛出ClassNotFoundException异常。不然将这个找到的类生成一个类的定义,将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

类加载机制:

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法去内,而后在堆区建立一个java.lang.Class对象,用来封装在方法区内的数据结构。类的加载最终是在堆区内的Class对象,Class对象封装了类在方法区内的数据结构,而且向Java程序员提供了访问方法区内的数据结构的接口。

类加载有三种方式:

1)命令行启动应用时候由JVM初始化加载

2)经过Class.forName()方法动态加载

3)经过ClassLoader.loadClass()方法动态加载

这么多类加载器,那么当类在加载的时候会使用哪一个加载器呢?

这个时候就要提到类加载器的双亲委派模型,流程图以下所示:

 

image

 

 

双亲委派模型的整个工做流程很是的简单,以下所示:

若是一个类加载器收到了加载类的请求,它不会本身立去加载类,它会先去请求父类加载器,每一个层次的类加器都是如此。层层传递,直到传递到最高层的类加载器只有当 父类加载器反馈本身没法加载这个类,才会有当子类加载器去加载该类。

二、为何要使用双亲委托这种模型呢?

由于这样能够避免重复加载,当父亲已经加载了该类的时候,就没有必要让子ClassLoader再加载一次。

考虑到安全因素,咱们试想一下,若是不使用这种委托模式,那咱们就能够随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在很是大的安全隐患,而双亲委托的方式,就能够避免这种状况,由于String已经在启动时就被引导类加载器(BootstrcpClassLoader)加载,因此用户自定义的ClassLoader永远也没法加载一个本身写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

三、可是JVM在搜索类的时候,又是如何断定两个class是相同的呢?

JVM在断定两个class是否相同时,不只要判断两个类名否相同,并且要判断是否由同一个类加载器实例加载的。

只有二者同时知足的状况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,若是被两个不一样的ClassLoader实例所加载,JVM也会认为它们是两个不一样class。

好比网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译以后生成字节码文件NetClasLoaderSimple.class,ClassLoaderA和ClassLoaderB这个类加载器并读取了NetClassLoaderSimple.class文件并分别定义出了java.lang.Class实例来表示这个类,对JVM来讲,它们是两个不一样的实例对象,但它们确实是一份字节码文件,若是试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCastException,提示这是两个不一样的类型。

Android类加载器

对于Android而言,最终的apk文件包含的是dex类型的文件,dex文件是将class文件从新打包,打包的规则又不是简单地压缩,而是彻底对class文件内部的各类函数表进行优化,产生一个新的文件,即dex文件。所以加载某种特殊的Class文件就须要特殊的类加载器DexClassLoader。

能够动态加载Jar经过URLClassLoader

1.ClassLoader 隔离问题:JVM识别一个类是由 ClassLoaderid + PackageName + ClassName。

2.加载不一样Jar包中的公共类:

  • 让父ClassLoader加载公共的Jar,子ClassLoade加载包含公共Jar的Jar,此时子ClassLoader在加载Jar的时候会先去父ClassLoader中找。(只适用Java)
  • 重写加载包含公共Jar的Jar的ClassLoader,在loClass中找到已经加载过公共Jar的ClassLoader,是把父ClassLoader替换掉。(只适用Java)
  • 在生成包含公共Jar的Jar时候把公共Jar去掉。

五、JVM跟Art、Dalvik对比

  

六、GC收集器简介?以及它的内存划分怎么样的?

(1)简介:

Garbage-First(G1,垃圾优先)收集器是服务类型的收集器,目标是多处理器机器、大内存机器。它高度符合垃圾收集暂停时间的目标,同时实现高吞吐量。Oracle JDK 7 update 4 以及更新发布版彻底支持G1垃圾收集器

(2)G1的内存划分方式:

它是将堆内存被划分为多个大小相等的 heap 区,每一个heap区都是逻辑上连续的一段内存(virtual memory). 其中一部分区域被当成老一代收集器相同的角色(eden, survivor, old), 但每一个角色的区域个数都不是固定的。这在内存使用上提供了更多的灵活性

七、Java的虚拟机JVM的两个内存:栈内存和堆内存的区别是什么?

Java把内存划分红两种:一种是栈内存,一种是堆内存。二者的区别是:

1)栈内存:在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的做用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间能够当即被另做他用。

2)堆内存:堆内存用来存放由new建立的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

八、JVM调优的常见命令行工具备哪些?JVM常见的调优参数有哪些?

(1)JVM调优的常见命令工具包括:

1)jps命令用于查询正在运行的JVM进程,

2)jstat能够实时显示本地或远程JVM进程中类装载、内存、垃圾收集、JIT编译等数据

3)jinfo用于查询当前运行这的JVM属性和参数的值。

4)jmap用于显示当前Java堆和永久代的详细信息

5)jhat用于分析使用jmap生成的dump文件,是JDK自带的工具

6)jstack用于生成当前JVM的全部线程快照,线程快照是虚拟机每一条线程正在执行的方法,目的是定位线程出现长时间停顿的缘由。

(2)JVM常见的调优参数包括:

-Xmx

  指定java程序的最大堆内存, 使用java -Xmx5000M -version判断当前系统能分配的最大堆内存

-Xms

  指定最小堆内存, 一般设置成跟最大堆内存同样,减小GC

-Xmn

  设置年轻代大小。整个堆大小=年轻代大小 + 年老代大小。因此增大年轻代后,将会减少年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

-Xss

  指定线程的最大栈空间, 此参数决定了java函数调用的深度, 值越大调用深度越深, 若值过小则容易出栈溢出错误(StackOverflowError)

-XX:PermSize

  指定方法区(永久区)的初始值,默认是物理内存的1/64, 在Java8永久区移除, 代之的是元数据区, 由-XX:MetaspaceSize指定

-XX:MaxPermSize

  指定方法区的最大值, 默认是物理内存的1/4, 在java8中由-XX:MaxMetaspaceSize指定元数据区的大小

-XX:NewRatio=n

  年老代与年轻代的比值,-XX:NewRatio=2, 表示年老代与年轻代的比值为2:1

-XX:SurvivorRatio=n

  Eden区与Survivor区的大小比值,-XX:SurvivorRatio=8表示Eden区与Survivor区的大小比值是8:1:1,由于Survivor区有两个(from, to)

九、jstack,jmap,jutil分别的意义?如何线上排查JVM的相关问题?

十、JVM方法区存储内容 是否会动态扩展 是否会出现内存溢出 出现的缘由有哪些。

十一、如何解决同时存在的对象建立和对象回收问题?

十二、JVM中最大堆大小有没有限制?

1三、JVM方法区存储内容 是否会动态扩展 是否会出现内存溢出 出现的缘由有哪些。

1四、如何理解Java的虚函数表?

1五、Java运行时数据区域,致使内存溢出的缘由。

1六、对象建立、内存布局,访问定位等。

转载:https://juejin.im/post/5e5c5c52f265da575f4e7558