Java常用基础知识(一)

目标:阿里巴巴、京东、百度、腾讯、美团、去哪儿等互联网公司!


1、一道类加载的问题,一个类,在类加载之后,如果对类的内容进行修改,如何在不重新启动虚拟机的情况下,加载已经变化过的类

你可以创建个新的class loader,然后用loadClass加载,再newInstance;原来加载的classloader是不能重新加载的;这算是一个典型的容器思路

《深入理解java虚拟机》


2、对java“书写一次,到处运行”(Write once, run anywhere)的理解?

一次编译、到处运行”说的是Java语言跨平台的特性,Java的跨平台特性与Java虚拟机的存在密不可分,可在不同的环境中运行。比如说Windows平台和Linux平台都有相应的JDK,安装好JDK后也就有了Java语言的运行环境。其实Java语言本身与其他的编程语言没有特别大的差异,并不是说Java语言可以跨平台,而是在不同的平台都有可以让Java语言运行的环境而已,所以才有了Java一次编译,到处运行这样的效果。
严格的讲,跨平台的语言不止Java一种,但Java是较为成熟的一种。“一次编译,到处运行”这种效果跟编译器有关。编程语言的处理需要编译器和解释器。Java虚拟机和DOS类似,相当于一个供程序运行的平台。
程序从源代码到运行的三个阶段:编码——编译——运行——调试。Java在编译阶段则体现了跨平台的特点。编译过程大概是这样的:首先是将Java源代码转化成.CLASS文件字节码,这是第一次编译。.class文件就是可以到处运行的文件。然后Java字节码会被转化为目标机器代码,这是是由JVM来执行的,即Java的第二次编译。“到处运行”的关键和前提就是JVM。

Java代码的整个生命周期如图:
这里写图片描述


3、谈谈你对 Java 平台的理解?“Java 是解释执行”,这句话正确吗?

Java 本身是一种面向对象的语言,最显著的特性有两个方面,一是所谓的“书写一次,到处运行”(Write once, run anywhere),能够非常容易地获得跨平台能力;另外就是垃圾收集(GC, Garbage Collection),Java 通过垃圾收集器(Garbage Collector)回收分配内存,大部分情况下,程序员不需要自己操心内存的分配和回收。
对于“Java 是解释执行”这句话,这个说法不太准确。我们开发的 Java 的源代码,首先通过 Javac 编译成为字节码(bytecode),然后,在运行时,通过 Java 虚拟机(JVM)内嵌的解释器将字节码转换成为最终的机器码。但是常见的 JVM,比如我们大多数情况使用的 Oracle JDK 提供的 Hotspot JVM,都提供了 JIT(Just-In-Time)编译器,也就是通常所说的动态编译器,JIT 能够在运行时将热点代码编译成机器码,这种情况下部分热点代码就属于编译执行,而不是解释执行了。


4、请对比 Exception 和 Error,另外,运行时异常与一般异常有什么区别?
Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。

Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类。Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。

Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。

Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。

这里写图片描述


5、下面的代码反映了异常处理中哪些不当之处?
try {
// 业务代码
// …
Thread.sleep(1000L);
} catch (Exception e) {
// Ignore it
}

第一,尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常,在这里是 Thread.sleep() 抛出的 InterruptedException。
第二,不要生吞(swallow)异常。


6、从性能角度来审视一下 Java 的异常处理机制,这里有两个可能会相对昂贵的地方:

①、try-catch 代码段会产生额外的性能开销,或者换个角度说,它往往会影响 JVM 对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码;
②、Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了。


7、ClassNotFoundException的产生原因?
Java支持使用Class.forName方法来动态地加载类,任意一个类的类名如果被作为参数传递给这个方法都将导致该类被加载到JVM内存中,如果这个类在类路径中没有被找到,那么此时就会在运行时抛出ClassNotFoundException异常。


8、谈谈 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。


9、下面代码会输出什么?
try {
// do something
System.exit(1);
} finally{
System.out.println(“Print from finally”);
}
上面 finally 里面的代码可不会被执行的哦,这是一个特例。
1,不要在 finally 中使用 return 语句。
2,finally 总是执行,除非程序或者线程被中断。


