Java工程师面试题

 2019-03-290 Comments

Java基础

1. 为什么重写 equals 还要重写 hashcode

hashcode 相当于身份证,而 equals 则相当于名字,名字相同的有很多,hashcode 相同的也有可能。
比如说在hashset中某一类型重写了equals我们会发现他虽然表面上看上去两个数值 equals 返回正确,但是实际上在 hashset 会分配两个 key ,因为他们的hash之后的hash值是不同的。

2. 说一下 map 的分类和常见情况

Map 不允许键重复,但允许值重复

  1. HashMap
    根据键的 hashcode 的值来存储数据,hash 冲突使用链表,遍历时候取得的数据是随机的,允许一个 null 键,多个 null 值,不支持线程同步,可以使用 synchronzied Map 或者 concurrentHashMap
  2. HashTable
    类似于 hashmap 不同的是 HashTable 不允许键值为空,线程同步,这也导致他速度变慢。
  3. LinkedHashMap
    HashMap 的一个子类,遍历的时候按照插入的顺序,也可以在构造时候带参数,按照自定义顺序遍历,遍历速度比 hashmap 慢
  4. TreeMap
    实现 sortMap 接口,返回的数是按key排序之后的,默认升。

根据各自的优势判断在什么时候使用什么

3. Object如果不重写 hashcode(),hashcode() 如何计算出来。

public native int hashCode();
native 是联合 c++ 的时候使用的

4. == 比较的是什么

基本数据类型比较的是数值
对象引用类型,比较对象内存地址

5. 若对一个类不重写,它的 equals() 方法是如何比较的?

public boolean equals(Object obj) {return (this == obj);}

结合上一题说明是比较两个对象的内存地址,也说明equals() 方法不能对基本数据类型使用

而 String 重写了 equals() 方法

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

先判断两个 String 内存地址,不同比较字符串数值。

6. java8新特性

  1. Lambda 表达式 − Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中。)

  2. 方法引用 − 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

  3. 默认方法 − 默认方法就是一个在接口里面有了一个实现的方法。

  4. 新工具 − 新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。

  5. Stream API −新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。

  6. Date Time API − 加强对日期与时间的处理。

  7. Optional 类 − Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。

  8. Nashorn, JavaScript 引擎 − Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。

7. 说说Lamdba表达式的优缺点

优点:
Lambda 表达式可以使代码变的更加简洁紧凑
Lambda 表达式中使用外部定义的局部变量的时候,局部变量不再必须是final型
缺点:
可读性变得差
只能用于一种情况,那就是仅能用在接口中,且仅仅只在接口中只有一个接口方法的时候有用

8. 一个十进制的数在内存中是怎么存的?

二进制补码

9. 为啥有时会出现4.0-3.6=0.40000001这种现象?

因为十进制的小数在转化为二进制的时候会出现误差,就像十进制无法精确的表达1/3一样,二进制也无法表达1/10。

10. Java支持的数据类型有哪些?什么是自动拆装箱?

数据类型 占用字节
byte 1
short 2
int 4
long 8
float 4
double 8
char 2
boolean 1
自动装箱:自动将基本数据类型转换为包装器类型 比如int i=new Integer(1);或其他包装器类型
拆箱:反一下

11.值传递和引用传递

值传递:(形式参数类型是基本数据类型):方法调用时,实际参数把它的值传递给对应的形式参数,形式参数只是用实际参数的值初始化自己的存储单元内容,是两个不同的存储单元,所以方法执行中形式参数值的改变不影响实际参数的值。
引用传递:(形式参数类型是引用数据类型参数):也称为传地址。方法调用时,实际参数是对象(或数组),这时实际参数与形式参数指向同一个地址,在方法执行中,对形式参数的操作实际上就是对实际参数的操作,这个结果在方法结束后被保留了下来,所以方法执行中形式参数的改变将会影响实际参数。
java类型都是值传递,但是传递的是地址

12. 数组(Array)和列表(ArrayList)有什么区别?什么时候应该使用Array而不是ArrayList?

