[面试]Java常见面试题,持续更新...

一.List集合类

ArrayList和LinkedList的区别

从底层数据结构以及访问、添加、删除的效率分别说明
注意: ArrayList当数组大小不知足时须要增长存储能力,就要将已经有数组的数据复制到新的存储空间java

Vector

Vector也是经过数组实现的,但它支持线程同步,避免同时写引发的不一致性,实现同步须要很高的开销,所以它的访问比ArrayList慢算法

二.Set 集合类

Set排序子类、HashSet、hashCode()、equals的关系
  • HashSet:依靠HashCode和equals方法判断重复,首先判断哈希值,若是哈希值相同再判断equals;相同哈希值但equals不一样的,放在一个bucket里,;它是无序的
  • TreeSet:二叉树原理;有序的,须要实现Comparable接口;
  • LinkedHashSet:继承了HashSet,方法接口与HashSet相同;底层使用LinkedHashMap 来保存全部元素;属于有序,增长顺序为保存顺序

三.Map类

HashMap的底层实现原理

jdk1.7中的HashMap
image.png
HashMap是一个数组,数组的每个元素是一个Entry的链表
每个Entry对象包括了Keyvaluehash值和指向下一个元素的next数组

HashMap包括两个构造参数:
1.capacity:当前数组的容量,可扩容,扩容后的大小为当前数组大小的两倍
2.loadFactor:负载因子,默认0.75缓存

HashMap(int initialCapacity, float loadFactor)安全

threshold:扩容的阈值,等于capacity * loadFactor数据结构

JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,在这些位置进行查找的时候能够下降时间复杂度为 O(logN)。多线程

(1). HashMap 容许有一个null的key,容许多个value为null
(2). HashMap 是线程不安全的,可经过Collections的synchronizedMap 方法使HashMap 具备线程安全的能力,或者使用ConcurrentHashMap
ConcurrentHashMap
  • ConcurrentHashMap支持并发操做,线程安全
  • 由一个个Segment组成,是一个Segment数组,Segment经过继承ReentrantLock进行加锁,每次须要加锁的操做锁住的是一个 segment,这样只要保证每一个 Segment 是线程安全的,也就实现了全局的线程安全.
  • 一个Segment至关于一个HashMap

构造参数:
concurrencyLevel:并行数,也即segment的个数,默认16,初始化后不可更改或扩容;一个segment同时只容许一个线程操做, 16个segment容许16个线程在各类不一样的segment上并发写;并发

HashMap和HashTable的区别

HashTable是上古版本的遗留类,如今不多使用,app

  • 继承自Dictionary类
  • HashTable线程安全, 但并发性不如ConcurrentHashMap, 由于后者引入了分段锁。 而HashMap非线程安全。
  • HashTable不容许key为null
  • HashMap把Hashtable的contains方法去掉了,改为containsValue和containsKey,
  • 初始容量和扩容:HashTable在不指定容量的状况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量必定要为2的整数次幂,而HashMap则要求必定为2的整数次幂。 Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍
HashSet

HashSet 底层是由HashMap实现, 因此也是非线程安全,
其实HashSet也是<K,V> 只不过对每一个元素都是同一个object对象做为Vjvm

TreeMap(可排序)
  • 实现SortedMap 接口,可以把它保存的记录根据键排序
  • key 必须实现Comparable 接口或者在构造TreeMap 传入自定义的Comparator
LinkHashMap(记录插入顺序)

它继承HashMap、底层使用哈希表与双向链表来保存全部元素
Entry元素比HashMap多了:
Entry<K, V> before
Entry<K, V> after
before、After是用于维护Entry插入的前后顺序的

大概的图:
image.png


多线程相关

四.线程池

为何使用线程池

1.减小了建立和销毁线程的次数,每一个工做线程均可以被重复利用,可执行多个任务。
2.能够根据系统的承受能力,调整线程池中工做线线程的数目,防止消耗过多的内存

newCachedThreadPool
  • 建立一个可根据须要建立新线程的线程池, 线程池不会对线程池大小作限制,线程池大小彻底依赖于操做系统(或者说JVM)可以建立的最大线程大小
  • 调用 execute 将重用之前构造的线程(若是线程可用)
  • 若是线程池的大小超过了处理任务所须要的线程,那么就会回收部分空闲(60秒不执行任务)的线程
newFixedThreadPool
  • 建立一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程
  • 建立固定大小的线程池。每次提交一个任务就建立一个线程,直到线程达到线程池的最大大小
  • 线程池的大小一旦达到最大值就会保持不变,若是某个线程由于执行异常而结束,那么线程池会补充一个新线程