10、强引用、软引用、弱引用、幻象引用有什么区别?具体使用场景是什么?
所谓强引用(”Strong” Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。
软引用(SoftReference),通过SoftReference类实现。是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
弱引用(WeakReference)通过WeakReference类实现。。并不能使对象豁免垃圾收集,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。
虚引用也叫幻象引用,通过PhantomReference类来实现。无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列 (ReferenceQueue)联合使用。


11、理解 Java 的字符串,String、StringBuffer、StringBuilder 有什么区别?
String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。
StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它的线程安全是通过把各种修改数据的方法都加上 synchronized 关键字实现的,非常直白。它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。
StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。


12、【String】/【StringBuffer】/【StringBuilder】理解心得:

1 String

(1) String的创建机理
由于String在Java世界中使用过于频繁,Java为了避免在一个系统中产生大量的String对象,引入了字符串常量池。其运行机制是:创建一个字符串时,首先检查池中是否有值相同的字符串对象,如果有则不需要创建直接从池中刚查找到的对象引用;如果没有则新建字符串对象,返回对象引用,并且将新创建的对象放入池中。但是,通过new方法创建的String对象是不检查字符串池的,而是直接在堆区或栈区创建一个新的对象,也不会把对象放入池中。上述原则只适用于通过直接量给String对象引用赋值的情况。

举例:String str1 = “123”; //通过直接量赋值方式,放入字符串常量池
String str2 = new String(“123”);//通过new方式赋值方式,不放入字符串常量池

注意:String提供了inter()方法。调用该方法时,如果常量池中包括了一个等于此String对象的字符串(由equals方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并且返回此池中对象的引用。

(2) String的特性
[A] 不可变。是指String对象一旦生成,则不能再对它进行改变。不可变的主要作用在于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅度提高系统性能。不可变模式是一个可以提高多线程程序的性能,降低多线程程序复杂度的设计模式。

[B] 针对常量池的优化。当2个String对象拥有相同的值时,他们只引用常量池中的同一个拷贝。当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。

2 StringBuffer/StringBuilder

StringBuffer和StringBuilder都实现了AbstractStringBuilder抽象类,拥有几乎一致对外提供的调用接口;其底层在内存中的存储方式与String相同,都是以一个有序的字符序列(char类型的数组)进行存储,不同点是StringBuffer/StringBuilder对象的值是可以改变的,并且值改变以后,对象引用不会发生改变;两者对象在构造过程中,首先按照默认大小申请一个字符数组,由于会不断加入新数据,当超过默认大小后,会创建一个更大的数组,并将原先的数组内容复制过来,再丢弃旧的数组。因此,对于较大对象的扩容会涉及大量的内存复制操作,如果能够预先评估大小,可提升性能。

唯一需要注意的是:StringBuffer是线程安全的,但是StringBuilder是线程不安全的。可参看Java标准类库的源代码,StringBuffer类中方法定义前面都会有synchronize关键字。为此,StringBuffer的性能要远低于StringBuilder。


13、谈谈 Java 反射机制,动态代理是基于什么原理?
反射机制是 Java 语言提供的一种基础功能,赋予程序在运行时自省(introspect,官方用语)的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。
动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装 RPC 调用、面向切面的编程(AOP)。
实现动态代理的方式很多,比如 JDK 自身提供的动态代理,就是主要利用了上面提到的反射机制。


14、一个简单JDK Proxy动态代理Demo

public interface Hello {

    String sayhello();
}

public class HelloImpl implements  Hello {
    @Override
    public String sayhello() {
        return "Hello World !";
    }
}
public class MyInvocationHandle implements InvocationHandler {

    private Object target;

    public MyInvocationHandle(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("prepare invoke!");
        Object result = method.invoke(target, args);
        return result;
    }
}

最后是动态代理的调用测试:

public class ProxyDemo {

    public static void main(String[] args) {
        Hello hello = new HelloImpl();
        MyInvocationHandle myInvocationHandle = new MyInvocationHandle(hello);
        Hello proxyHello = (Hello) Proxy.newProxyInstance(HelloImpl.class.getClassLoader(), hello.getClass().getInterfaces(), myInvocationHandle);
        System.out.println(proxyHello.sayhello());
    }

}

15、【JDK Proxy 】和【 cglib 】两种动态代理的实现方式,我们在开发中怎样选择呢?简单对比下两种方式各自优势。
JDK Proxy 的优势:
最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。
平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。
代码实现简单。

基于类似 cglib 框架的优势:
有的时候调用目标可能不便实现额外接口,从某种角度看,限定调用者实现接口是有些侵入性的实践,类似 cglib 动态代理就没有这种限制。
只操作我们关心的类,而不必为其他相关类增加工作量。
高性能。


16、动态代理使用场景:

这里写图片描述


17、 关于反射:
反射最大的作用之一就在于我们可以不在编译时知道某个对象的类型,而在运行时通过提供完整的”包名+类名.class”得到。注意:不是在编译时,而是在运行时。

功能:
•在运行时能判断任意一个对象所属的类。
•在运行时能构造任意一个类的对象。
•在运行时判断任意一个类所具有的成员变量和方法。
•在运行时调用任意一个对象的方法。
说大白话就是,利用Java反射机制我们可以加载一个运行时才得知名称的class,获悉其构造方法,并生成其对象实体,能对其fields设值并唤起其methods。

应用场景:
反射技术常用在各类通用框架开发中。因为为了保证框架的通用性,需要根据配置文件加载不同的对象或类,并调用不同的方法,这个时候就会用到反射——运行时动态加载需要加载的对象。

特点:
由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。


18、动态代理:
为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在两者之间起到中介的作用(可类比房屋中介,房东委托中介销售房屋、签订合同等)。
所谓动态代理,就是实现阶段不用关心代理谁,而是在运行阶段才指定代理哪个一个对象(不确定性)。如果是自己写代理类的方式就是静态代理(确定性)。

组成要素:
(动态)代理模式主要涉及三个要素:
其一:抽象类接口
其二:被代理类(具体实现抽象接口的类)
其三:动态代理类:实际调用被代理类的方法和属性的类

实现方式:
实现动态代理的方式很多,比如 JDK 自身提供的动态代理,就是主要利用了反射机制。还有其他的实现方式,比如利用字节码操作机制,类似 ASM、CGLIB(基于 ASM)、Javassist 等。
举例,常可采用的JDK提供的动态代理接口InvocationHandler来实现动态代理类。其中invoke方法是该接口定义必须实现的,它完成对真实方法的调用。通过InvocationHandler接口,所有方法都由该Handler来进行处理,即所有被代理的方法都由InvocationHandler接管实际的处理任务。此外,我们常可以在invoke方法实现中增加自定义的逻辑实现,实现对被代理类的业务逻辑无侵入。


19、int 和 Integer 有什么区别?谈谈 Integer 的值缓存范围?

int 是我们常说的整形数字,是 Java 的 8 个原始数据类型(Primitive Types,boolean、byte 、short、char、int、float、double、long)之一。Java 语言虽然号称一切都是对象,但原始数据类型是例外,其并不是对象(对象都是引用类型)。
Integer 是 int 对应的包装类,它有一个 int 类型的字段存储数据,并且提供了基本操作,比如数学运算、int 和字符串之间转换等。在 Java 5 中,引入了自动装箱和自动拆箱功能(boxing/unboxing),Java 可以根据上下文,自动进行转换,极大地简化了相关编程。
关于 Integer 的值缓存,这涉及 Java 5 中另一个改进。构建 Integer 对象的传统方式是直接调用构造器,直接 new 一个对象。但是根据实践,我们发现大部分数据操作都是集中在有限的、较小的数值范围,因而,在 Java 5 中新增了静态工厂方法 valueOf,在调用它的时候会利用一个缓存机制,带来了明显的性能改进。按照 Javadoc,这个值默认缓存是 -128 到 127 之间。


20、理解自动装箱、拆箱
自动装箱实际上算是一种语法糖。什么是语法糖?可以简单理解为 Java 平台为我们自动进行了一些转换,保证不同的写法在运行时等价,它们发生在编译阶段,也就是生成的字节码是一致的。
原则上,建议避免无意中的装箱、拆箱行为,尤其是在性能敏感的场合,创建 10 万个 Java 对象要比 10 万个原始数据类型的开销大很多,不管是内存使用还是处理速度,光是对象头的空间占用就已经是数量级的差距了。


21、使用原始数据类型的优缺点:
优点:1.比创建对象的性能高的多;
2.因为原始数据类型是不存在差异的,不管是 32 位还是 64 位环境,开发者无需担心数据的位数差异;
3. 原始类型线程安全。
缺点:
1.原始数据类型和 Java 泛型并不能配合使用;
2.无法高效地表达数据,也不便于表达复杂的数据结构,比如 vector 和 tuple


22、对比 Vector、ArrayList、LinkedList 有何区别?

常规回答:
Vector 是 Java 早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。
ArrayList 是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。与 Vector 近似,ArrayList 也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector 在扩容时会提高 1 倍,而 ArrayList 则是增加 50%。
LinkedList 顾名思义是 Java 提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的。

精辟回答:
Vector、ArrayList、LinkedList均为线型的数据结构,但是从实现方式与应用场景中又存在差别。
1 底层实现方式:
ArrayList内部用数组来实现;LinkedList内部采用双向链表实现;Vector内部用数组实现。
2 读写机制:
ArrayList在执行插入元素是超过当前数组预定义的最大值时,数组需要扩容,扩容过程需要调用底层System.arraycopy()方法进行大量的数组复制操作;在删除元素时并不会减少数组的容量(如果需要缩小数组容量,可以调用trimToSize()方法);在查找元素时要遍历数组,对于非null的元素采取equals的方式寻找。
LinkedList在插入元素时,须创建一个新的Entry对象,并更新相应元素的前后元素的引用;在查找元素时,需遍历链表;在删除元素时,要遍历链表,找到要删除的元素,然后从链表上将此元素删除即可。
Vector与ArrayList仅在插入元素时容量扩充机制不一致。对于Vector,默认创建一个大小为10的Object数组,并将capacityIncrement设置为0;当插入元素数组大小不够时,如果capacityIncrement大于0,则将Object数组的大小扩大为现有size+capacityIncrement;如果capacityIncrement<=0,则将Object数组的大小扩大为现有大小的2倍。
3 读写效率:
ArrayList对元素的增加和删除都会引起数组的内存分配空间动态发生变化。因此,对其进行插入和删除速度较慢,但检索速度很快。
LinkedList由于基于链表方式存放数据,增加和删除元素的速度较快,但是检索速度较慢。
4 线程安全性:
ArrayList、LinkedList为非线程安全;Vector是基于synchronized实现的线程安全的ArrayList。

5.注意要点:单线程应尽量使用ArrayList,Vector因为同步会有性能损耗;即使在多线程环境下,我们可以利用Collections这个类中为我们提供的synchronizedList(List list)方法返回一个线程安全的同步列表对象。


23、对比 Vector、ArrayList、LinkedList适用场景。

Vector 和 ArrayList 作为动态数组,其内部元素以数组形式顺序存储的,所以非常适合随机访问的场合。除了尾部插入和删除元素,往往性能会相对较差,比如我们在中间位置插入一个元素,需要移动后续所有元素。
而 LinkedList 进行节点插入、删除却要高效得多,但是随机访问性能则要比动态数组慢。


24、集合(Collection)框架的整体设计。

这里写图片描述

我们可以看到 Java 的集合框架,Collection 接口是所有集合的根,然后扩展开提供了三大类集合,分别是:
List:也就是我们前面介绍最多的有序集合,它提供了方便的访问、插入、删除等操作。
Set:Set 是不允许重复元素的,这是和 List 最明显的区别,也就是不存在两个对象 equals 返回 true。我们在日常开发中有很多需要保证元素唯一性的场合。
Queue/Deque:则是 Java 提供的标准队列结构的实现,除了集合的基本功能,它还支持类似先入先出(FIFO, First-in-First-Out)或者后入先出(LIFO,Last-In-First-Out)等特定行为。这里不包括 BlockingQueue,因为通常是并发编程场合,所以被放置在并发包里。


25、对比 Hashtable、HashMap、TreeMap 、LinkedHashMap有什么不同?

Hashtable、HashMap、TreeMap、LinkedHashMap 都是最常见的一些 Map 实现,是以键值对的形式存储和操作数据的容器类型。
Hashtable 是早期 Java 类库提供的一个哈希表实现,本身是同步的,不支持 null 键和值,由于同步导致的性能开销,所以已经很少被推荐使用。
HashMap 是应用更加广泛的哈希表实现,行为上大致上与 HashTable 一致,主要区别在于 HashMap 不是同步的,支持 null 键和值等。通常情况下,HashMap 进行 put 或者 get 操作,可以达到常数时间的性能,所以它是绝大部分利用键值对存取场景的首选,比如,实现一个用户 ID 和用户信息对应的运行时存储结构。
TreeMap 则是基于红黑树的一种提供顺序访问的 Map,和 HashMap 不同,它的 get、put、remove 之类操作都是 O(log(n))的时间复杂度,具体顺序可以由指定的 Comparator 来决定,或者根据键的自然顺序来判断。


26、HashMap 的实例有两个参数影响其性能:
初始容量 和加载因子。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。
加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。
当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。


27、LinkedHashMap的理解。
其为HashMap的子类。
LinkedHashMap 通常提供的是遍历顺序符合插入顺序,它的实现是通过为条目(键值对)维护一个双向链表。
注意,通过特定构造函数,我们可以创建反映访问顺序的实例,所谓的 put、get、compute 等,都算作“访问”。

使用: LinkedHashMap在初始化对象时,需要重写removeEldestEntry方法 , 实现自定义删除策略,否则行为就和普遍 Map 没有区别。

 LinkedHashMap<String, String> accessOrderedMap = new LinkedHashMap<>(16, 0.75F, true){
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, String> eldest) { 
                return size() > 3;
            }
        };

28、Hashtable、HashMap、TreeMap心得:

三者均实现了Map接口,存储的内容是基于key-value的键值对映射,一个映射不能有重复的键,一个键最多只能映射一个值。

(1) 元素特性
HashTable中的key、value都不能为null;HashMap中的key、value可以为null,很显然只能有一个key为null的键值对,但是允许有多个值为null的键值对;TreeMap中当未实现 Comparator 接口时,key 不可以为null;当实现 Comparator 接口时,若未对null情况进行判断,则key不可以为null,反之亦然。

(2)顺序特性
HashTable、HashMap具有无序特性。TreeMap是利用红黑树来实现的(树中的每个节点的值,都会大于或等于它的左子树种的所有节点的值,并且小于或等于它的右子树中的所有节点的值),实现了SortMap接口,能够对保存的记录根据键进行排序。所以一般需要排序的情况下是选择TreeMap来进行,默认为升序排序方式(深度优先搜索),可自定义实现Comparator接口实现排序方式。

(3)初始化与增长方式
初始化时:HashTable在不指定容量的情况下的默认容量为11,且不要求底层数组的容量一定要为2的整数次幂;HashMap默认容量为16,且要求容量一定为2的整数次幂。
扩容时:Hashtable将容量变为原来的2倍加1;HashMap扩容将容量变为原来的2倍。

(4)线程安全性
HashTable其方法函数都是同步的(采用synchronized修饰),不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性。也正因为如此,在多线程运行环境下效率表现非常低下。因为当一个线程访问HashTable的同步方法时,其他线程也访问同步方法就会进入阻塞状态。比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被阻塞,大大降低了程序的运行效率,在新版本中已被废弃,不推荐使用。
HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步(1)可以用 Collections的synchronizedMap方法;(2)使用ConcurrentHashMap类,相较于HashTable锁住的是对象整体, ConcurrentHashMap基于lock实现锁分段技术。首先将Map存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。ConcurrentHashMap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升。

(5)一段话HashMap
HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法用来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD, 8),链表就会被改造为树形结构。
在并发修改期间,相应 Segment 会被锁定。


