java面试题整理(一)基础部分

Q:hashCode相同的两个对象一定相同吗?equals呢?

A:都不一定,hashCode和equals都是可以随便重写的,但是JDK规定,equals相同hashCode一定相同,反之不一定,hashCode默认实现是对象在内存当中的地址。equals默认使用的是 ==

Q:Hashtable、HashMap底层实现是什么?Hashtable和ConcurrentHashMap底层实现的区别?

A:HashMap底层实现是通过数组加链表的方式,每个添加进来的元素,通过hash算法计算出存储在数组当中的索引位置,如果当前位置上已经有元素就追加到当前索引上 链表的末端,HashTable在hashMap每个方法上面都添加了synchronized关键字,访问hashTable必须要竞争同一把锁,不接受key值为null, 非failFast。ConcurrentHashMap在jdk1.8的实现是在每次添加元素时,计算元素hash值,算出元素在数组当中的位置,如果索引位置上没有元素,直接将添加的元素挡在索引位置,如果已经有元素,使用synchronized锁定当前位置上的链表,加新添加的元素追加至链表末尾,如果超过设定长度,将单向链表转为红黑树。

ConcurrentSkipListMap代替TreeMap,每个节点不单单包含下一个节点的指针,可能包含很多个指向后续节点的指针,这样就可以跳过一些不必要的节点,从而加快查找,删除的速度。对于每个链表中包含多少个指向后续节点的指针,后续节点的个数,是通过随机函数生成器得到的。线性结构中,向有序链表中插入一个节点需要O(n)的时间,查询操作需要O(n)的时间。而跳跃表将查找时间降低为O(n/2),先大幅度查询范围,再小范围查找,ConcurrentSkipListMap底层是通过死循环+CAS操作来保证线程安全,

Q:HashMap和TreeMap的区别是什么?

A:HashMap对数据的存储是通过hash(key)&(length-1),length必须为2的n次幂,来计算元素存储索引,是无序的,key值可以为null,并且线程不安全的,当两个线程同时添加元素,hashMap需要扩容时会发生死锁,

TreeMap:实现了sortMap接口,基于红黑二叉实现,在添加元素时,会根据key值进行升序进行排序,当然也可以通过自定义比较器进行排序,因此在遍历treeMap时,其实是已经排序过得。

Q:线程池都有什么参数,底层是如何实现的?

A:ThreadPoolExecutor当中的参数,corePoolSize:核心线程数,maxPoolSize:最大线程数,keepAliveTime:线程失效时间,TimeUnit:失效时间单位,handler:拒绝策略(丢弃抛出异常,直接丢弃,抛出队列中最早提交的线程,自定义)

java当中提供了有四种线程池,固定数量的线程池,只有一个线程的线程池(thread因为异常结束时,会重新创建一个线程),CachedThreadPool :使用的是没有队列长度的synchronousQueue阻塞队列,corePoolSize为0,失效时间为60秒,如果一个任务提交过来,会offer到队列当中,这个时候刚好有空闲线程的话,就会把offer进去的任务取出来执行,如果一开始没有任务,就创建一个线程去执行。ScheduledThreadPool:具有时间调度特性的线程池,可以根据设定的频率定时执行,使用的DelayedWorkQueue队列。

底层原理:如果初始化一个线程池,一开始没有线程的情况下,会先去创建线程执行任务,当执行任务的线程数量超过corePoolSize话,会将新提交过来的任务放在队列当中,这时候,如果核心线程池当中有线程执行完了任务,就会从队列中获取任务执行,假如核心池当中线程都在执行任务,同时又有线程过来,而且存放线程的队列已经满了,判断下当前线程的数量和最大线程数量的关系,如果此时核心池当中的线程数量还没超过最大线程数量,那么新提交过来的任务就会再创建线程来执行。直到线程数量达到了最大线程数量。可是这个时候还有人任务提交过来,需要处理,核心线程池也满了,队列也满了,线程数量也达到了最大线程数量,这个时候就会执行决绝策略,丢掉掉一些任务。当线程池当中的线程把任务都执行差不多了,会有一部分线程就空闲下来了,这时候,判断这个线程是不是大于核心线程数量,如果超过核心线程数量的那部分空闲线程,他们空闲的时间超过了设定的空闲时间,就需要把这些线程给停掉了。

设置合理的线程数方式:N核服务器,通过执行业务的单线程分析出本地计算时间x, 等待时间为y, 工作线程数设置为n*(x+y)/x 比较合理。当然对于非CPU密集型业务,工作瓶颈主要是在数据库,本地计算时间很少,因此设定几十个和上百个线程也是可以的。