newScheduledThreadPool

建立一个线程池,它可安排在给定延迟后运行命令或者按期地执行。

ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
scheduledThreadPool.schedule(newRunnable(){
    @Override
    public void run() {
    System.out.println("延迟三秒");
    }}, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){
    @Override
    public void run() {
    System.out.println("延迟1 秒后每三秒执行一次");
    }},1,3,TimeUnit.SECONDS);
newSingleThreadExecutor
  • 建立一个单线程的线程池。这个线程池只有一个线程在工做,也就是至关于单线程串行执行全部任务
  • 若是这个惟一的线程由于异常结束,那么会有一个新的线程来替代它。
  • 此线程池保证全部任务的执行顺序按照任务的提交顺序执行

五.java线程的生命周期

在线程的生命周期中,它要通过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态

新建状态 new

当程序使用new 关键字建立了一个线程以后,该线程就处于新建状态,此时仅由JVM 为其分配内存,并初始化其成员变量的值

就绪状态 Runnable

当线程对象调用了start()方法以后,该线程处于就绪状态。Java 虚拟机会为其建立方法调用栈和程序计数器,等待调度运行。

运行状态 Running

若是处于就绪状态的线程得到了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。

阻塞状态 Blocked

阻塞状态是指线程由于某种缘由放弃了cpu 使用权,暂时中止运行
阻塞的状况分三种:

  1. 等待阻塞: o.wait->等待队列

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

  1. 同步阻塞(lock->lockPool)

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

  1. 其余阻塞(sleep/join)

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

死亡状态 dead

正常结束

  1. run()或call()方法执行完成,线程正常结束。

异常结束

  1. 线程抛出一个未捕获的Exception 或Error。

调用 stop

  1. 直接调用该线程的stop()方法来结束该线程—该方法一般容易致使死锁,不推荐使用。

六.Volatile 关键字

可见性

在访问volatile 变量时不会执行加锁操做,所以也就不会使执行线程阻塞,所以volatile 变量是一种比sychronized 关键字更轻量级的同步机制。volatile 适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值

image.png
简单原理:
当对非 volatile 变量进行读写的时候,每一个线程先从内存拷贝变量到CPU 缓存中。若是计算机有
多个CPU,每一个线程可能在不一样的CPU 上被处理,这意味着每一个线程能够拷贝到不一样的 CPU
cache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache这一步。

屏蔽指令重排序

指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果是正确的,可是没法保证程序的操做顺序与代码顺序一致。这在单线程中不会构成问题,可是在多线程中就会出现问题。很是经典的例子是在单例方法中同时对字段加入voliate,就是为了防止指令重排序。

public class Singleton {
    private volatile static Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) { // 1
            synchronized(Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton(); // 2
                }
            }
        }
        return singleton;
    }
}

实际上当程序执行到2处的时候,若是咱们没有使用volatile关键字修饰变量singleton,就可能会形成错误。这是由于使用new关键字初始化一个对象的过程并非一个原子的操做,它分红下面三个步骤进行:

a. 给 singleton 分配内存
b. 调用 Singleton 的构造函数来初始化成员变量
c. 将 singleton 对象指向分配的内存空间(执行完这步 singleton 就为非 null 了)

若是虚拟机存在指令重排序优化,则步骤b和c的顺序是没法肯定的。若是A线程率先进入同步代码块并先执行了c而没有执行b,此时由于singleton已经非null。这时候线程B到了1处,判断singleton非null并将其返回使用,由于此时Singleton实际上还未初始化,天然就会出错。synchronized能够解决内存可见性,可是不能解决重排序问题。

不能保证原子性

举例说明:
以i++为例,其包括读取、操做、赋值三个操做,下面是两个线程的操做顺序
image.png

假如说线程A在作了i+1,但未赋值的时候,线程B就开始读取i,那么当线程A赋值i=1,并回写到主内存,而此时线程B已经再也不须要i的值了,而是直接交给处理器去作+1的操做,因而当线程B执行完并回写到主内存,i的值仍然是1,而不是预期的2。也就是说,volatile缩短了普通变量在不一样线程之间执行的时间差,但仍然存有漏洞,依然不能保证原子性。

七.java的可见性、有序性、原子性

参考文章 : java的可见性、有序性和原子性

八.什么是CAS 乐观锁

参考资料: Java:CAS(乐观锁)

sychronized 实现同步的问题

在JDK 5以前Java语言是靠synchronized关键字保证同步的,该机制存在如下问题:

(1)在多线程竞争下,加锁、释放锁会致使比较多的上下文切换和调度延时,引发性能问题