28、如何保证容器是线程安全的?ConcurrentHashMap 如何实现高效地线程安全?
(1)、手动在容器操作上加上同步锁(synchronized)或者使用HashtableVector等同步容器。但是这种容器在并发操作的性能消耗太昂贵。
(2)、调用 Collections 的工具类同步包装器(Synchronized Wrapper)来获取一个同步的包装容器(如 Collections.synchronizedMap),但是它们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较低下。
(3)、更加推荐的选择是使用并发包提供的线程安全容器类(如:ConcurrentHashMapCopyOnWriteArrayList等),远优于早期的简单同步实现。

ConcurrentHashMap是基于分离锁实现的,将内部进行分段(Segment),里面则是 HashEntry 的数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放。
其内部结构的示意图(如图所示—JDK7)。

这里写图片描述

ConcurrentHashMap进行扩容时,不是对整体,而是单独对 Segment 进行扩容;
另外一个 Map 的 size 方法同样需要关注,它的实现涉及分离锁的一个副作用。因为如果不进行同步,简单的计算所有 Segment 的总值,可能会因为并发 put,导致结果不准确,但是直接锁定所有 Segment 进行计算,就会变得非常耗能。
所以ConcurrentHashMap 的实现是通过重试机制(RETRIES_BEFORE_LOCK,指定重试次数 2),来试图获得可靠值。

JDK8大的时候ConcurrentHashMap又被做了非常大的改动,其锁的颗粒度,是加在链表头上的,这个是个思路上的突破。


29、Java 提供了哪些 IO 方式? NIO 如何实现多路复用?
IO(同步阻塞的):它是基于流模型实现的。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。(代码简单直观,但是效率和扩展性存在局限)
IO中日常开发使用的较多的类结构图如下:

这里写图片描述
NIO(同步非阻塞):提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。
NIO2(异步非阻塞):也称AIO,是Java7时对NIO的改进。引入了异步非阻塞 IO 方式,异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。

NIO 的主要组成部分有:
Buffer,高效的数据容器,除了布尔类型,所有原始数据类型都有相应的 Buffer 实现。
Channel,类似在 Linux 之类操作系统上看到的文件描述符,是 NIO 中被用来支持批量式 IO 操作的一种抽象。File 或者 Socket,通常被认为是比较高层次的抽象,而 Channel 则是更加操作系统底层的一种抽象,这也使得 NIO 得以充分利用现代操作系统底层机制,获得特定场景的性能优化。
Selector,是 NIO 实现多路复用的基础,它提供了一种高效的机制,可以检测到注册在 Selector 上的多个 Channel 中,是否有 Channel 处于就绪状态,进而实现了单线程对多 Channel 的高效管理。
Chartset,提供 Unicode 字符串定义,NIO 也提供了相应的编解码器等,例如,进行字符串到 ByteBuffer 的转换(Charset.defaultCharset().encode(“Hello world!”));)
其多路复用原理可以参照下图:

这里写图片描述


30、区分同步和异步(synchronous/asynchronous):
同步和异步关注的是消息通信机制
同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用结果返回,才会进行下一步;
而异步则相反,其他任务不需要等待当前调用返回,继续进行其他操作,被调用者通常依靠事件、回调处理此次的调用(既可以认为有操作结果后会回调我们)。

这里写图片描述


31、区分阻塞与非阻塞(blocking/non-blocking):
阻塞和非阻塞一般是针对当前线程(单线程是阻塞还是继续进行)
在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件返回结果才能继续。
而非阻塞则是不管 IO 操作是否结束,直接返回,继续当前线程的其他操作,相应操作在后台继续处理。
这里写图片描述


32、输入流、输出流(InputStream/OutputStream)和 Reader、Writer的区别:
输入流、输出流(InputStream/OutputStream)是用于读取或写入字节的,例如操作图片文件。
Reader、Writer 则是用于操作字符,增加了字符编解码等功能,适用于类似从文件中读取或者写入文本信息。
本质上计算机操作的都是字节,不管是网络通信还是文件读取,Reader、Writer 相当于构建了应用逻辑和原始数据之间的桥梁。


33、分别使用IO和NIO实现一个可以让多个客户端同时连接使用的服务端?
IO因为是同步阻塞的,所以为了让多客户端操作只能依赖多个线程来实现,因为其启动或者销毁一个线程是有明显开销的,每个线程都有单独的线程栈等结构,所以需要占用非常明显的内存。

这里写图片描述
NIO利用单线程轮询事件机制实现,应用的性能和扩展能力有了非常大的提高。
这里写图片描述