Q:synchronized和lock的区别是什么?synchronized什么情况下是对象锁,什么情况下是全局锁,为什么?

A:synchronized 是jvm执行的,悲观锁,如果有异常的话,会自动释放到锁,synchronized修改的代码块中的变量是从主内存当中获取,即使没有被volatile修饰,修改完之后也会把数据写会主内存,锁的获取和释放时通过monitorenter和monitorexist指令,synchronized的锁在实现上有偏向锁,轻量级锁和重量级锁,偏向锁是在jdk1.6以后加入的,轻量级锁在多线程竞争情况下会升级为重量级锁。关于锁的信息都保存在对象头中。lock的实现方式是java代码,通过AQS+CAS更新状态实现,对于锁的获取和释放都是手动的,condition.await和condition.signl实现线程间的通信.

synchronized在非静态方法,和实例对象上,属于对象锁,在静态方法和类对象上,属于全局锁。

Q:ThreadLocal如何使用的?说出在项目中的例子?底层实现是什么?

A: 通过ThreadLocal保存的数据最终是保存在Thead当中的ThreadLocalMap当中,key值就是ThreadLocal,value是我们通过ThreadLocal保存在线程中的本地变量。我们在调用ThreadLocal的set方法时,是将ThreadLocal作为key, value就是threadLocalMap中的value, 这样存储的好处就是当线程消亡时,ThreadLocal也会随着销毁掉。

关于内存泄漏:ThreadLocalMap当中存储的是key是ThreadLocal的弱引用,value是强引用,但是我们在调用ThreadLocalMap的remove或者get方法时会将key为null的对象移除掉,这样就会防止内存泄漏,如果长时间不调用get或者remove方法,只有当线程消亡时,value对象才会被gc掉,尤其需要注意的是,线程池当中对ThreadLocal的使用

项目当中:web项目中,每个用户请求过来,为每个用户保存他的基本信息在ThreadLocal当中,这样避免dao层,service层以及controller层之间参数传递,直接从threadlocal中获取即可。

Q:volatile的工作原理是什么?

A:volatile的作用是用来保证内存可见性,比如一个线程A对变量做得修改会刷新到主存中去,线程B在获取变量时,会直接从主存当中获取,而不是使用当前线程缓存中的变量。

主要实现方式:对volatile变量写操作时,会在写操作后面添加一个store屏障指令,将写入之后的数据从本地内存中刷新到主存中去。对volatile的读操作,会在每个读操作之前添加一个load屏障指令,从主内存中获取共享变量。

volatile写指令顺序:普通写-----》StoreStore屏障----》volatile写----》StoreLoad屏障-----》普通读写

                            StoreStore屏障禁止了上面的普通写和volatile写指令重排序,StoreLoad屏障禁止了volatile写和下面的普通                             读写执行重排序

volatile读执行令: volatile读-----》LoadLoad屏障----》LoadStore屏障-----》普通读写

                           LoadLoad屏障禁止了下面所有的普通读操作和volatile读重排序,LoadStore禁止下面的普通写和volatile写                              指令重排序

系统层面实现:在多处理器下,保证各个处理器的缓存是一致的,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行的内存地址被修改,就会将当前处理器的缓存行设置成无效。

应用场景:1、多线程之间状态标记 2、单例双重检查锁定 3、定期观察成员变量状态

Q:CAS是什么,如何实现的呢?

A: CAS需要三个参数,内存地址V,旧的预期值A,新的值B,当前仅当内存旧的预期值A和内存地址V中的值相同时,修改为B,否则什么都不做

CAS的缺点:1、CPU开销大,一般在使用CAS操作的时候,都是在一个循环当中,一直尝试去更新,直到更新成功,当并发量很大的情况下,一直更新不成功,又一直循环更新,会给CPU带来很大的压力

2、只能保证某个变量的原子性操作,不能保证整个代码块的原子性

3、ABA问题,解决办法使用AtomicStampedReference

Q:写出单例模式(至少4种)

A:饿汉模式

public class SingletonTest1 {
    private SingletonTest1() {

    }
    private static final SingletonTest1 singletonTest1= new SingletonTest1();

    public static SingletonTest1 getInstance() {
        return singletonTest1;
    }
}

优点:实现起来比较简单,而且不存在多线程同步问题,避免了synchronized所造成的性能问题