(2)一个线程持有锁会致使其它全部须要此锁的线程挂起

(3)若是一个优先级高的线程等待一个优先级低的线程释放锁会致使优先级倒置,引发性能风险。

悲观锁和乐观锁

独占锁是一种悲观锁,synchronized就是一种独占锁,会致使其它全部须要锁的线程挂起,等待持有锁的线程释放锁。
而另外一个更加有效的锁就是乐观锁。
所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止
乐观锁用到的机制就是CAS,Compare and Swap。

CAS 原理:

CAS机制当中使用了3个基本操做数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改成B。

CAS存在的问题(了解,说个大概)
  1. ABA问题。由于CAS须要在操做值的时候检查下值有没有发生变化,若是没有发生变化则更新,可是若是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,可是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法做用是首先检查当前引用是否等于预期引用,而且当前标志是否等于预期标志,若是所有相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
  1. 循环时间长开销大自旋CAS若是长时间不成功,会给CPU带来很是大的执行开销。 当须要修改共享变量的线程增多时,状况会更为严重;
  2. 只能保证一个共享变量的原子操做。当对一个共享变量执行操做时,咱们可使用循环CAS的方式来保证原子操做,可是对多个共享变量操做时,循环CAS就没法保证操做的原子性,这个时候就能够用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操做。好比有两个共享变量i=2,j=a,合并一下ij=2a,而后用CAS来操做ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你能够把多个变量放在一个对象里来进行CAS操做。

九.内存模型相关

说说jvm内存模型

参加我的文章 java内存模型

垃圾回收的算法,各自优缺点

同上

Java 中的对象必定在堆上分配吗?
  • 栈上分配

JVM在Server模式下的逃逸分析能够分析出某个对象是否永远只在某个方法、线程的范围内,并无“逃逸”出这个范围,逃逸分析的一个结果就是对于某些未逃逸对象能够直接在栈上分配,因为该对象必定是局部的,因此栈上分配不会有问题。


其余java题目

一.java 多态的原理

假设有一个Person类,以及两个子类Boy和Girl
当这三个类加载到虚拟机后,方法区就包含了这三个类的信息,包括各自的方法表
Girl 和 Boy 在方法区中的方法表可表示以下:

方法表中的条目指向的具体的方法地址
如 Girl 继承自 Object 的方法中,只有 toString() 指向本身的实现(Girl 的方法代码,被重写),其他皆指向 Object 的方法代码;其继承自于 Person 的方法 eat() [未重写]和 speak() [被重写]分别指向 Person 的方法实现和自己的实现。

那么父类方法表和子类方法的方法表有什么关系呢?

若是子类改写了父类的方法,那么子类和父类的那些同名的方法仍然共享一个方法表项。(能够理解为这个方法在方法表里的叫法/直接引用仍是不变的)
所以,方法在方法表中的偏移量老是固定的,全部继承父类的子类的方法表中,其父类所定义的方法的偏移量也老是一个定值。(能够理解为即便被重写,子类还能够经过和父类的一样直接引用找到该方法)
这样 JVM 在调用实例方法其实只须要指定调用方法表中的第几个方法便可

知道了方法表是怎样,那怎么经过方法表调用方法的过程是怎样呢?

假设代码以下:

class Party {
    void happyHour() {
        Person girl = new Girl();
        girl.speak();
    }
}

(1)在常量池中找到方法调用的符号引用
(2)查看Person的方法表,获得speak方法在该方法表的偏移量(假设为15),这样就获得该方法的直接引用。 
(3)根据this指针获得具体的对象(即 girl 所指向的位于堆中的对象)。
(4)根据对象获得该对象对应的方法表根据偏移量15查看有无重写(override)该方法,若是重写,则能够直接调用(Girl的方法表的speak项指向自身的方法而非父类);若是没有重写,则须要拿到按照继承关系从下往上的基类(这里是Person类)的方法表,一样按照这个偏移量15查看有无该方法。

类的加载过程?

下面代码的执行结果是?

class Person{
    {
        System.out.println("P1");
    }
    static {
        System.out.println("P2");
    }
    public Person(){
        System.out.println("P3");
    }
}
class Student extends Person{
    static {
        System.out.println("S1");
    }

    {
        System.out.println("S2");
    }

    public Student(){
        System.out.println("S3");
    }
}

public class Main {

    public static void main(String[] args) {
        Student s = new Student();
    }
}

答案:P2,S1,P1,P3,S2,S3
参考 java中类加载与静态变量、静态方法与静态代码块详解与初始化顺序

相关文章
相关标签/搜索