34、Java 有几种文件拷贝方式?哪一种最高效?
Java 有多种比较典型的文件拷贝实现方式,比如:
(1)、 利用 java.io 类库,直接为源文件构建一个 FileInputStream 读取,然后再为目标文件构建一个 FileOutputStream,完成写入工作。

 public static void copyFileByStream(File source, File dest) throws IOException {
        InputStream is = null;
        OutputStream os = null;
        try {
            is = new FileInputStream(source);
            os = new FileOutputStream(dest);
            byte[] buffer = new byte[1024];
            int length ;
            while ((length = is.read(buffer)) > 0){
                os.write(buffer, 0, length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            is.close();
            os.close();
        }
    }
(2)、利用 java.nio 类库提供的 transferTo 或 transferFrom 方法实现。
public class NIOFileCopy {

    public static void copyFile(File source, File dest) throws Exception {
        FileChannel sourceChannel = new FileInputStream(source).getChannel();
        FileChannel destChannel = new FileOutputStream(dest).getChannel();
        for (long count = sourceChannel.size() ;count>0 ;) {
            /* 返回已传输的字节数 */
            long transferred = sourceChannel.transferTo(sourceChannel.position(), count, destChannel);
            sourceChannel.position(sourceChannel.position() + transferred);
            count-=transferred;
        }
    }

}

(3)还有就是Java 标准类库本身已经提供了几种 Files.copy 的实现。

总体上来说,NIO 中transferTo/From 的方式可能更快,因为它更能利用现代操作系统底层机制,避免不必要拷贝和上下文切换。
当我们使用输入输出流进行读写时,实际上是进行了多次上下文切换,比如应用读取数据时,先在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存。这种方式会带来一定的额外开销,可能会降低 IO 效率。
如图:

这里写图片描述
而基于 NIO transferTo 的实现方式,在 Linux 和 Unix 上,则会使用到零拷贝技术,数据传输并不需要用户态参与,省去了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。(transferTo 不仅仅是可以用在文件拷贝中,与其类似的,例如读取磁盘文件,然后进行 Socket 发送也是相同高效)
如图:

这里写图片描述
至于第三种方式,即 Java 标准库提供的文件拷贝方法(java.nio.file.Files.copy),个人还没有去研究过其源码,还不能妄下定论。


35、理解内核态空间(Kernel Space)和用户态空间(User Space)??
内核态空间是操作系统层面的基本概念,操作系统内核、硬件驱动等运行在内核态空间,具有相对高的特权;而用户态空间,则是给普通应用和服务使用。


36、对Buffer 的理解:
Buffer 是 NIO 操作数据的基本工具,Java 为每种原始数据类型都提供了相应的 Buffer 实现(布尔除外)。
如图:

这里写图片描述
Buffer主要属性有:
capcity,它反映这个 Buffer 到底有多大,也就是数组的长度。
position,要操作的数据起始位置。
limit,相当于操作的限额。在读取或者写入时,limit 的意义很明显是不一样的。比如,读取操作时,很可能将 limit 设置到所容纳数据的上限;而在写入时,则会设置容量或容量以下的可写限度。
mark,记录上一次 postion 的位置,默认是 0,算是一个便利性的考虑,往往不是必须的。

梳理下 Buffer 的基本操作:
我们创建了一个 ByteBuffer,准备放入数据,capcity 当然就是缓冲区大小,而 position 就是 0,limit 默认就是 capcity 的大小。
当我们写入几个字节的数据时,position 就会跟着水涨船高,但是它不可能超过 limit 的大小。
如果我们想把前面写入的数据读出来,需要调用 flip 方法,将 position 设置为 0,limit 设置为以前的 position 那里。
如果还想从头再读一遍,可以调用 rewind,让 limit 不变,position 再次设置为 0。


37、谈谈接口和抽象类有什么区别?
接口是对行为的抽象,它是抽象方法的集合(既内部所有方法都是抽象方法),利用接口可以达到 API 定义实现分离的目的。接口,不能实例化;不能包含任何非常量成员,任何 field 都是隐含着 public static final 的意义;同时,没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法。Java 标准类库中,比如 java.util.List。
抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。除了不能实例化,形式上和一般的 Java 类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。Java 标准库中,例如 java.util.AbstractList。
Java 类实现 interface 使用 implements 关键词,继承 abstract class 则是使用 extends 关键词(只能单继承但是可以多实现)。


38、Interface附加说明:
接口的职责也不仅仅限于抽象方法的集合,其实有各种不同的实践。有一类没有任何方法的接口,通常叫作 Marker Interface,顾名思义,它的目的就是为了声明某些东西,比如我们熟知的 Cloneable、Serializable 等。似乎和 Annotation 异曲同工,它的好处就是简单直接。
Java 8 以后,接口也是可以有方法实现的!从 Java 8 开始,interface 增加了对 default method 的支持。Java 9 以后,甚至可以定义 private default method。可以参考下面代码片段:

public interface Collection extends Iterable {
/**
* Returns a sequential Stream with this collection as its source
**/
default Stream stream() {
return StreamSupport.stream(spliterator(), false);
}
}


39、对面向对象编程(OOP)的理解?
面向对象的基本要素:封装、继承、多态。
封装的目的是隐藏事务内部的实现细节,以便提高安全性和简化编程。封装提供了合理的边界,避免外部调用者接触到内部的细节。
继承是代码复用的基础机制。但要注意,继承可以看作是非常紧耦合的一种关系,父类代码修改,子类行为也会变动。在实践中,过度滥用继承,可能会起到反效果。
多态,主要涉及到重写(override)和重载(overload)、向上转型。重写是父子类中相同名字和参数的方法,不同的实现;重载则是相同名字的方法,但是不同的参数。(注意方法名和参数相同,但是返回值不同不属于重载,会编译错误)。

面向对象编程基本的设计原则:S.O.L.I.D 原则
(1)、单一职责(Single Responsibility),类或者对象最好是只有单一职责,在程序设计中如果发现某个类承担着多种义务,可以考虑进行拆分。
(2)、开关原则(Open-Close, Open for extension, close for modification),设计要对扩展开放,对修改关闭。(对于相同模块的不同处理逻辑,尽量不要用if-else来处理,应该抽一个公共接口,然后使用不同的实现。)
(3)、里氏替换(Liskov Substitution),这是面向对象的基本要素之一,进行继承关系抽象时,凡是可以用父类或者基类的地方,都可以用子类替换。
(4)、接口分离(Interface Segregation),我们在进行类和接口设计时,如果在一个接口里定义了太多方法,其子类很可能面临两难,就是只有部分方法对它是有意义的,这就破坏了程序的内聚性。对于这种情况,可以通过拆分成功能单一的多个接口,将行为进行解耦。
(5)、依赖反转(Dependency Inversion),实体应该依赖于抽象而不是实现。也就是说高层次模块,不应该依赖于低层次模块,而是应该基于抽象。实践这一原则是保证产品代码之间适当耦合度的法宝。


40、接口、抽象类和类的一些比较
(1)、 支持多重继承:接口支持;抽象类不支持;类不支持;
(2)、支持抽象函数:接口语义上支持;抽象类支持;类不支持;
(3)、允许函数实现:接口不允许;抽象类支持;类允许;
(4)、允许实例化:接口不允许;抽象类不允许;类允许;
(5)、允许部分函数实现:接口不允许;抽象类允许;类不允许。
(6)、定义的内容:接口中只能包括public函数以及public static final常量;抽象类与类均无任何限制。


41、谈谈你知道的设计模式?请手动实现单例模式,Spring 等框架中使用了哪些模式?
按照设计模式的实现目标,设计模式可以分为创建型模式、结构型模式和行为型模式。
创建型模式:是对对象创建过程的各种问题和解决方案的总结。包括工厂模式(Factory、Abstract Factory)、单例模式(Singleton)等。
结构型模式:是针对软件设计结构的总结。常见的结构型模式有装饰者模式(Decorator)、代理模式(Proxy)等。
行为型模式:是从类或对象之间交互、职责划分等角度总结的模式。比较常见的有策略模式(Strategy)、观察者模式(Observer)。

单例模式参考博客:【《Head First设计模式》之单例模式】

Spring 等框架中使用的设计模式举例:
①、BeanFactoryApplicationContext应用了工厂模式。
②、在 Bean 的创建中,Spring 也为不同 scope 定义的对象,使用了单例模式。
③、 AOP 领域则是使用了代理模式、装饰器模式等。
④、各种事件监听器,是观察者模式的典型应用。
⑤、类似 JdbcTemplate 等则是应用了模板模式。


42、java 的 io包中InputStream使用什么设计模式?
IO 框架中,我们知道 InputStream 是一个抽象类,标准类库中提供了 FileInputStream、ByteArrayInputStream 等各种不同的子类,分别从不同角度对 InputStream 进行了功能扩展,这是典型的装饰器模式应用案例。
下面的类图里,简单总结了 InputStream 的装饰模式实践:

这里写图片描述


43、在业务代码中,经常发现大量 XXXFacade等外观模式的使用,谈谈对外感模式的理解?
外观模式形象上来讲就是在原系统之前放置了一个新的代理对象,只能通过该对象才能使用该系统,不再允许其它方式访问该系统。该代理对象封装了访问原系统的所有规则和接口方法,提供的API接口较之使用原系统会更加的简单。
举例:JUnitCore是JUnit类的 Facade模式的实现类,外部使用该代理对象与JUnit进行统一交互,驱动执行测试代码。
使用场景:当我们希望封装或隐藏原系统。
缺点:违反了开闭原则。如有扩展,只能直接修改代理对象。


44、synchronized 和 ReentrantLock 有什么区别?有人说 synchronized 最慢,这话靠谱吗?
synchronized 是 Java 内建(其实现不在java标准库中,而是在JVM代码中【C++】)的同步机制,它提供了互斥的语义和可见性,即也称为互斥锁,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。synchronized 可以用来修饰方法,也可以使用在特定的代码块上。

synchronized (this) {
     do something
    // …
}

ReentrantLock ,通常翻译为再入锁,它的语义和 synchronized 基本相同。再入锁通过代码直接调用 lock() 方法获取锁,调用 unlock() 方法释放。代码书写也更加灵活。与此同时,ReentrantLock 提供了很多实用的方法,能够实现很多 synchronized 无法做到的细节控制,比如公平性、获取当前等待线程信息等。

class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() { 
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

synchronizedReentrantLock 的性能不能一概而论,早期版本 synchronized 在很多场景下性能确实较差,但在后续版本进行了较多改进。在低竞争场景中表现可能优于 ReentrantLock。


45、线程安全需要保证几个基本特性?
三个 :
原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。
有序性,是保证线程内串行语义,避免指令重排等。


46、使用ReentrantLock的注意事项?
在使用ReentrantLock类的时,一定要注意三点:
在finally中释放锁,目的是保证在获取锁之后,最终能够被释放
不要将获取锁的过程写在try块内,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁无故被释放。
ReentrantLock提供了一个newCondition的方法,以便用户在同一锁的情况下可以根据不同的情况执行等待或唤醒的动作。


47、synchronized 底层如何实现?什么是锁的升级、降级?
synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。)
JDK 中,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
当没有竞争出现时,默认会使用偏斜锁,使用偏斜锁可以降低无竞争开销。如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。如果锁定成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
当 JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,然后试图进行降级。


48、为什么我们需要读写锁(ReadWriteLock)等其他锁呢?
虽然 ReentrantLock 和 synchronized 简单实用,但是行为上有一定局限性,通俗点说就是“太霸道”,要么不占,要么独占。实际应用场景中,有的时候不需要大量竞争的写操作,而是以并发读取为主,这种场景下性能就非常不优。
而并发包提供的读写锁扩展了锁的能力,它所基于的原理是多个读操作是不需要互斥的,因为读操作并不会更改数据,所以不存在互相干扰。
下面是一个基于读写锁实现的数据结构,当数据量较大,并发读多、并发写少的时候,能够比纯同步版本凸显出更大的优势。

public class RWSample {
    private final Map<String, String> m = new TreeMap<>();
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();
    public String get(String key) {
        r.lock();
        System.out.println(" 读锁锁定!");
        try {
            return m.get(key);
        } finally {
            r.unlock();
        }
    }

    public String put(String key, String entry) {
        w.lock();
    System.out.println(" 写锁锁定!");
            try {
                return m.put(key, entry);
            } finally {
                w.unlock();
            }
        }
    // …
    }

但在实际应用中,ReadWriteLock表现也并不尽如人意,主要还是因为相对比较大的开销。所以更推荐使用StampedLock(JDK8)来操作读写锁。


49、对自旋锁的理解:
自旋锁:竞争锁的失败的线程,并不会真实的在操作系统层面挂起等待,而是JVM会让线程做几个空循环(基于预测在不久的将来就能获得),在经过若干次循环后,如果可以获得锁,那么进入临界区,如果还不能获得锁,才会真实的将线程在操作系统层面进行挂起。

适用场景:自旋锁可以减少线程的阻塞,这对于锁竞争不激烈,且占用锁时间非常短的代码块来说,有较大的性能提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗。


50、CAS
(Conmpare And Swap)


51、一个线程两次调用 start() 方法会出现什么情况?谈谈线程的生命周期和状态转换。
Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常。

线程的生命周期可以参考java.lang.Thread.State:
新建(NEW):至今尚未启动的线程处于这种状态。
就绪(RUNNABLE):表示该线程已经在 JVM 中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它 CPU 片段,在就绪队列里面排队。
阻塞(BLOCKED):无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
等待(WAITING):表示正在等待其他线程采取某些操作。一个常见的场景是类似生产者消费者模式,发现任务条件尚未满足,就让当前消费者线程等待(wait),另外的生产者线程去准备任务数据,然后通过类似 notify 等动作,通知消费线程可以继续工作了。Thread.join() 也会令线程进入等待状态。
计时等待(TIMED_WAIT):其进入条件等待状态,和等待状态类似。
终止(TERMINATED):不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡。

线程的状态转换图如下:

这里写图片描述


52、一个进程包含多个线程


53、守护进程的理解:
守护线程(Daemon Thread),有的时候应用中需要一个长期驻留的服务程序,但是不希望其影响应用退出,就可以将其设置为守护线程,如果 JVM 发现只有守护线程存在时,将结束进程。

Thread daemonThread = new Thread();
daemonThread.setDaemon(true);
daemonThread.start();

54、什么是 ThreadLocal
用于实现线程内部的数据共享叫线程共享(对于同一个线程内部数据一致),即相同的一段代码 多个线程来执行 ,每个线程使用的数据只与当前线程有关。
通常弱引用都会和引用队列配合清理机制使用,不会长期占用内存。但是 ThreadLocal 内的弱引用是个例外,所以通常都会建议,一定要自己负责 remove,并且不要和线程池配合。


55、什么情况下 Java 程序会产生死锁?如何定位、修复?
死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。死锁不仅仅是在线程之间会发生,存在资源独占的进程之间同样也可能出现死锁。通常来说,我们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,而永久处于阻塞的状态。

这里写图片描述
定位死锁最常见的方式就是利用 jstack 等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。【参考博客:https://blog.csdn.net/qq_33404395/article/details/81297884
如果程序运行时发生了死锁,绝大多数情况下都是无法在线解决的,只能重启、修正程序本身问题。


56、试着写一个可能死锁的程序?
public class DeadLockSample extends Thread {
private String first;
private String second;
public DeadLockSample(String name, String first, String second) {
super(name);
this.first = first;
this.second = second;
}

public  void run() {
    synchronized (first) {
        System.out.println(this.getName() + " obtained: " + first);
        try {
            Thread.sleep(1000L);
            synchronized (second) {
                System.out.println(this.getName() + " obtained: " + second);
            }
        } catch (InterruptedException e) {
            // Do nothing
        }
    }
}
public static void main(String[] args) throws InterruptedException {
    String lockA = "lockA";
    String lockB = "lockB";
    DeadLockSample t1 = new DeadLockSample("Thread1", lockA, lockB);
    DeadLockSample t2 = new DeadLockSample("Thread2", lockB, lockA);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
}

}


57、如何在编程中尽量预防死锁呢?
(1)、如果可能的话,尽量避免使用多个锁,并且只有需要时才持有锁。
(2)、如果必须使用多个锁,尽量设计好锁的获取顺序。
(3)、使用带超时的方法。指定超时时间,并为无法得到锁时准备退出逻辑。


58、Java并发包提供了哪些并发工具类?
我们通常所说的并发包也就是 java.util.concurrent 及其子包,内包含了:
(1)、提供了比 synchronized 更加高级的各种同步结构,比如利用 Semaphore 作为资源控制器,限制同时进行工作的线程数量。
(2)、各种线程安全的容器,比如最常见的 ConcurrentHashMap(无序)、 ConcunrrentSkipListMap(有序)、CopyOnWriteArrayList 等。
(3)、各种并发队列实现,如各种 BlockedQueue 实现。
(4)、 Executor 框架,可以创建各种不同类型的线程池。


59、对Semaphore的理解:
Java 提供了经典信号量(Semaphore))的实现,它通过控制一定数量的允许(permit)的方式,来达到限制通用资源访问的目的。你可以想象一下这个场景,在车站、机场等出租车时,当很多空出租车就位时,为防止过度拥挤,调度员指挥排队等待坐车的队伍一次进来 5 个人上车,等这 5 个人坐车出发,再放进去下一批,这和 Semaphore 的工作原理有些类似。
使用示例参考博客:【https://blog.csdn.net/qq_33404395/article/details/81304623


60、梳理下并发包中线程安全的容器:
如图:

这里写图片描述
如果我们应用侧重于Map的putget速度,不在乎顺序,推荐使用ConcurrentHashMap,反之则使用ConcunrrentSkipListMap。我们需要对大量数据进行频繁的修改,ConcunrrentSkipListMap也可能表现出优势。
在并发容器中并没有TreMap衍生的相应容器,主要是因为其基于复杂的红黑树,实现难度很大。
CopyOnWriteArrayListCopyOnWriteArraySet其原理是,在任何修改操作(add、set、remove)都会拷贝原数组,修改后进行替换。通过这种防御性的方式,实现线程安全的。所以这类容器较适合读多写少的操作,不然其开销还是非常明显的。


61、并发包中的ConcurrentLinkedQueueLinkedBlockingQueue有什么区别?
Java提供的线程安全的Queue可以分为阻塞队列非阻塞队列,其中阻塞队列的典型例子是LinkedBlockingQueue,非阻塞队列的典型例子是ConcurrentLinkedQueue
由于LinkedBlockingQueue实现是线程安全的(内部是基于锁),实现了先进先出等特性,是作为生产者消费者的首选,LinkedBlockingQueue可以指定容量,也可以不指定,不指定的话,默认最大是Integer.MAX_VALUE,其中主要用到put和take方法,put方法在队列满的时候会阻塞直到有队列成员被消费,take方法在队列空的时候会阻塞,直到有队列成员被放进来。
ConcurrentLinkedQueueQueue的一个线程安全实现(基于 lock-free),Queue中元素按FIFO原则进行排序.采用CAS操作,来保证元素的一致性,一般可以提供较高吞吐量。(通常Concurrent *** 往往为弱一致性,其体现有①、容器在遍历过程中发生了修改,则抛出 ConcurrentModificationException,不再继续遍历。②、size 等操作准确性是有限的,未必是 100% 准确。③、读取的性能具有一定的不确定性。)


62、Deque和Queue?
Deque双向队列,可以从头尾插入或者删除;
Queue单向队列,先进先出。


63、各种队列的边界分析:
ArrayBlockingQueue 是最典型的的有界队列,其内部以 final 的数组保存数据,数组的大小就决定了队列的边界,所以我们在创建 ArrayBlockingQueue 时,都要指定容量,如public ArrayBlockingQueue(int capacity, boolean fair)
LinkedBlockingQueue,容易被误解为无边界,但其实其行为和内部代码都是基于有界的逻辑实现的,只不过如果我们没有在创建队列时就指定容量,那么其容量限制就自动被设置为Integer.MAX_VALUE,成为了无界队列。
SynchronousQueue,这是一个非常奇葩的队列实现,每个删除操作都要等待插入操作,反之每个插入操作也都要等待删除动作。那么这个队列的容量是多少呢?是 1 吗?其实不是的,其内部容量是 0。不能缓存任何元素。
PriorityBlockingQueue 是无边界的优先队列。
DelayedQueueLinkedTransferQueue 同样是无边界的队列。
对于无边界的队列,有一个自然的结果,就是 put 操作永远也不会发生其他 BlockingQueue 的那种等待情况。


64、LinkedBlockingQueue 和 ArrayBlockingQueue对比?
ArrayBlockingQueue 是有明确的容量限制的,而 LinkedBlockingQueue 则取决于我们是否在创建时指定。
从空间利用角度,数组结构的 ArrayBlockingQueue 要比 LinkedBlockingQueue 紧凑,因为其不需要创建所谓节点,但是其初始分配阶段就需要一段连续的空间,所以初始内存需求更大。
通用场景中,LinkedBlockingQueue 的吞吐量一般优于ArrayBlockingQueue,因为它实现了更加细粒度的锁操作。但是ArrayBlockingQueue 实现比较简单,性能更好预测,属于表现稳定的“选手”。


65、线程安全的队列实现都有哪些?

这里写图片描述


66、Java 并发类库提供的线程池有哪几种? 分别有什么特点?
通常开发者都是利用 Executors 提供的通用线程池创建方法,去创建不同配置的线程池,主要区别在于不同的ExecutorService 类型或者不同的初始参数。

Executors 目前提供了 5 种不同的线程池创建配置:
(1)、newCachedThreadPool(),它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列。
(2)、newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads。
(3)、newSingleThreadExecutor(),它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。
(4)、newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize),创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
(5)、newWorkStealingPool(int parallelism),这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。


