Looking forward to interview
yuuki Lv5

Java 基础

  • 介绍一下 volatile?

    volatile 关键字是用来保证有序性和可见性的。这跟 Java 内存模型有关。比如我们所写的 代码,不一定是按照我们自己书写的顺序来执行的,编译器会做重排序,CPU 也会做重排序的, 这样的重排序是为了减少流水线的阻塞的,引起流水阻塞,比如数据相关性,提高 CPU 的执行效 率。需要有一定的顺序和规则来保证,不然程序员自己写的代码都不知带对不对了,所以有 happens-before 规则,其中有条就是 volatile 变量规则:对一个变量的写操作先行发生于后面 对这个变量的读操作;有序性实现的是通过插入内存屏障来保证的。可见性:首先 Java 内存模 型分为,主内存,工作内存。比如线程 A 从主内存把变量从主内存读到了自己的工作内存中,做 了加 1 的操作,但是此时没有将 i 的最新值刷新会主内存中,线程 B 此时读到的还是 i 的旧值。 加了volatile关键字的代码生成的汇编代码发现,会多出一个lock前缀指令。 Lock指令对Intel 平台的 CPU,早期是锁总线,这样代价太高了,后面提出了缓存一致性协议,MESI,来保证了多 核之间数据不一致性问题。

  • 锁有了解吗? 说一下 Synchronized 和 lock

    synchronized 是 Java 的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证 在同一时刻最多只有一个线程执行该段代码。JDK1.5 以后引入了自旋锁、锁粗化、轻量级锁, 偏向锁来有优化关键字的性能。
    Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现; synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;Lock 可以让等待锁的线程响应中断,而 synchronized 却不行, 使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断;通过 Lock 可以知道有没 有成功获取锁,而 synchronized 却无法办到。

  • Java 里面的 final 关键字的作用

    当用 final 修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用 final 进行修饰。final 类中的成员变量可以更具需要设定为 final,但是要注意 final 类中的所有成员方法都会被隐式的指定为final方法。

    对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后就不能再被改变;如果是引用类型变量,则在对其初始化之后便不能再让其指向另一个对象。

  • Java 有哪些特性,举个多态的例子。

    封装、继承、多态。多态:指允许不同类的对象对同一消息做出响应。即同一消息可以根据 发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)

  • String 为啥不可变?

    不可变对象是指一个对象的状态在对象被创建之后就不再变化。不可改变的意思就是说:不 能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对 象,引用类型指向的对象的状态也不能改变。
    String 不可变是因为在 JDK 中 String 类被声明为一个 final 类,且类内部的 value 字 节数组也是 final 的,只有当字符串是不可变时字符串池才有可能实现,字符串池的实现可以 在运行时节约很多 heap 空间,因为不同的字符串变量都指向池中的同一个字符串;如果字符串 是可变的则会引起很严重的安全问题,譬如数据库的用户名密码都是以字符串的形式传入来获得 数据库的连接,或者在 socket 编程中主机名和端口都是以字符串的形式传入,因为字符串是不 可变的,所以它的值是不可改变的,否则黑客们可以钻到空子改变字符串指向的对象的值造成安 全漏洞;因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享, 这样便不用因为线程安全问题而使用同步,字符串自己便是线程安全的;因为字符串是不可变的 所以在它创建的时候 hashcode 就被缓存了,不变性也保证了 hash 码的唯一性,不需要重新计 算,这就使得字符串很适合作为 Map 的键,字符串的处理速度要快过其它的键对象,这就是 HashMap 中的键往往都使用字符串的原因。

  • 请列举你所知道的 Object 类的方法

    Object()默认构造方法。clone() 创建并返回此对象的一个副本。equals(Object obj) 指 示某个其他对象是否与此对象“相等”。finalize()当垃圾回收器确定不存在对该对象的更多引 用时,由对象的垃圾回收器调用此方法。getClass()返回一个对象的运行时类。hashCode()返回 该对象的哈希码值。 notify()唤醒在此对象监视器上等待的单个线程。 notifyAll()唤醒在此 对象监视器上等待的所有线程。toString()返回该对象的字符串表示。wait()导致当前的线程等 待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。wait(long timeout)导 致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超 过指定的时间量。wait(long timeout, int nanos) 导致当前的线程等待,直到其他线程调用此 对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某 个实际时间量。

  • StringBuffer 和 StringBuilder 有什么区别?底层区别?

    StringBuffer 线程安全,StringBuilder 线程不安全,底层实现上的话,StringBuffer 其 实就是比 StringBuilder 多了 Synchronized 修饰符。

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

    双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

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

  • 解释 extends 和 super 泛型限定符-上界不存下界不取

    • 上界 <? extend Fruit> ,表示所有继承 Fruit 的子类,但是具体是哪个子类,无法确 定,所以调用 add 的时候,要 add 什么类型,谁也不知道。但是 get 的时候,不管是什么子类, 不管追溯多少辈,肯定有个父类是 Fruit,所以,我都可以用最大的父类 Fruit 接着,也就是把 所有的子类向上转型为 Fruit。
    • 下界 <? super Apple>, 表示Apple的所有父类,包括Fruit, 一直可以追溯到老祖宗Object 。 那么当我 add 的时候,我不能 add Apple 的父类,因为不能确定 List 里面存放的到底是哪个父 类。但是我可以 add Apple 及其子类。因为不管我的子类是什么类型,它都可以向上转型为 Apple 及其所有的父类甚至转型为 Object 。但是当我 get 的时候,Apple 的父类这么多,我用什么接 着呢,除了 Object,其他的都接不住。
  • 谈谈如何通过反射创建对象?

    • 方法 1:通过类对象调用 newInstance() 方法,例如:String.class.newInstance()
    • 方法 2:通过类对象的 getConstructor() 或 getDeclaredConstructor() 方法获得构造器 (Constructor)对象并调用其 newInstance()方法创建对象,例如: String.class.getConstructor(String.class).newInstance(“Hello”);
  • hashMap 内部具体如何实现的?

    Hashmap 基于数组实现的,通过对 key 的 hashcode & 数组的长度得到在数组中位置,如当 前数组有元素,则数组当前元素 next 指向要插入的元素,这样来解决 hash 冲突的,形成了拉链 式的结构。put 时在多线程情况下,会形成环从而导致死循环。数组长度一般是 2n,从 0 开始编 号,所以 hashcode & (2n-1),(2n-1)每一位都是 1,这样会让散列均匀。需要注意的是, HashMap 在 JDK1.8 的版本中引入了红黑树结构做优化,当链表元素个数大于等于 8 时,链表转 换成树结构;若桶中链表元素个数小于等于 6 时,树结构还原成链表。因为红黑树的平均查找长 度是 log(n),长度为 8 的时候,平均查找长度为 3,如果继续使用链表,平均查找长度为 8/2=4, 这才有转换为树的必要。链表长度如果是小于等于 6,6/2=3,虽然速度也很快的,但是转化为 树结构和生成树的时间并不会太短。还有选择 6 和 8,中间有个差值 7 可以有效防止链表和树频 繁转换。假设一下,如果设计成链表个数超过 8 则链表转换成树结构,链表个数小于 8 则树结构 转换成链表,如果一个 HashMap 不停的插入、删除元素,链表个数在 8 左右徘徊,就会频繁的发 生树转链表、链表转树,效率会很低。

  • 如果 hashMap 的 key 是一个自定义的类,怎么办?

    使用 HashMap,如果 key 是自定义的类,就必须重写 hashcode()和 equals()。

  • Concurrenthashmap 有什么优势, 1.7, 1.8 区别?

    Concurrenthashmap 线程安全的, 1.7 是在 jdk1.7 中采用 Segment + HashEntry 的方式进 行实现的, lock 加在 Segment 上面。 1.7size 计算是先采用不加锁的方式,连续计算元素的个数, 最多计算 3 次:1、如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;2、如果 前后两次计算结果都不同,则给每个 Segment 进行加锁,再计算一次元素的个数;
    1.8 中放弃了 Segment 臃肿的设计,取而代之的是采用 Node + CAS + Synchronized 来 保证并发安全进行实现,1.8 中使用一个 volatile 类型的变量 baseCount 记录元素的个数,当 插入新数据或则删除数据时,会通过 addCount()方法更新 baseCount,通过累加 baseCount 和 CounterCell 数组中的数量,即可得到元素的总个数;

  • 线程池运行流程,参数,策略

    线程池主要就是指定线程池核心线程数大小,最大线程数,存储的队列,拒绝策略,空闲线 程存活时长。当需要任务大于核心线程数时候,就开始把任务往存储任务的队列里,当存储队列 满了的话,就开始增加线程池创建的线程数量,如果当线程数量也达到了最大,就开始执行拒绝 策略,比如说记录日志,直接丢弃,或者丢弃最老的任务。

  • Java 中有常用的几种线程池

    1、newFixedThreadPool 创建一个指定工作线程数量的线程池。每当提交一个任务就创建一 个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
    2、newCachedThreadPool 创建一个可缓存的线程池。这种类型的线程池特点是:
    1).工作线程的创建数量几乎没有限制(其实也有限制的,数目为 Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
    2).如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为 1 分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个 工作线程。
    3、newSingleThreadExecutor 创建一个单线程化的 Executor,即只创建唯一的工作者线程 来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行(我觉得这点是它的特 色)。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个 线程是活动的 。
    4、newScheduleThreadPool 创建一个定长的线程池,而且支持定时的以及周期性的任务执 行,类似于 Timer。(这种线程池原理暂还没完全了解透彻)

  • 概括的解释下线程的几种可用状态。

    1. 新建( new ):新创建了一个线程对象。

    2. 可运行( runnable ):线程对象创建后,其他线程(比如 main 线程)调用了该对象 的 start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获 取 cpu 的使用权 。

    3. 运行( running ):可运行状态( runnable )的线程获得了 cpu 时间片( timeslice ) , 执行程序代码。

    4. 阻塞( block ):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有 机会再次获得 cpu timeslice 转到运行( running )状态。

      阻塞的情况分三种:

      (一). 等待阻塞:运行( running )的线程执行 o . wait ()方法, JVM 会把该线程放 入等待 队列( waitting queue )中。

      (二). 同步阻塞:运行( running )的线程在获取对象的同步锁时,若该同步锁 被别的线程占用, 则 JVM 会把该线程放入锁池( lock pool )中。

      (三). 其他阻塞: 运行( running )的线程执行 Thread . sleep ( long ms )或 t . join () 方法,或者发出了 I / O 请求时, JVM 会把该线程置为阻塞状态。 当 sleep ()状态超时、join ()等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行( runnable )状态。

    5. 死亡( dead ):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则 该线程结束生命周期。死亡的线程不可再次复生。

  • 讲一下非公平锁和公平锁在 reetrantlock 里的实现。

    如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,FIFO。对于非公 平锁,只要 CAS 设置同步状态成功,则表示当前线程获取了锁,而公平锁还需要判断当前节点是 否有前驱节点,如果有,则表示有线程比当前线程更早请求获取锁,因此需要等待前驱线程获取 并释放锁之后才能继续获取锁。

 评论