Array 可以存储基础数据类型或对象,ArrayList 只能存储对象。ArrayList里面的不同的数据可以类型不同,ArrayList<List>
Array 长度固定,ArrayList 会扩容。
ArrayList 提供更多的处理方法,add()等。

适用场景,因为ArrayList 的扩容,可以有时候对于开辟的数据数量会多,造成资源的浪费,而且数值越多,浪费的就可能会越多。所以对于那些固定长度的,无需插入删除的,可以采用数组。而对于那些需要删除等操作的,ArrayList的性能也很低。

13. 你了解大O符号(big-O notation)么?你能给出不同数据结构的例子么?

大O描述当数据结构中的元素增加时,算法的规模和性能在最坏情景下有多好。
大O还可以描述其它行为,比如内存消耗。因为集合类实际上是数据结构,因此我们一般使用大O符号基于时间,内存,性能选择最好的实现。
大O符号可以对大量数据性能给予一个很好的说明。大O符号一般对空间,时间的复杂度进行说明。

14. 如何输出某一种编码的字符串

public String(byte bytes[], String charsetName)
            throws UnsupportedEncodingException {
        this(bytes, 0, bytes.length, charsetName);
    }

String str=new String(new String("ok").getBytes("ISO-8859-1"),"GBK");

15. &和&&的区别

&会将左右两个表达式都执行,然后查看true,false;&&当左边的表达式为false之后就不执行右边的了。
&对于int型的比较会将两个int型转化为二进制,然后逐位比较,都为1则为1,其他为0,然后将结果转化为十进制输出。

16. 在Java中,如何跳出当前的多重嵌套循环?

在你需要跳出循环的地方做一个标记,然后break 到这个标记的地方。

public static void main(String[] args) {
        int z=0;
        //做的一个标记,如果不加这个标记,输出的应该为10,加了标记之后输出1,即直接跳出两重for循环。
        Retr:
        for(int i=0;i<10;i++){
            for(int j=0;j<10;j++){
                z++;
                break Retr;
            }
        }
        System.out.println(z);
    }

17. 正则表达式,用途以及java中的使用。

正则表达式可以用来描述一些文本的规则,例如一个手机号 ^1(3|4|5|7|8)\d{9}$表示以1开头,3或4或5或7或8是第二位剩下九位为随机0-9的数字的字符串。
java中使用:

Pattern r = Pattern.compile("^1(3|4|5|7|8)\d{9}$");
r.matcher(r).find();//true,false

Java关键字

1. Syncronized

Syncronized,一种同步锁,如果两个线程同时公用一个变量,则会导致这个变量的数据无法达到我们的要求,这个时候需要用到所,当一个线程访问这个变量的时候就将他锁起来。用完之后再释放。
(1)修饰普通方法(锁是当前实例对象)
(2)修饰静态方法(锁是当前对象的Class对象)
(3)修饰代码块(锁是Synchonized括号里配置的对象)

2.介绍一下volatile?

参考这里的
volatile作为java中的关键词之一,用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。volatile会禁止指令重排。

并发编程中的三个概念:

  1. 原子性:即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
    比如说一个我们需要在一个线程中执行i++(i++分为读,i+1,写)而当读完之后之后被另一个线程打断另一个线程读取了i的值并进行+1,而最后两个线程运行完会变为i+1,而不是i+2。
  2. 可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  3. 有序性:即程序执行的顺序按照代码的先后顺序执行。
    单线程不会因为重排序而影响最后的结果,而多线程则有可能会导致最后结果不同。
    如下的例子,a与z没有数据依赖性,则有可能导致a与z进行重排序,此时,线程二会以为z为true了就跳出循环,执行equals,而此时a还为null,会出现NullPointException。

     //线程一
     a="111";
     z=true;
    
     //线程2
     while(!z){}
     a.equals("111");
    

