Java面试宝典 -基础篇

泛型中extends和super的区别

1.<?extends T>表示包括T在内的任何T的子类

2.<?super T>表示包括T在内的任何T的父类

Java反射机制

反射机制:是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意属性和方法;这种动态获取信息以及动态调用对象的方法的功能称为Java语言的反射机制

他的工作原理:当一个字节码文件加载到内存的时候,jvm会对该字节码进行解析,然后创建一个对象的class对象,jvm把字节码文件的信息全部都存储到该CLASS对象中,我们只要获取到class对象,我们就可以使用该对象设置对象的属性或者调用对象的方法等操作

获取Class类的三种方式

  • 类名.Class属性

  • 对象名.getClass()方法

  • Class.forName(全类名)方法

这三种方式分别在程序不用阶段调用

反射的应用场景

  • 使用JDBC时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序 ;

  • 多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;

  • 面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现

==和equals的区别

1.Object 类中的equals源代码

Public boolean equals (Objectobj){

return (this==obj);

}

这个方法是Object类的默认实现

2.sun公司设计equals方法的目的是什么?

equals方法是比较两个对象的内容是否相等

3.==的目的是什么?

==比较的是变量内存中存放的对象的内存地址,用来判断两个对象的地址是否相同,即是否是指同一个对象

4.基本数据类型

byte short char int long float double boolean

基本类型之间比较用==,因为他们比较的是值

5.引用数据类型

接口、类、数组、String(String属于引用类型,因为String是一个类)

当他们用(==)来比较时,比较的是他们在内存中的存放地址,除非是同一个new出来的对象,他们比较后的结果是true,否则比较后的结果是false。因为每new一次,就会重新开辟一个新的堆内存空间

String、StringBuffer、StringBuilder

String是final修饰的,不可变,每次操作都会产生新的对象

StringBuffer和StringBuilder都是在原对象上操作的

StringBuffer是线程安全的,StringBuilder线程不安全

StringBuffer的方法都是被synchronized修饰

性能:StringBuilder>StringBuffer>String

Arraylist和LinkedList的区别

Arraylist:基于动态数组,连续内存存储,适合下标(随机访问)

扩容机制:因为数组长度是固定的,超出长度存数据时,需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能

LinkedList:基于链表,可以存放在分散的内存中,适合做数据的插入以及删除操作,不适合查询,需要逐一遍历,遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需要对list重新进行遍历,性能消耗大

另外 不要试图使用indexof等返回元素的索引,并利用其进行遍历,使用indexof对list遍历,当结果为空时,会遍历整个列表

hashMap和hashTable的区别

1.继承父类不同、但是都实现了Map接口;hashMap继承AbstractMap、hashTable继承自Dictionary

2.HashTable是同步的(方法中使用了synchronize)线程安全

3.hashMap线程不安全

4.HashMap可以允许null值,hashTable 都不允许出现null

5.遍历方式内部实现不同(hashTable使用的是Enumeration、HashMap使用的是Iterator)

6.初始化和扩容不同

说一下hashMap的put方法

先说hashMap的Put方法的大体流程:

1.根据key通过哈希算法与与运算得出数组下标

int hash = hash(key.hashCode());          
int i = indexFor(hash, table.length);

2.如果下标位置元素为空,则将key和value封装成entry对象(jdk1.7中是entry对象,jdk1.8是Node对象)并放入该位置

3.如果数组下标元素不为空,则要分情况讨论

a.如果是jdk1.7则先判断是否需要扩容,如果要扩容就进行扩容,如果不需要扩容就生产entry对象,并使用头插法添加到当前位置的链表中

b.如果是jdk1.8,则会先判断当前位置上的Node的类型,看是红黑树Node还是链表node

如果是红黑树node,则将key和value封装成一个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value
如果此位置上的node对象是链表节点,则将key和value封装为一个链表node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链 表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value值,当遍历完链表后,将新链表node插入到链表中,插入到链表后,会看当前链 表的节点个数,如果超过了8,那么则会将该链表转成红黑树
将key和value封装为node插入到链表或者红黑树后,再判断是否需要进行扩容,如果需要就扩容,不需要就结束put方法 