缺点:当类SingletonTest1被加载的时候,会初始化singletonTest1,静态变量被创建并分配内存,从这以后,这个static变量的对象一直占用着这段内存(可能你并用没有使用这个实例),当类被卸载时,静态变量被摧毁,并释放所占用的内存,因此在特定条件下会耗费内存。

饱汉模式(非线程安全)

public class SingletonTest2 {
    private SingletonTest2(){

    }
    private static SingletonTest2 instance;
    public static SingletonTest2 getInstance() {
        if(instance == null)
            instance = new SingletonTest2();
        return instance;
    }
}

优点:写起来比较简单,当类被加载的时候,并没有去创建分配内存空间,当getInstance方法被调用的时候,初始化instance变量,并分配内存,在某些特定条件下可以节约空间

缺点:并发环境下可能会创建多个SingletonTest2实例

饱汉式(线程安全)

public class SingletonTest3 {
    private SingletonTest3(){

    }
    private static SingletonTest3 instance;
    public static synchronized SingletonTest3 getInstance() {
        if(instance == null)
            instance = new SingletonTest3();
        return instance;
    }
}

优点:使用synchronized关键字,避免了创建多个SingletonTest3实例问题

缺点:每次获取实例对象时,都需要进行加锁,当并发访问量高时,性能有所下降

第四种方式:

public class SingletonTest4 {
    private SingletonTest4(){

    }
    private static volatile SingletonTest4 instance;
    public static SingletonTest4 getInstance(){
        if(instance == null) {
            synchronized (SingletonTest4.class) {
                if(instance == null)
                    instance = new SingletonTest4();
            }
        }
        return instance;
    }
}

性能安全并且效率高,volatile保证了其他线程的可见性。

还有其他的实现方式,比如使用lock方式添加锁

Q:AQS(AbstractQueuedSynchronized)实现方式

A:其内部维护了一个volatile int state 的变量和一个FIFO队列,通过CAS方式修改state的值,修改成功,并且满足指定条件的,获取到锁,如果修改失败,或者说state已经处于加锁状态,就会将线程封装成一个对象,放在队列里面,并等待被唤醒。挂起线程使用的LockSupport.park方式,唤醒使用的是LockSupport.unpark(Thread)方式。

Q:java线程状态

A: new --->start---->runnable---->分配cpu----->running----->waiting----->blocked----->dead

Thread.sleep(mils):调用sleep方法事,线程将会阻塞,但是并不释放掉当前所持有的锁,会让出CPU,休眠一段时间之后,进入可运行状态。

thread.join()和thread.join(mils)调用这个方法的线程会处于阻塞状态,直到thread将其run方法执行结束之后,当前线程才会继续执行,当前调用线程并不会释放掉锁。

obj.wait() 和obj.wait(mils) wait方法会释放掉当前线程所持有的锁,并进入到对象的等待池中,(每个对象都有两个池,锁池和等待池),需要依靠对象的notify/notifyAll方法才可以被唤醒继续执行,wait方法的调用必须在synchronized代码块中,

obj.notify()和obj.notifyAll方法,调用这两个方法必须在synchronzied代码块中,随机唤醒一个/所有等待在该对象上的线程,进入到就绪队列中,等待cpu调度,notify唤醒的是,调用过wait之后的线程,如果先调用了notify,再调用wait,那么wait等待的线程将不会被唤醒。

Q:简述synchronized的monitor机制

A:synchronized代码块使用了monitorenter和monitorexit指令实现,synchronized修饰方法,会在方法修饰符上加上ACC_SYNCHRONIZED实现。无论是哪种方式,都是对指定对象的monitor的获取,这个过程是互斥的,同一时间只能有一个线程获取成功,其他获取失败的线程会在对象的锁池当中等待。

Q:简述happens-before原则

A:编译器或运行时为了效率可以在允许的时候对指令进行重排序,这就会引起线程之间可见性问题,通过制定一些列的规则规定某些场景某个线程修改的变量对另一线程可见,happens-before不是描述实际操作的先后顺序,而是用来描述可见性的一个原则,如果线程A 解锁了a,线程B锁定了a,那么线程A解锁之前的所有操作对线程B都是可见的。

Q:java怎么做序列化,java序列化时,编码如何设置?

A:序列化:将java对象转化为字节序列的过程。反序列化:将字节序列转化为java对象的过程。序列化的用途:1、将对象的字节永久的保存在磁盘上,2、在网络传输时需要对对象序列化。