volatile:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。volatile会将修改完后的值强行写入主存,这样其他的线程访问就可以访问到修改候的值。
2)禁止进行指令重排序。在一定程度上保证了有序性。
3) volatile无法保证原子性。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
使用volatile必须具备以下2个条件:
在保证原子性的前提下,可以替换synchronized关键字,因为synchronized的性能低于volatile
1)对变量的写操作不依赖于当前值,对变量的读操作不依赖于其他非volatile值。
2)适用于读多写少的场景。
3)可用作状态标志。
4)JDK中volatie应用:JDK中ConcurrentHashMap的Entry的value和next被声明为volatile,AtomicLong中的value被声明为volatile。AtomicLong通过CAS原理(也可以理解为乐观锁)保证了原子性。

3. Synchronized和lock

还是参考这个人的
Synchronized的不足:
Synchronized释放锁的条件可以有两种:执行完代码,或是线程运行异常。这样就会导致如果带锁的线程被阻塞了,其他线程就无法获得锁,会影响效率
对文件进行读操作时,线程之间不会有冲突,那么如果此时使用了Synchronized,就会产生一个线程读取的时候其他线程无法读取的问题。
也就是说lock作为类可以可以实现更多的功能,但是lock需要手动释放锁,不然会导致死锁问题,所以在lock的使用时候一般加入try{}catch{}然后在finally中释放锁lock.unlock();

不同点:

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
  2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  3. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  4. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  5. Lock可以提高多个线程进行读操作的效率。
    锁的几个概念:
  6. 可重入锁:像synchronized和ReentrantLock都是可重入锁,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
    2.可中断锁:synchronized就不是可中断锁,而Lock是可中断锁,可以中断其他等待(不可以中断正在运行的线程)锁的线程。
    3.公平锁:公平锁即尽量以请求锁的顺序来获取锁。先给等最长时间的线程。synchronized非公平锁。ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
  7. 读写锁:读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁,使线程不会发生冲突。ReentrantReadWriteLock可重入读写锁,可以通过readLock()获取读锁,通过writeLock()获取写锁。

4. final关键字

final根据修饰位置的不同作用也不相同,针对三种情况:
1)修饰变量,被final修饰的变量必须要初始化,赋初值后不能再重新赋值。
注意:局部变量不在我们讨论的范畴,因为局部变量本身就有作用范围,不使用private、public等词修饰。
2)修饰方法,被final修饰的方法代表不能重写。
3)修饰类,被final修饰的类,不能够被继承。所以String不可以被继承。
注意:final修饰的类,类中的所有成员方法都被隐式地指定为final方法。
final修饰变量指的是:这个变量被初始化后便不可改变,这里不可改变的意思对基本类型来说是其值不可变,而对于对象变量来说其引用不可变,即不能再指向其他的对象。

a a=new a();
a.z=11;
a.z=12
//会输出12

Java面向对象

1. String对象为什么不可变。

private final char value[];使用的数组前加了fianl即value一旦初始化之后就无法修改。而且 String 也没有提供 setValue 等类似的操作。
我们看见的 replace 方法实际返回的是一个新的String数组。

2. Object类的方法

  1. clone
  2. getClass
  3. toString
  4. finalize
  5. equals
  6. hashCode
  7. wait
  8. notify
  9. notifyAll

3. 重载和重写的区别?相同参数不同返回值能重载吗?

重载是本方法中同一个方法名不同参数的重写,可以返回值相同,但是参数一定不同。
重写是子类重写父类的方法。而且参数与返回值必须都相同。

4. Static

Static会在对象初始化的时候就会加载。
static类不可以被覆盖(重写),覆盖不会报错,但会返回父类的结果。

5.StringBuilder 和 StringBuffer

都继承自 AbstractStringBuilder,但是 StringBuffer 方法在前面加入了很多 synchronized,因此 StringBuffer 线程安全,StringBuilder 线程不安全,但是单线程的情况下他的速度比后者快。

6. 类加载机制,双亲委派模型,好处是什么?

类加载,JVM第一次使用到这个类时需要对,这个类的信息进行加载。一个类只会加载一次,之后这个类的信息放在堆空间,静态属性放在方法区。JVM类加载器从上到下一共分为三类 1.启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。 2. 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。 3. 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。 JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。

加载阶段:
1.通过“类全名”来获取定义此类的二进制字节流
2.将字节流所代表的静态存储结构转换为方法区的运行时数据结构
3.在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

双亲委派模型过程

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。