线程的生命周期、线程状态

线程五种状态:新建、就绪、运行、阻塞、死亡

新建:创建一个线程对象

就绪:处于新建状态的线程,执行了start方法后,该线程进入线程队列等待CPU时间片,此时已经具备了运行的条件,只是没有获取到CPU资源

运行:当就绪的线程被调度并获得CPU的资源,该线程便进入运行状态

阻塞:运行的线程因为某种原因,放弃了CPU的使用权,暂时停止执行。直到线程进行就绪状态,才有机会转到运行状态

死亡:线程执行完毕或异常退出

sleep()、wait()、join()、yield()区别

sleep 方法 是属于 Thread 类中的,sleep 过程中线程不会释放锁,只会阻塞线程,让出cpu给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态,可中断,sleep 给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会

wait 方法 是属于 Object 类中的,wait 过程中线程会释放对象锁,只有当其他线程调用 notify 才能唤醒此线程。wait 使用时必须先获取对象锁,即必须在 synchronized 修饰的代码块中使用,那么相应的 notify 方法同样必须在 synchronized 修饰的代码块中使用,如果没有在synchronized 修饰的代码块中使用时运行时会抛出IllegalMonitorStateException的异常

yield 方法 和 sleep 一样都是 Thread 类的方法,都是暂停当前正在执行的线程对象,不会释放资源锁,和 sleep 不同的是 yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。还有一点和 sleep 不同的是 yield 方法只能使同优先级或更高优先级的线程有执行的机会

join 方法 等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。例如:主线程创建并启动了子线程,如果子线程中药进行大量耗时运算计算某个数据值,而主线程要取得这个数据值才能运行,这时就要用到join 方法了

Java线程死锁如何避免

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象

产生死锁的原因

1.互斥条件:系统要求对所分配的资源进行排他性控制,即在一段时间内某个资源仅为一个进程所占有(比如:打印机,同一时间只能一个人打印)。此时若有其他进程请求该资源,则请求只能等待,直到有资源释放了位置

2.请求和保持条件:进程已经持有了一个资源,但是又要访问一个新的被其他进程占用的资源那么就会阻塞,并且对自己占用的一个资源保持不放

3.不剥夺条件:进程对已经获取的资源未使用完之前不能被剥夺,只能使用完之后自己释放。

4.环路等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。

如何避免死锁

要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,而在操作系统中,互斥条件和不可剥夺条件是系统规定的,这也没办法人为更改,而且这两个条件很明显是一个标准的程序应该所具备的特性。所以目前只有请求并持有和环路等待条件是可以被破坏的

1.保持加锁顺序。所有的线程都是按照相同的顺序获得锁

2.获取锁添加时限。基于获取锁无线等待的情况,如果我们在获取锁的时候进行限时等待,例如wait(1000)或者使用ReentrantLock的tryLock(1,TimeUntil.SECONDS)这样在指定时间内获取锁失败就不等待;

ThreadLocal

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对于其他线程而言是隔离的,该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了副本,每个线程可以访问自己内部的副本变量。

ThreadLocal提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。

ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有ThreadLocal 相对的实例副本都可被回收

ThreadLocal内存泄露问题

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,value无法永远回收,造成内存泄露

ThreadLocal与Synchronized的区别

ThreadLocal<T>其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问

1.Synchronized用于线程间的数据共享,ThreadLocal用于线程间的数据隔离

2.Synchronized是利用锁的机制,使变量和代码块在某一时刻只能被该线程访问。ThreadLocal为每一个线程提供了变量副本,使每一个线程在某一时间访问的并不是同一个对象,隔离了多个线程对数据的共享。

Synchronized修饰普通方法和静态方法的区别

Synchronized修饰普通方法:实际上是对调用该方法的对象加锁,俗称“对象锁