java实现序列化方式:1、实现Serializable接口,只是一个可序列化的标志,并没有包含实际的属性和方法

                                 2、如果不在方法当中添加readObject和writeObject方法,则采用默认的序列化机制。如果添加了这两个方法,还想使用默认的机制,则在这两个方法中分表调用defaultReadObject和defaultWriteObject方法。

                                3、为了保证安全性,可以使用transient关键字进行修饰不需要序列化的属性,因为在反序列化时,private修饰的属性也能被查看到。

注意事项:

        被static修饰的属性不会被序列化,

        对象的类名,方法名都会被序列化,方法不会被序列化,

        要保证序列化对象所在类的属性是可以被序列化的

        通过网络、文件进行序列化时,必须按照写入的顺序读取对象

        反序列化时必须有序列化对象的class文件

        被transient的字段不会被序列化。在反序列化之后,transient变量的值被设置为初始值。


Q:java对象的生命周期

A:创建----》使用(有强引用指向的对象)----》不可见----》不可达-----》回收-----》终结-----》对象空间再分配

Q:为什么wait方法一般建议放在while循环当中?

A:如果使用if条件判断,当线程从wait中唤醒之后,会沿着代码逻辑往下执行,但是可能有一种情况就是,当前唤醒时,并不满足条件,但是仍然沿着代码逻辑往下执行,这就会造成错误,如果放在while循环当中,当线程被唤醒之后,会再次判断是否满足天机,如果满足条件,再继续向下执行。

Q:概述对象在内存当中初始化的过程

A:要初始化一个对象,首先加载这个对象所对应的class文件,该文件的数据会被加载到永久代,并创建一个底层的instanceKlass对象代表该class,再为将要初始化的对象分配内存空间,优先在线程私有内存空间中分配大小,如果空间不足,再到eden中进行内存分配。

Q:Object类的finalize方法的实现原理

A:新建一个对象时,在jvm中会判断对象对应的类是否重写了finalize方法,并且finalize方法体不为空,则把这个对象封装成Finalizer对象,并添加到Finalizer链表。Finalizer类中会初始化一个FinalizerThread类型的线程,负责从一个引用队列中获取Finalizer对象,并执行该Finalizer对象的runFinalizer方法,最终会执行原始对象当中finalize方法。

Q:为什么wait、notify和notifyAll方法不是在Thread类当中

A: java当中提供的锁是对象级锁而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象当中的wait方法就是有意义的。如果wait方法定义在Thread类当中,线程在等待的是哪个对象锁就不明显了。

Q:static和final的区别和用途

A: static :

    修饰变量:静态变量随着类加载时完成初始化,内存当中只有一个,且JVM只会为其分配一次内存,所有的类共享静态变量

    修饰方法:在类加载时存在,不依赖于任何实例,static修饰的方法,必须实现,而且不能使用abstract修饰

    修饰代码块:在类加载完成之后就会执行代码块中的内容

    父类静态代码块--》子类静态代码块---》父类非静态代码块--》父类构造方法---》子类非静态代码块---》子类构造方法

 final:

    修饰变量:

        编译时期常量:类加载的过程中完成初始化,编译后带入到任何计算中,只能是基本类型

        运行时常量:基本数据类型或者引用数据类型。引用不可变,但是引用的对象内容可变

    修饰方法:不能被继承,不能被子类修改

    修饰类:不能被继承

    修饰形参:final形参不可变

Q:对String,StringBuiler,StringBuffer以及String不变性的理解

A:都是final类型,不能被继承,String是长度不可变得,StringBuilder和StringBuffer是可变的,StringBuffer是线程安全的,StringBuilder不是线程安全的,

String使用加号拼装字符串变量其实是调用了StringBuilder的append方法,最终toString转化为String

String类new的方式创建一个变量,可能会创建两个对象,如果字符串常量池中不包含创建的字符串,那么会在常量池中创建一个字符串,如果字符串常量池当中已经存在了就不创建,同时会在堆上创建一个字符串对象

Q:String是否重写了Object当中的toString和hashCode方法?如果只重写equals方法不重写hashCode方法会出现什么问题?

A:String重写了toString和hashCode方法;当equals方法被重写时,有必要重写hashCode方法,以维护hashCode方法的常规协定,这个协定规定对相等的两个对象必须有相同的hashCode

重写equals方法不重写hashCode方法会有什么问题,在存储散列对象,如hashSet,hashMap等,如果两个对象的equals方法相同,而hashCode方法不同,则在集合当中会存储两个值相同的对象,比较容易混淆,因此在重写equals方法时,必须重写hashCode方法。

Q:java当中NIO,BIO,AIO分别是什么?