7. 通过反射创建对象。

Class<?> clz = Class.forName("test.java.zxj.test_project.User");获取class后可采用两种方式进行创建。
1.通过Class字节码对象newInstance();(默认通过无参构造创建)Object object = clz.newInstance();
2.通过获取构造器getConstructor(Class<?>..parameterTypes);(通过有参的构造器,参数可以指定具体类型和多个数量)

Constructor<?> constructor = clz.getConstructor(String.class, String.class);
Object object = constructor.newInstance("userId_001","name_jack");

8. 栈帧的结构

名称 作用
局部变量表 局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。
操作数栈 当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。
动态连接 在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。
方法出口 当一个方法开始执行时,可能有两种方式退出该方法:正常完成出口、异常完成出口

集合

1. HashMap如果key是一个自定义的类。

需要重写这个类的equals(),和hashcode()方法。
也可以只new,new完之后直接使用这个new。

2. hashmap负载因子,

负载因子表示一个散列表的空间的使用程度,有这样一个公式:initailCapacity*loadFactor=HashMap的容量。
所以负载因子越大则散列表的装填程度越高,也就是能容纳更多的元素,元素多了,链表大了,所以此时索引效率就会降低。
反之,负载因子越小则链表中的数据量就越稀疏,此时会对空间造成浪费,但是此时索引效率高。

initialCapacity为什么是2的n次:
当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了,这是因为hashcode的值总是会和initialCapacity-1与一下(为了可以将元素位置尽量的分布均匀些),如果不是2的n次,减一转换为二进制之后必定有某几位为0,此时hashcode当前位置无论是什么都会返回0,那么就会导致更多的值相同,是的数据分布不均匀

3. 基本的集合接口

collection-list、set
Map

4. 为什么集合类没有实现Cloneable和Serializable接口?

克隆(cloning)或者是序列化(serialization)的语义和含义是跟具体的实现相关的。因此,应该由集合类的具体实现来决定如何被克隆或者是序列化。
(1)什么是克隆?
克隆是把一个对象里面的属性值,复制给另一个对象。而不是对象引用的复制
(2)实现Serializable序列化的作用
1.将对象的状态保存在存储媒体中一边可以在以后重写创建出完全相同的副本
2.按值将对象从一个应用程序域法相另一个应用程序域

线程

1. 生产者消费者模式

生产数据一方称为生产者,处理数据一方成为消费者。
生产者生产出来的数据放入到缓存区,再由消费者在缓存区中读取处理。
缓存区的好处:
解耦:如果没有缓冲区,则消费者会对生产者产生依赖,增加耦合度。
支持并发:如果没有这个缓冲区,消费者在生产者生产完成之前需要一直等到,因为是同步的,而有了缓冲区两者可以同时进行。
支持忙闲不均:当生产数据时快时慢的时候,如果生产者的生产速度大于处理速度的时候,可以将多余的数据先放入缓冲区。
具体实现(wait/notity方法)如下

/**
 * 生产者
 */
class Producers implements Runnable{
    private ArrayList<Integer> arrayList;
    public Producers(ArrayList<Integer> arrayList) {
        this.arrayList=arrayList;
    }

    @Override
    public void run() {
        synchronized (arrayList){
            for(int i=1;i<=100;i++){
                while(arrayList.size()==5){
                    try {
                        arrayList.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName()+"生产"+i+":"+arrayList.add(i));
                arrayList.notify();
            }
        }
    }
}
/**
 * 消费者
 */
class Consumers implements Runnable{
    private ArrayList<Integer> arrayList;
    public Consumers(ArrayList<Integer> arrayList) {
        this.arrayList=arrayList;
    }