67、线程池的内部工作原理?

这里写图片描述
(1)、工作队列负责存储用户提交的各个任务,这个工作队列,可以是容量为 0 的 SynchronousQueue(如: newCachedThreadPool),也可以是 LinkedBlockingQueue(如newFixedThreadPool);

private final BlockingQueue<Runnable> workQueue;

(2)、内部的“线程池”,这是指保持工作线程的集合,线程池需要在运行过程中管理线程创建、销毁。例如,对于带缓存的线程池,当任务压力较大时,线程池会创建新的工作线程。当业务压力退去,线程池会在闲置一段时间后结束线程;

//线程池的工作线程被抽象为静态内部类 Worker,基于AQS实现。
private final HashSet<Worker> workers = new HashSet<>();

(3)、ThreadFactory提供上面所需要的创建线程逻辑;

(4)、如果任务提交时被拒绝,比如线程池已经处于 SHUTDOWN状态,需要为其提供处理逻辑,Java 标准库提供了类似ThreadPoolExecutor、AbortPolicy等默认实现,也可以按照实际需求自定义。

68、线程池的基本组成部分
从上面的分析,就可以看出线程池的几个基本组成部分,基本都体现在线程池的构造函数中。如:

public ThreadPoolExecutor(int corePoolSize,
                        int maximumPoolSize,
                        long keepAliveTime,
                        TimeUnit unit,
                        BlockingQueue<Runnable> workQueue,
                        ThreadFactory threadFactory,
                        RejectedExecutionHandler handler)