A:BIO 同步阻塞,服务器实现方式是一个连接一个线程,即客户端有连接请求时服务器启动一个线程进行处理,如果这个线程不做任何处理会造成不必要的开销,当然可以通过线程池进行优化

适用于连接数比较小的请求,这种对服务器资源要求比较高。

NIO:同步非阻塞,服务器实现模式是一个连接对应一个线程,客户端发送的连接请求都会注册在多路复用器上面 ,多路复用器,selector一直处于轮询状态,当连接有I/O请求时才可以启动一个线程进行处理

NIO适用于连接比较多,且比较短的架构,比如聊天服务器,

AIO:异步非阻塞,服务器实现模式是一个有效请求一个线程,客户端的I/O请求都是由操作系统先处理完成之后,再通知服务器应用去启动线程进行处理。

适用于连接比较多,且连接比较长的架构,充分调用os进行并发编程操作。

Q:java当中的clone方法是深拷贝还是浅拷贝?

A:关于java当中的深拷贝和浅拷贝,浅拷贝指的是,被拷贝的对象的所有变量都含有和原来的对象相同的值,而所有的其他对象的引用仍然指向原来的对象,换言之,浅拷贝只拷贝需要拷贝的对象,不拷贝所包含的引用对象。

深拷贝:被拷贝对象的所有变量都含有与原来的对象相同的值,那些引用其他对象的变量将指向被复制过来的新对象,而不是原来对象锁指向的引用对象。也就是说,深拷贝是把对象当中的所有变量都拷贝了一份过来。

clone方法是浅拷贝,在java对象当中如果要实现clone方法,必须调用父类的super.clone方法,此方法声明必须是public的,在派生类当中实现Cloneable接口,这个接口没有方法体,在运行时,Object类当中的clone方法会标识出你要复制的是那个对象,然后为这个对象分配内存空间,并进行对象的复制,

如果想要实现深拷贝,有两个方法,1、重写clone方法,super.clone把基础类型变量拷贝一份,然后再手动拷贝一份引用类型的变量进行保存 2、需要拷贝的对象实现Serializable接口,然后再把对象写到流里面,再从流里面读出来,就实现了深拷贝,这安阳做得前提是所有引用到的对象都是可串行化的,否则就需要考虑那些不可串行化的对象可否设置成transient,从而将其排除在复制过程之外,序列化操作不仅可以序列化当前对象,也会序列化这个对象当中的引用类型,从而实现深拷贝。

Q:JAVA当中实现多态的机制是什么?

A: 父类或者接口定义的引用变量可以指向子类或具体实现类的实例对象,而程序调用的方法在运行期才动态绑定,就是引用变量所指向具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量类型中定义的方法。

Q:简述一下fork/join框架的实现,以及开发中如何使用?

A:Fork/Join框架是一个用于并行执行任务的框架,把一个大任务分割成若干个小任务,最终汇总每个小任务结果然后的到大任务的框架,底层通过使用工作窃取算法来实现,我们将任务分解成一个个小任务之后,将这些任务放在不同的队列中去,并为每个队列创建一个线程来执行对列当中的任务,但是有时候线程会先把自己队列的任务执行完成,而其他的线程对应的队列还有任务等待处理,这个时候空闲的线程就会去其他线程的队列当中窃取一个任务来执行,为了减少窃取任务的线程和被窃取线程之间的冲突,会使用双端队列,被窃取任务线程从队列头部拿任务执行,窃取任务的队列从尾部拿任务执行。

关于如何使用,创建一个ForkJoinTask,用来执行fork和join机制,主要是实现compute方法,实际开发当中实现RecursiveTask和RecursiveAction任何一个子类就可以,同时需要一个ForkJoinPool 来执行ForkJoinTask 的任务。

Q: try-catch-finally当中,如果try当中有return,catch当中也有return, finally当中也有return 会返回哪个结果?

A:  1、当finally中有return语句时,无论是否发生异常,都以finally当中的return为准,其他的return都不起作用

     2、当发生异常时,try当中有return, catch当中有return, 以catch当中的return 为准,finally当中的修改不起作用

     3、当发生异常时,try当中有return, catch当中有return, 方法最后有return, 以方法最后的return为准,catch和finally当中对变量的修改都起作用

     4、当不发生异常时,try当中有return,catch 、finally 以及方法最后不管有没有return, 都以try当中的return为准,catch,finally当中的修改不起作用。

Q:线程和进程的概念,并发和并行的概念

Q:Exchanger原理

未完~

如有错误,欢迎指正~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值