    @Override
    public void run() {
        synchronized (arrayList){
            while(true) {
                while (arrayList.size() == 0) {
                    try {
                        arrayList.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName()+"消费了" + arrayList.remove(0));
                arrayList.notify();
            }
        }
    }
}
public class Main {
    public static ArrayList<Integer> arrs=new ArrayList<>();//阻塞队列
    public static void main(String[] args) {
        Producers p=new Producers(arrs);
        Consumers c=new Consumers(arrs);
        Producers p1=new Producers(arrs);
        Consumers c1=new Consumers(arrs);
        new Thread(p).start();
        new Thread(c).start();
        new Thread(p1).start();
        new Thread(c1).start();
    }
}

2. 线程和进程

进程 线程
操作系统资源分配的基本单位 任务调度和执行的基本单位
切换进程开销大 切换线程开销小
有各自的内存空间 共享线程中的资源
操作系统中可以有多个进程 一个进程可以有多个线程

3. 四种线程池

(1)CachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。将核心池的大小设置为0(这样可以回收空闲队列),最大线程池设置为无限大,阻塞队列使用同步队列。
(2)FixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 阻塞队列无限长,核心池和最大池长度相同,且都为传输进来的数据。
(3)ScheduledThreadPool 创建一个定时线程池,支持定时及周期性任务执行。
(4)SingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。线程池和核心池都为1,保证单线程,阻塞队列无限长,保证不会被饱和策略拒绝。

4. yield(),wait(),sleep()

  1. sleep:Thread类的方法,必须带一个时间参数。会让当前线程休眠进入阻塞状态并释放CPU(阿里面试题 Sleep释放CPU,wait呢),提供其他线程运行的机会且不考虑优先级,但如果有同步锁则sleep不会释放锁即其他线程无法获得同步锁

  2. yield:让出CPU调度,Thread类的方法,类似sleep只是不能由用户指定暂停多长时间 ,并且yield()方法只能让同优先级的线程有执行的机会。 yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。调用yield方法只是一个建议,告诉线程调度器我的工作已经做的差不多了,可以让别的相同优先级的线程使用CPU了,没有任何机制保证采纳。

  3. wait:Object类的方法(notify()、notifyAll() 也是Object对象),必须放在循环体和同步代码块中,执行该方法的线程会释放锁,进入线程等待池中等待被再次唤醒(notify随机唤醒,notifyAll全部唤醒,线程结束自动唤醒)即放入锁池中竞争同步锁

5. 当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?

可以调用对象中的其他非同步方法,可以调用其他或这个同步方法

6.四种饱和策略

  1. AbortPolicy 拒绝并抛出异常。
  2. DiscartPolicy 直接拒绝。
  3. DiscartOldestPolicy 将最早进入队列的任务删除,然后再进入队列。
  4. CallerRunsPolicy 使用调用者的线程来运行。

7.死锁具体实例

当一个1线程获取A锁后,线程暂停,此时2线程获取B锁可以获取,然后获取A锁导致2线程阻塞,此时1线程暂停结束,获取B锁,B被2占用则1阻塞

public class Main {
    //注意二者不可相同,例如A=0,B=0;相同则相当于一个实例,由于可重入锁,线程1再获取A锁后仍然可以获取B锁。
    private static Integer A = 1;
    private static Integer B = 2;
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (A){
                try {
                    Thread.currentThread().sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (B){
                    System.out.println("1");
                }
            }
        }).start();
        new Thread(() -> {
            synchronized (B){
                synchronized (A){
                    System.out.println("2");
                }
            }
        }).start();
    }
}

JVM

1. JVM回收算法和回收器,CMS采用哪种回收算法,怎么解决内存碎片问题?

标记清除算法,复制算法,标记整理算法。
6个收集器:serials收集器、pranew收集器、parallel收集器、parallel old收集器、cms收集器、G1收集器
老年代采用标记整理,年轻代采用复制算法。
标记整理方法将存活的对象推到内推的某一个区域,然后回收此区域外的对象。
复制算法清理之后将对象按链表顺序复制到另半边内存。

2. JVM分区

执行引擎
数据区:本地堆、虚拟机堆、java栈、方法区、程序计数器。
本地接口

3. eden区,survial区

新生代中会分为一个eden区和两个survial区,新建的对象会在eden区,然后一次GC之后存活对象回到survial区,然后再经过一次GC回到第二个survial区,重复多次之后(默认15次),第二个survial区域的对象就变为了老年代。

4. GC中如何判断对象需要被回收