情况1:同一个对象在两个线程中分别访问该对象的两个同步方法
结果:会产生互斥
原因:因为锁针对的是对象,当对象调用一个synchronized方法时,其他同步方法需要等待其执行结束并释放锁才能执行
情况2:不同的对象在两个线程中调用同一个同步方法
结果:不会产生互斥
原因:锁针对的是对象,不是方法,可以并行执行,不会互斥。

Synchronized修饰静态方法:实际上是对该类对象加锁,俗称”类锁

情况1:用类直接在两个线程中调用两个不同的同步方法
结果:会产生互斥
原因:对静态对象加锁实际上是对类(.class)加锁,类对象只有一个,因此同步方法之间一定会产生互斥
情况2:用一个类的静态对象在两个线程中调用静态方法和非静态方法
结果:会产生互斥
原因:因为是一个对象调用,同上。都调用静态方法的时候,相当于是同一个类锁,用的都是同一个类对象。
都调用非静态方法的时候,相当于是同一个对象锁。
情况3:一个对象在两个线程中分别调用一个静态同步方法和一个非静态同步方法
结果:不会产生互斥
原因:虽然是一个对象调用,但是两个方法的锁不同,调用静态方法实际上是类对象在调用,即这两个方法产生的并不是同一个对象锁,因此不会互斥,会并发执行

线程池架构

Executors

静态工厂类,它通过静态工厂方法返回ExecutorServiceScheduledExecutorService等线程池示例对象

Executor

Executor提供了void execute(Runnablecommand);接口来执行已提交的Runnable执行目标实例

ExecutorService

继承Executor,对外提供异步任务的接收服务

submit向线程池提交单个异步任务

invokeAll向线程池提交批量异步任务

AbstractExecutorService

抽象类,实现了ExecutorService

ThreadPoolExecutor

线程池核心实现类,继承于AbstractExecutorService

ScheduledExecutorService

继承于ExecutorService。他是一个可以完成“延时”和“周期性”任务的调度线程池接口

ScheduledThreadPoolExecutor

继承于ThreadPoolExecutor,实现了ScheduleExecutorService中延时执行和周期执行等抽象方法

Executors创建线程的方法

1.newSingleThreadExecutor创建“单线程化线程池”

public static ExecutorService newSingleThreadExecutor(){
    return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}  

2.newFixedThreadPool创建"固定数量的线程池"

    public static ExecutorService newFixedThreadPool(int  nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L,  TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

3.newCachedThreadPool创建"可缓冲线程数"

public static ExecutorService newCachedThreadPool(){
    return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,newSynchronousQueue<Runnable>());
}

4.newScheduledThreadPool创建“可调度线程池”

线程池的任务调度流程

任务阻塞队列

常见的几种阻塞队列的实现:

ArrayBlockingQueue:是一个数组实现的有界阻塞队列(有界队列),队列中的元素按FIFO排序,ArrayBlockingQueue在创建时必须设置大小

LinkedBlockingQueue:是一个基于链表实现的阻塞队列,按FIFO排序任务,可以设置容量(有界队列),不设置容量则默认使用Integer.Max_VALUE作为容量(无界队列)

PriorityBlockingQueue:是具有优先级的无界队列

线程池中阻塞队列的作用是什么,为什么是先添加队列而不是先创建最大线程数

1.一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。 阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放CPU资源 阻塞队列自带阻塞和唤醒功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存货、不至于一直占用CPU资源

2.在创建新线程的时候,是要获取全局锁的,这个时候其他的就的阻塞,影响了整体效率。

线程池的拒绝策略

AbortPolicy:拒绝策略

新任务就会被拒绝,并且抛出RejectedExecutionException异常。该策略是线程池默认的拒绝策略

DiscardPolicy:抛弃策略

新任务就会直接被丢掉,并且不会有任何异常抛出

DiscardOldestPolicy:抛弃最老任务策略

将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列(一般队头元素最老)

CallerRunsPolicy:调用者执行策略

新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务

自定义策略

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值