(1)、corePoolSize:所谓的核心线程数,可以大致理解为长期驻留的线程数目(除非设置了 allowCoreThreadTimeOut)。对于不同的线程池,这个值可能会有很大区别,比如 newFixedThreadPool 会将其设置为 nThreads,而对于 newCachedThreadPool 则是为 0。
(2)、maximumPoolSize,顾名思义,就是线程不够时能够创建的最大线程数。同样进行对比,对于 newFixedThreadPool,当然就是 nThreads,因为其要求是固定大小,而 newCachedThreadPool 则是 Integer.MAX_VALUE。
(3)、keepAliveTimeTimeUnit,这两个参数指定了额外的线程能够闲置多久,显然有些线程池不需要它。
(4)、workQueue,工作队列,必须是 BlockingQueue
通过配置不同的参数,我们就可以创建出行为大相径庭的线程池,这就是线程池高度灵活性的基础。


68、线程池大小的选择策略:
①、如果我们的任务主要是进行计算,那么就意味着 CPU 的处理能力是稀缺的资源,我们能够通过大量增加线程数提高计算能力吗?往往是不能的,如果线程太多,反倒可能导致大量的上下文切换开销。所以,这种情况下,通常建议按照 CPU 核的数目 N 或者 N+1
②、如果是需要较多等待的任务,例如 I/O 操作比较多,可以参考:
线程数 = CPU 核数 × (1 + 平均等待时间 / 平均工作时间)