引用计数(引用了+1,释放了-1,为0释放,不可以在循环应用时候使用),可达分析法(从GCRoots向下搜索,没搜索到的就是需要回收的)。

5. 哪些可以用作root

  1. 方法区中的静态引用指向的对象
  2. 方法区中常量引用指向的对象
  3. 被启动类(bootstrap 加载器)加载的类和创建的对象(final?)
  4. 本机栈JNI引用的对象
  5. 虚拟机栈中引用的对象

4. 内存泄漏

内存泄漏定义(memory leak):一个不再被程序使用的对象或变量还在内存中占有存储空间。

容易发生的场景:

  1. 集合类,单单添加元素没有删除就会产生内存泄漏(全局变量不会发生)
  2. 单例模式。
  3. 变量不合理的作用域。
  4. 内部类持有外部类
  5. 改变哈希值

redis

1. Redis的数据结构

String,Hash,list,set,sortedset

2.持久化方式,优缺点

  1. RDB 在指定的一段时间中将内存中的数据快照写入磁盘,fork子进程,先将数据存放在临时文件,写入完成之后,覆盖之前的文件,用二进制压缩存储。
  2. AOF 持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

优缺点:

RDB优点方便回复、性能好。
1). 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。
2). 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。
3). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。
4). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。
RDB又存在哪些劣势呢? 无法保证数据全部不丢失。数据过大,写入过程慢
1). 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
AOF的优势有哪些呢? 数据不容易丢失
1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。
2). 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。
3). 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。
4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
AOF的劣势有哪些呢? 恢复速度慢,性能不好
1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。
二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。

3.redis集群

注册中心宕机

  1. 注册中心对等集群,任意一台宕掉,将自动切换到另一台
  2. 注册中心全部宕掉后,服务提供者和服务消费者,仍能通过本地缓存通讯(注册中心在启动时消费者会拉取生产者的地址等信息缓存到本地)
  3. 数据库宕掉注册中心,仍能通过缓存提供服务列表查询,但不能注册新服务
  4. 服务提供者无状态,任意一台宕掉,不影响使用
  5. 服务提供者全部宕掉,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
  6. 监控中心宕掉不影响使用,只是丢失部分采样数据
  7. 服务提供者全部宕掉,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复

设计模式

1. 双重加锁式懒汉单例模式

为了弥补饿汉模式下的启动时创建对象的缺点。采用用到单例对象的时候没有单例对象再去创建。不在方法上加锁保证在对象不为空时候不需要阻塞可以直接返回。8、10行两重check是因为如下例子:A线程如果运行到8行切换到B,如果没有10行,B获取锁,A阻塞,B创建,释放锁,A获取锁,A运行,A创建实例,会产生两个对象,所以需要加上10行的判断。如果没有8行,每次想要获取对象的时候都会锁,就降低了效率,加上8行后只有等到第一次加载的时候才会锁。
使用volatile创建instance的原因是当A线程创建Singleton时候会主动写入主存,让B读到

public class Singleton {
    private volatile static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {//8行
            synchronized (Singleton.class) {
                if (instance == null) {//10行
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

数据库

1. 数据库四大特性和事务隔离级别

事务的四大特性(ACID): 原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)
事务隔离等级: Read uncommitted(读未提交)、Read committed(读已提交)、Repeatable read(可重复读)、Serializable(序列化)
三个问题: 脏读、不可重复度、幻读

级别 脏读 不可重复读 幻读
读未提交 可能 可能 可能
读已提交 不可能 可能 可能
可重复读 不可能 不可能 可能
可串行化 不可能 不可能 不可能

2. SQL语句

如果存在就更新,否则插入
insert into … on duplicate key update …;判断的key需要为唯一索引
例如一个表

key value
1 1
2 2

如果key存在就value+1 否则插入
insert into table(‘key’,’value’) value(1,2) on duplicate key update value=value+1;

3. B和B+树

b+树更加稳定,因为每一个都是需要查到子节点,查询次数一样。
b+树非子节点不存储数据,使他可以有更多的中间节点,更加矮胖。
b+树子节点用链连接再一起,使范围查找不需要像b树那样从新来一遍