上面是仅仅考虑了 CPU 等限制,实际还可能受各种系统资源限制影响。不要把解决问题的思路全部指望到调整线程池上,很多时候架构上的改变更能解决问题。
(更多线程池相关参考博客【https://blog.csdn.net/qq_33404395/article/details/81328599】)


69、AtomicInteger底层实现原理是什么?所谓的CAS是什么?
AtomicIntger 是对 int 类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于 CAS(compare-and-swap)技术。
CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据(CAS),性能较悲观锁有很大的提高。
CAS 操作包含三个操作数 内存位置(V)预期原值(A)新值(B)。如果内存地址V里面的值和预期的值A是一样的,那么就可以执行更新操作,将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
CAS 是 Java 并发中所谓 lock-free 机制的基础。


70、CAS有什么缺点?
CAS也并不是没有副作用,试想,其常用的失败重试机制,隐含着一个假设,即竞争情况是短暂的。大多数应用场景中,确实大部分重试只会发生一次就获得了成功,但是总是有意外情况,所以在有需要的时候,还是要考虑限制自旋的次数,以免过度消耗 CPU。
另外一个就是著名的ABA问题,这是通常只在 lock-free 算法下暴露的问题。如果对方只是恰好相同,例如期间发生了 A -> B -> A 的更新,仅仅判断数值是 A,可能导致不合理的修改操作。


71、什么是AQS?
AbstractQueuedSynchronizer(AQS),AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。是 Java 并发包中,实现各种同步结构和部分其他组成单元(如线程池中的 Worker)的基础。
AQS 内部数据和方法,可以简单拆分为:
一个 volatile 的整数成员表征状态,同时提供了 setState 和 getState 方法。private volatile int state;
一个先入先出(FIFO)的等待线程队列,以实现多线程间竞争和等待,这是 AQS 机制的核心之一。
各种基于 CAS 的基础操作方法,以及各种期望具体同步结构去实现的 acquire/release 方法。
利用 AQS 实现一个同步结构,至少要实现两个基本类型的方法,分别是 acquire 操作,获取资源的独占权;还有就是 release 操作,释放对某个资源的独占。

ReentrantLock 为例,它内部通过扩展 AQS 实现了 Sync 类型,以 AQS 的 state 来反映锁的持有情况。源码如下:

public void lock() {
    sync.acquire(1);
}
public void unlock() {
    sync.release(1);
}

72、请介绍类加载过程,什么是双亲委派模型?
一般来说,我们把 Java 的类加载过程分为三个主要步骤:加载、链接、初始化。
(1)、加载阶段(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象);加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
(2)、链接(Linking),这是核心的步骤,把原始的类定义信息平滑地转化入 JVM 运行的过程中。这里可进一步细分为三个步骤:
验证(Verification),JVM 需要核验字节信息是符合 Java 虚拟机规范的;
准备(Preparation),创建类或接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行的 JVM 指令。
解析(Resolution),在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。
(3)、初始化阶段(initialization),这一步真正去执行类初始化的代码逻辑。

双亲委派模型,简单说就是当类加载器(Class-Loader)试图加载某个类型的时候,除非找不到更顶层的父类加载器来加载这个类型,否则尽量将这个任务代理给当前加载器的父加载器去执行。使用委派模型的目的是避免重复加载 Java 类型。
类加载器的层级关系可以参考图片:

这里写图片描述


73、类与类加载器
类加载器非常重要,因为每个类加载器都有一个独立的类名称空间。比如我们要加载两个类,如果要比较两个类是否相等(包括equals()方法、isAssignableFrom()方法、isInstance()方法),只有在这两个类被同一个类加载器加载的前提下,比较才有意义。否则,即使两个类来自同一个class文件,被同一个JVM加载,但是加载它们的类加载器不同,则这两个类就不相等。


74、有哪些方法可以在运行时动态生成一个 Java 类?
我们可以从常见的 Java 类来源分析,通常的开发过程是,开发者编写 Java 代码,调用 javac 编译成 class 文件,然后通过类加载机制载入 JVM,就成为应用运行时可以使用的 Java 类了。
那么如何动态生成一个Java类(实质就是在程序中将Java 源码编译成为 JVM 可以理解的字节码,然后进行类加载):
(1)、有一种笨办法,直接用 ProcessBuilder 之类启动 javac 进程,并指定上面生成的文件作为输入,进行编译。最后,再利用类加载器,在运行时加载即可。
(2)、使用 Java Compiler API,这是 JDK 提供的标准 API,java.lang.Compiler里面提供了与 javac 对等的编译器功能。


75、简述普通的 Java 动态代理实现过程:
(1)、提供一个基础的接口,作为被调用类型(即接口的实现类)和代理类之间的统一入口;
(2)、实现InvocationHandler,对代理对象方法的调用,会被分派到其 invoke 方法来真正实现动作。
(3)、通过 Proxy 类,调用其 newProxyInstance 方法(如下),生成一个实现了相应基础接口的代理类实例。
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
(4)、通过代理类实例调用需要执行的方法。


76、谈谈 JVM 内存区域的划分,哪些区域可能发生 OutOfMemoryError?
通常可以把 JVM 内存区域分为下面几个方面:
(1),程序计数器(PC,Program Counter Register)。在 JVM 规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,如果是在执行本地方法,则是未指定值(undefined)。
(2),Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用。
(3),堆(Heap),它是 Java 内存管理的核心区域,用来放置 Java 对象实例,几乎所有创建的 Java 对象实例都是被直接分配在堆上。堆被所有的线程共享,在虚拟机启动时,我们指定的“Xmx”之类参数就是用来指定最大堆空间等指标。
理所当然,堆也是垃圾收集器重点照顾的区域,所以堆内空间还会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分。
(4),方法区(Method Area)。这也是所有线程共享的一块内存区域,用于存储所谓的元(Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。
(5),运行时常量池(Run-Time Constant Pool),这是方法区的一部分。如果仔细分析过反编译的类文件结构,你能看到版本号、字段、方法、超类、接口等各种信息,还有一项信息就是常量池。Java 的常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在运行时决定的符号引用,所以它比一般语言的符号表存储的信息更加宽泛。
(6),本地方法栈(Native Method Stack)。它和 Java 虚拟机栈是非常相似的,支持对本地方法的调用,也是每个线程都会创建一个。

这里写图片描述

除了程序计数器,其他区域都有可能会因为可能的空间不足发生 OutOfMemoryError,简单总结如下:
(1)、堆内存不足是最常见的 OOM 原因之一,抛出的错误信息是“java.lang.OutOfMemoryError:Java heap space”,原因可能千奇百怪,例如,可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定 JVM 堆大小或者指定数值偏小;或者出现 JVM 处理引用不及时,导致堆积起来,内存无法释放等。
(2)、对于 Java 虚拟机栈和本地方法栈。如果我们写一段程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM 实际会抛出 StackOverFlowError;当然,如果 JVM 试图去扩展栈空间的的时候失败,则会抛出 OutOfMemoryError。
(3)、对于老版本的 Oracle JDK,因为永久代的大小是有限的,并且 JVM 对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现 OutOfMemoryError 也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似 Intern 字符串缓存占用太多空间,也会导致 OOM 问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError: PermGen space”。
(4)、直接内存不足,也会导致 OOM。


77、如何监控和诊断JVM堆内存和堆外内存的使用情况?
在windows系统打开命令行 输入Jconsole既可监控JVM虚拟机内存情况(可视化界面)


78、Java 常见的垃圾收集器有哪些?
Serial GC,它是最古老的垃圾收集器,“Serial”体现在其收集工作是单线程的,并且在进行垃圾收集过程中,会进入臭名昭著的“Stop-The-World”状态。当然,其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是 Client 模式下 JVM 的默认选项。
从年代的角度,通常将其老年代实现单独称作 Serial Old,它采用了标记 - 整理(Mark-Compact)算法,区别于新生代的复制算法。
Serial GC 的对应 JVM 参数是:
-XX:+UseSerialGC

ParNew GC,很明显是个新生代 GC 实现,它实际是 Serial GC 的多线程版本,最常见的应用场景是配合老年代的 CMS GC 工作,下面是对应参数
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC

CMS(Concurrent Mark Sweep) GC,基于标记 - 清除(Mark-Sweep)算法,设计目标是尽量减少停顿时间,这一点对于 Web 等反应时间敏感的应用非常重要,一直到今天,仍然有很多系统使用 CMS GC。但是,CMS 采用的标记 - 清除算法,存在着内存碎片化问题,所以难以避免在长时间运行等情况下发生 full GC,导致恶劣的停顿。另外,既然强调了并发(Concurrent),CMS 会占用更多 CPU 资源,并和用户线程争抢。

Parrallel GC,在早期 JDK 8 等版本中,它是 server 模式 JVM 的默认 GC 选择,也被称作是吞吐量优先的 GC。它的算法和 Serial GC 比较相似,尽管实现要复杂的多,其特点是新生代和老年代 GC 都是并行进行的,在常见的服务器环境中更加高效。
开启选项是:-XX:+UseParallelGC
另外,Parallel GC 引入了开发者友好的配置项,我们可以直接设置暂停时间或吞吐量等目标,JVM 会自动进行适应性调整,例如下面参数:

-XX:MaxGCPauseMillis=value
-XX:GCTimeRatio=N // GC 时间和用户时间比例 = 1 / (N+1)

G1 GC这是一种兼顾吞吐量和停顿时间的 GC 实现,是 Oracle JDK 9 以后的默认 GC 选项。G1 可以直观的设定停顿时间的目标,相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。
G1 GC 仍然存在着年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个 region。Region 之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更加明显。
G1 吞吐量和停顿表现都非常不错,并且仍然在不断地完善,与此同时 CMS 已经在 JDK 9 中被标记为废弃(deprecated),所以 G1 GC 值得你深入掌握。


79、Java 内存模型中的 happen-before 是什么?
Happen-before 关系,是 Java 内存模型(Java Memory Model,JMM)中保证多线程操作可见性的机制,例如:
线程内执行的每个操作,都保证 happen-before 后面的操作,这就保证了基本的程序顺序规则,这是开发者在书写程序时的基本约定。
对于 volatile 变量,对它的写操作,保证 happen-before 在随后对该变量的读取操作。
对于一个锁的解锁操作,保证 happen-before 加锁操作。
对象构建完成,保证 happen-before 于 finalizer 的开始动作。
甚至是类似线程内部操作的完成,保证 happen-before 其他 Thread.join() 的线程等。
这些 happen-before 关系是存在着传递性的,如果满足 a happen-before b 和 b happen-before c,那么 a happen-before c 也成立。
happen-before不仅仅是对执行时间的保证,也包括对内存读、写操作顺序的保证。


80、什么是JMM(Java内存模型)
可从四个维度去理解JMM:
(1)从JVM运行时视角来看,JVM内存可分为JVM栈、本地方法栈、PC计数器、方法区、堆;其中前三区是线程所私有的,后两者则是所有线程共有的
(2)从JVM内存功能视角来看,JVM可分为堆内存、非堆内存与其他。其中堆内存对应于上述的堆区;非堆内存对应于上述的JVM栈、本地方法栈、PC计数器、方法区;其他则对应于直接内存
(3)从线程运行视角来看,JVM可分为主内存与线程工作内存。Java内存模型规定了所有的变量都存储在主内存中;每个线程的工作内存保存了被该线程使用到的变量,这些变量是主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量
(4)从垃圾回收视角来看,JVM中的堆区=新生代+老年代。新生代主要用于存放新创建的对象与存活时长小的对象,新生代=E+S1+S2;老年代则用于存放存活时间长的对象。


81、Java 程序运行在 Docker 等容器环境有哪些新问题?
对于 Java 来说,Docker 毕竟是一个较新的环境,例如,其内存、CPU 等资源限制是通过 CGroup(Control Group)实现的,早期的 JDK 版本(8u131 之前)并不能识别这些限制,进而会导致一些基础问题:
如果未配置合适的 JVM 堆和元数据区、直接内存等参数,Java 就有可能试图使用超过容器限制的内存,最终被容器 OOM kill,或者自身发生 OOM。
或者错误判断了可获取的 CPU 资源,例如,Docker 限制了 CPU 的核数,JVM 就可能设置不合适的 GC 并行线程数等。

解决方法:
(1)明确设置堆、元数据区等内存区域大小,保证 Java 进程的总大小可控。
例如,我们可能在环境中,这样限制容器内存:
$ docker run -it --rm --name yourcontainer -p 8080:8080 -m 800M repo/your-java-container:openjdk
就可以额外配置下面的环境变量,直接指定 JVM 堆大小。
-e JAVA_OPTIONS='-Xmx300m'
(2)明确配置 GC 和 JIT 并行线程数目,以避免二者占用过多计算资源。

-XX:ParallelGCThreads
-XX:CICompilerCount

(3)明确配置 GC 和 JIT 并行线程数目,以避免二者占用过多计算资源。

-XX:ParallelGCThreads
-XX:CICompilerCount

81、你了解 Java 应用开发中的注入攻击吗?
注入式(Inject)攻击是一类非常常见的攻击方式,其基本特征是程序允许攻击者将不可信的动态内容注入到程序中,并将其执行,这就可能完全改变最初预计的执行过程,产生恶意效果。
最常见的是 SQL 注入攻击,还有操作系统命令注入、XML 注入攻击等。
在应用实践中,如果对安全要求非常高,建议打开 SecurityManager,-Djava.security.manager。(请注意其开销,通常只要开启 SecurityManager,就会导致 10% ~ 15% 的性能下降,在 JDK 9 以后,这个开销有所改善。)


82、如何预防SQL注入?
(1)可以进行输入校验,限定什么类型的输入是合法的;
(2)Java 应用进行数据库访问时,如果不用完全动态的 SQL,而是利用 PreparedStatement,可以有效防范 SQL 注入。


83、如何尽量保证应用的安全性?
(1)产品设计和框架构建前,需要由团队中的安全方面人员评测其安全性;
(2)开发过程中和团队的code review环节尽量遵循标准的开发规范;(阿里规约插件)


84、JVM 优化 Java 代码时都做了什么?
JVM 在对代码执行的优化可分为运行时(runtime)优化和即时编译器(JIT)优化。
运行时优化主要是解释执行和动态编译通用的一些机制,比如说锁机制(如偏斜锁)、内存分配机制(如 TLAB)等。
JVM 的即时编译器优化是指将热点代码以方法为单位转换成机器码,直接运行在底层硬件之上。


85、谈谈 MySQL 支持的事务隔离级别,以及悲观锁和乐观锁的原理和应用场景?
按照隔离程度从低到高,MySQL 事务隔离级别分为四个不同层次:
读未提交(Read uncommitted),就是一个事务能够看到其他事务尚未提交的修改,这是最低的隔离水平,允许脏读出现。
读已提交(Read committed),事务能够看到的数据都是其他事务已经提交的修改,也就是保证不会看到任何中间性状态,当然脏读也不会出现。读已提交仍然是比较低级别的隔离,并不保证再次读取时能够获取同样的数据,也就是允许其他事务并发修改数据,允许不可重复读和幻象读(Phantom Read)出现。
可重复读(Repeatable reads),保证同一个事务中多次读取的数据是一致的,这是 MySQL InnoDB 引擎的默认隔离级别,但是和一些其他数据库实现不同的是,可以简单认为 MySQL 在可重复读级别不会出现幻象读。
串行化(Serializable),并发事务之间是串行化的,通常意味着读取需要获取共享读锁,更新需要获取排他写锁等,这是最高的隔离级别。

“悲观锁”即认为数据出现冲突的可能性更大,而“乐观锁”则是认为大部分情况不会出现冲突,进而决定是否采取排他性措施。
关于乐观锁(比较适合读取操作比较频繁的场景)和悲观锁(比较适合写入操作比较频繁的场景)的应用场景:
比如火车余票查询和购票系统。同时查询的人可能很多,虽然一个座位只能是卖给一个人,余票可能很多的情况就余票查询就可以采用乐观锁。
但是当真正要购票操作,查询价格等信息时就比较推荐用悲观锁。


86、Hibernate、MyBatis、Spring JDBC Template 各自的设计特点?
Hibernate 是一个 JPA Provider。顾名思义,它是以对象为中心的,其强项更体现在数据库到 Java 对象的映射,可以很方便地在 Java 对象层面体现外键约束等相对复杂的关系,提供了强大的持久化功能。内部大量使用了Lazy-load等技术提高效率。并且,为了屏蔽数据库的差异,降低维护开销,Hibernate 提供了类 SQL 的 HQL,可以自动生成某种数据库特定的 SQL 语句。
但是过度强调持久化和隔离数据库底层细节,也导致了很多弊端,例如 HQL 需要额外的学习,未必比深入学习 SQL 语言更高效;减弱程序员对 SQL 的直接控制,还可能导致其他代价,本来一句 SQL 的事情,可能被 Hibernate 生成几条,隐藏的内部细节也阻碍了进一步的优化。
MyBatis 更加以 SQL 为中心,开发者可以侧重于 SQL 和存储过程,非常简单、直接。如果我们的应用需要大量高性能的或者复杂的 SELECT 语句等,“半自动”的 MyBatis 就会比 Hibernate 更加实用。
Spring JDBC Template 也是更加接近于 SQL 层面的ORM框架。

猜你喜欢

转载自blog.csdn.net/qq_33404395/article/details/81540849