java面试题

1. 操作系统

1.1 进程和线程

线程:是程序执行的最小单位,共享线程的数据空间
进程:是系统资源分配的最小单位,拥有独立的数据空间

2.java

2.1 java基础

2.1.1、为什么重写equals还要重写hashcode?

HashMap中,如果要比较key是否相等,要同时使用这两个函数!因为自定义的类的hashcode()方法继承于0bject类,其hashcode码为默认的内存地址,这样即便有相同含义的两个对象,比较也是不相等的。HashMap中的比较key是这样的,先求出key的hashcode(),比较其值是否相等,若相等再比较equals(),若相等则认为他们是相等的。若equals()不相等则认为他们不相等
HashMap用来判断key是否相等的方法,其实是调用了HashSet判断加入元素是否相等。重载hashCode()是为了对同一个key,能得到相同的HashCode,这样HashMap就可以定位到我们指定的key上。重载equals()是为了向HashMap表明当前对象和key.上所保存的对象是相等的,这样我们才真正地获得了这个key所对应的这个键值对。

2.1.2、说一下map的分类和常见的情况

它有四个实现类,分别是HashMap、Hashtable、 LinkedHashMap和TreeMap.

Hashmap是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的
值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。HashMap 最多只允许一条
记录的键为Null;允许多条记录的值为Null;HashMap 不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用Collections 的!synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap.

Hashtable与HashMap类似,它继承自Dictionary类,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了Hashtable在写入时会比较慢。

LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历
LinkedHashMap时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。

一般情况下,我们用的最多的是HashMap,在Map中插入、删除和定位元素,HashMap 是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列.

2.1.3.==比较的是什么?

“== “ 对比两个对象基于内存引用,如果两个对象的引用完全相同(指向同一个对象)时,“== ”操作将返回true, 否则返回false。“== ”如果两边是基本类型,就是比较数值是否相等。

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

Java语言支持的8种基本数据类型是:
byte、short、int、long、float、double、boolean、char
自动装箱是Java编译器在基本数据类型和对应的对象包装类型之间做的一个转化。比如:把int转化成Integer, double 转化成Double,等等。反之就是自动拆箱。

2.1.6、String 和StringBuffer的区别*

JAVA平台提供了两个类: String和StringBuffer,它们可以储存和操作字符串,即包含多个字符的字符数据。
这个String类提供了数值不可改变的字符串。
而StringBuffer类提供的字符串进行修改。当你知道字符数据要改变的时候你就可以使用StringBuffer,典型地,你可以使用StringBuffers来动态构造字符数据。

2.1.7.类和对象的区别

1.类是对某一类事物的描述,是抽象的;而对象是一个实实在在的个体,是类的-个实例。比如:“人”是一个类,而“教师”则是“人”的一个实例。
2.对象是函数、变量的集合体;类是一组具有相同属性
的对象集合体。

2.1.8.讲讲 static 关键字和 final 关键字

1、final修饰的类不可以被继承;final修饰的方法不可以被重写;final修饰的变量不可以被修改;
2.static修饰的方法:
只能调用其他的static方法
只能使用static变量
不能以任何方式引用this或者super关键字

静态方法在编译时,最先被加载
静态方法和变量可以直接通过类名调用,因此静态方法中不能用this和super关键字,不能直接访问所属类的实例变量和实例方法

2.1.9.synchronized 关键字是怎么用的

①、修饰一个方法:
②、修饰代码块
③、修饰静态方法
④、修饰一个类
定义接口方法、构造方法时不能使用synchronized关键字
无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得锁的是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码,其他对象需要等待。

修饰一个类,其作用的范围是synchronized后面括号括起来的部分, 作用的对象是这个类的所有对象。
修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法, 作用的对象是调用这个方法的对象;
修改一个静态的方法,其作用的范围是整个静态方法, 作用的对象是这个类的所有对象;
修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;

2.1.10.BIO、NIO、AIO 区别有哪些

BIO (BlockingI/O):同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。这里使用那个经典的烧开水例子,这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是,叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。

NIO (New I/O):同时支持阻塞与非阻塞模式,在BIO模型中,如果需要并发处理多个I/O请求,那就需要多线程来支持,NIO使用了多路复用器机制,以socket使用来说,多路复用器通过不断轮询各个连接的状态,只有在socket有流可读或者可写时,应用程序才需要去处理它,在线程的使用上,就不需要一个连接就必须使用一个处理线程了,而是只是有效请求时(确实需要进行I/O处理时),才会使用一个线程去处理,这样就避免了BIO模型下大量线程处于阻塞等待状态的情景。

AIO ( AsynchronousI/O):异步非阻塞I/O模型。异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理

2.1.11.接口和抽象类的区别?什么时候用接口,什么时候用抽象类?接口可以继承接口吗?

Java提供和支持创建抽象类和接口。它们的实现有共同点,不同点在于:
1.接口中所有的方法都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
2.类可以实现很多个接口,但是只能继承一个抽象类
3.类可以不实现抽象类和接口声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。抽象类可以在不提供接口方法实现的情况下实现接口。
4.Java接口中声明的变量默认都是final的。抽象类可以包含非final的变量。
5.Java接口中的成员函数默认是public的。抽象类的成员函数可以是private, protected 或者是public.
6.接口是绝对抽象的,不可以被实例化。抽象类也不可以被实例化,但是,如果它包含main方法的话是可以被调用的。
7.接口可以继承接口

2.1.12集合部分

2.1.12.1 HashMap 和 HashSet 的区别

HashSet实现了Set接口,它不允许集合中出现重复元素。
HashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许出现重复的键(Key)

HashMap :
实现了Map接口

存储键值对

调用put()

HashMap使用键(Key)计算Hashcode

对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false,HashMap相对于HashSet较快,因为它是使用唯一的键获取对象,HashSet较HashMap来说比较慢

HashSet :
实现Set接口、
仅存储对象
向map中添加元素、
调用add()方法向Set中添加元素
HashSet使用成员对象来计算hashcode值,

2.1.12.2 HashMap 和 Hashtable 有什么区别?
  1. hashmap可以允许一个键和多个值为空,Hashtable 不允许键和值为空
  2. 线程安全性不同:HashMap的方法都没有使用synchronized关键字修饰,都是非线程安全的,而Hashtable的方法几乎都是被synchronized关键字修饰的。但是,当我们需要HashMap是线程安全的时,怎么办呢?我们可以通过Collections.synchronizedMap(hashMap)来进行处理,亦或者我们使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
  3. 初始容量大小和每次扩充容量大小的不同:Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
  4. 计算hash值的方法不同:为了得到元素的位置,首先需要根据元素的 KEY计算出一个hash值,然后再用这个hash值来计算得到最终的位置。Hashtable直接使用对象的hashCode
2.1.12.3 HashSet如何检查重复

当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会和其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现,但是如果发现有相同的hashcode值的对象,这时候会调用equals方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。

2.1.12.4 请说一下HashMap的底层实现原理

HashMap使用数组加链表实现。每个数组中储存着链表。

当使用put方法储存key-value键值对时,会先调用key的hashCode方法,得到此key的哈西值,然后将此值通过其他运算最终得到一个值作为此key在数组中的索引值,然后将key-value键值对储存进去。通过这种方法将储存的不同key-value键值对“散列”到数组的不同位置。

在储存的时候,如果索引位置尚无元素,那么直接储存。如果有元素,那么就调用此key的equals方法与原有的元素的Key进行比较。如果返回true,说明在这个equals定义的规则上,这两个Key相同,那么将原有的key保留,用新的value代替原来的value。如果返回false,说明这两个key是不同元素,那么就与此链表的下一个结点进行比较,知道最后一个结点都没有相同元素,再下一个是null的时候,就用头插法将此key-value添加到链表上。HashMap对重复元素的处理方法是:key不变,value覆盖。

当使用get方法获取key对应的value时,会和储存key-value时用同样的方法,得到key在数组中的索引值,如果此索引值上没有元素,就返回null。如果此索引值上有元素,那么就拿此key的equals方法与此位置元素上的key进行比较,如果返回true。就返回此位置元素对应的value。如果返回false,就一直按链表往下比较,如果都是返回false,那么就返回null。

2.1.12.5 说一下 HashSet 的实现原理?

HashSet 是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。

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

Array和ArrayList的不同点:
Array可以包含基本类型和对象类型,ArrayList 只能包含对象类型。
Array大小是固定的,ArrayList的大小是动态变化的。
ArrayList提供了更多的方法和特性,比如: addAll(), removeAll(), iterator()等等。对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。

2.1.12.7 ArrayList 和 LinkedList 的区别是什么?

数据结构实现:ArrayList 是数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。

随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。

增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。

综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

2.1.13 线程部分

2.1.13.1 Java 中线程的状态?

1.新建(new ):新创建了一个线程对象。
2.可运行(runnable ):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权。
3.运行(running ):可运行状态( runnable )的线程获得了cpu时间片(timeslice ) ,执行程序代码。
4.阻塞( block ):阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu的时间片timeslice ,暂时停止运行。直到线程进入可运行(runnable )状态,才有机会再次获得cpu的使用权转到运行( running)状态。
阻塞的情况分三种:
(一).等待阻塞:运行( running )的线程执行o.wait()方法,JVM会把该线程放入等待队列( waitting queue )中。
(二).同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool )中。
(三).其他阻塞:运行(running )的线程执行Thread.sleep (long ms)或t.join ()
方法,或者发出了Ⅰ/О请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者Ⅰ/0处理完毕时,线程重新转入可运行(runnable)状态。
5.死亡( dead ):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

2.1.13.2 并行和并发有什么区别?

并行:多个处理器或多核处理器同时处理多个任务。
并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
如下图:【并发 = 两个队列和一台咖啡机】 【并行 = 两个队列和两台咖啡机】

2.1.13.3 多线程有几种实现方式?

1.继承Thread类
2.实现Runnable接口
3.实现Callable接口通过FutureTask包装器来创建Thread线程
4.通过线程池创建线程,使用线程池接口ExecutorService结合Callable、Future实现有返回结果的多线程。

2.1.13.4 如何保证线程安全?

通过合理的时间调度,避开共享资源的存取冲突。另外,在并行任务设计上可以通过适当的策略,保证任务与任务之间不存在共享资源,设计一个规则来保证一个客户的计算工作和数据访问只会被一个线程或一台工作机完成,而不是把一个客户的计算工作分配给多个线程去完成。

2.1.13.5 请说明一下sleep() 和 wait() 有什么区别?

sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

前面两种【无返回值】原因:通过重写run方法,run方法的返回值是void,所以没有办法返回结果。
后面两种【有返回值】原因:通过Callable接口,就要实现call方法,这个方法的返回值是Object,所以返回的结果可以放在Object对象中。

2.1.13.6 线程有哪些状态?

线程的6种状态:

初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

阻塞(BLOCKED):表示线程阻塞于锁。

等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

终止(TERMINATED):表示该线程已经执行完毕。

2.1.13.7 创建线程池有哪几种方式?

线程池创建有七种方式,最核心的是最后一种:

newSingleThreadExecutor():它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;

newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;

newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;

newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;

newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;

newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;

ThreadPoolExecutor():是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。

2.1.13.8 线程池都有哪些状态?

running:这是最正常的状态,接受新的任务,处理等待队列中的任务。

shutdown:不接受新的任务提交,但是会继续处理等待队列中的任务。

stop:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。

tidying:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为
TIDYING 状态时,会执行钩子方法 terminated()。

terminated:terminated()方法结束后,线程池的状态就会变成这个。

2.1.13.9 线程池中 submit() 和 execute() 方法有什么区别?

execute():只能执行 Runnable 类型的任务。
submit():可以执行 Runnable 和 Callable 类型的任务。
Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。

2.1.13.10 什么是死锁?

当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

2.1.14 反射部分

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

2.1.15.Object 类下有哪些方法?

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

2.1.16.在JAVA中怎么把字符串 “123” 转换成整型123,整型123转换成字符串“123”的 API 又是什么?

字符串 “123” 转换成整型123:
1.Integer.parseInt(“123”);
2. Integer.valueOf(String);
整型123转换成字符串“123”
1.) String s = String.valueOf(i);
2.) String s = Integer.toString(i);
3.) String s = “” + i;

2.1.17.创建线程有几种方式?分别是怎么做的?

一、继承Thread类创建线程类

(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。

(2)创建Thread子类的实例,即创建了线程对象。

(3)调用线程对象的start()方法来启动该线程。

package com.nf147.Constroller;
public class FirstThreadTest extends Thread {

    int i = 0;

    //重写run方法,run方法的方法体就是现场执行体
    public void run() {
        for (; i < 100; i++) {
            System.out.println(getName() + "  " + i);
        }
    }

    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "  : " + i);
            if (i == 50) {
                new FirstThreadTest().start();
                new FirstThreadTest().start();
            }
        }
    }


}

上述代码中Thread.currentThread()方法返回当前正在执行的线程对象。GetName()方法返回调用该方法的线程的名字。

二、通过Runnable接口创建线程类

(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。

package com.nf147.Constroller;

public class RunnableThreadTest implements Runnable{
       private int i;
       public void run()
       {
           for(i = 0;i <100;i++)
           {
               System.out.println(Thread.currentThread().getName()+" "+i);
           }
       }
       public static void main(String[] args)
       {
           for(int i = 0;i < 100;i++)
           {
               System.out.println(Thread.currentThread().getName()+" "+i);
               if(i==20)
               {
                   RunnableThreadTest rtt = new RunnableThreadTest();
                   new Thread(rtt,"新线程1").start();
                   new Thread(rtt,"新线程2").start();
               }
           }

       }
}

三、通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

package com.nf147.Constroller;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableThreadTest implements Callable<Integer> {


    public static void main(String[] args) {
        CallableThreadTest ctt = new CallableThreadTest();
        FutureTask<Integer> ft = new FutureTask<>(ctt);
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
            if (i == 20) {
                new Thread(ft, "有返回值的线程").start();
            }
        }
        try {
            System.out.println("子线程的返回值:" + ft.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }

    @Override
    public Integer call() throws Exception {
        int i = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
        return i;
    }
}

=创建线程的三种方式的对比

①采用实现Runnable、Callable接口的方式创建多线程时,
优势是:

1.线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
2.在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势是:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

②使用继承Thread类的方式创建多线程时
优势是:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。

2.1.18 synchronized、ReentrantLock 区别?

原始构成:

Synchronized 是关键字,属于JVM层面
Lock 是 java.util.concurrent.locks.lock 包下的,是 api层面的锁。

使用方法:
Synchronized 不需要用户手动释放锁,代码完成之后系统自动让线程释放锁
ReentrantLock 需要用户手动释放锁,没有手动释放可能导致死锁。

等待是否可以中断:

Synchronized 不可中断,除非抛出异常或者正常运行完成
ReentrantLock 可以中断。一种是通过 tryLock(long timeout, TimeUnit unit),另一种是lockInterruptibly()放代码块中,调用interrupt()方法进行中断。

加锁是否公平:

synchronized 是非公平锁
ReentrantLock 默认非公平锁,可以在构造方法传入 boolean 值,true 代表公平锁,false 代表非公平锁。

锁绑定多个 Condition:

Synchronized 只有一个阻塞队列,只能随机唤醒一个线程或者唤醒全部线程。
ReentrantLock 用来实现分组唤醒,可以精确唤醒。

2.1.19 CountDownLatch 和 Semaphore 他们的区别是什么?

CountDownLatch: 使一个线程A或是组线程A等待其它线程执行完毕后,一个线程A或是组线程A才继续执行
Semaphore:是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源

使用场景
由于CountDownLatch有个countDown()方法并且countDown()不会引起阻塞,countDown()用于在一个线程执行完毕后调用,使CountDownLatch计数器减一,所以CountDownLatch可以应用于主线程等待所有子线程结束后再继续执行的情况

比如现在要让第 5 个线程等待前 4 个线程执行完毕再执行,具体怎么做?

创建countDownLatch计数器为4,当前面线程执行完就调用countDown()方法将计数器减一,在线程五前面加countDownLatch.await();当计数器不等于0时线程五一直处于阻塞状态

public class ThreadTest  extends Thread{

    public static void main(String[] args) {
        final CountDownLatch countDownLatch = new CountDownLatch(4);//计数器
        (new ThreadTest(){
            public void run() {
                System.out.println("线程1开始执行..........");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程1执行完了............");
                countDownLatch.countDown();
            }
        }).start();
        (new ThreadTest(){
            public void run() {
                System.out.println("线程2开始执行..........");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程2执行完了............");
                countDownLatch.countDown();
            }
        }).start();
        (new ThreadTest(){
            public void run() {
                System.out.println("线程3开始执行..........");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程3执行完了............");
                countDownLatch.countDown();
            }
        }).start();
        (new ThreadTest(){
            public void run() {
                System.out.println("线程4开始执行..........");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程4执行完了............");
                countDownLatch.countDown();
            }
        }).start();
        (new ThreadTest(){
            public void run() {
                System.out.println("线程5开始执行..........");
                try {
                    countDownLatch.await();
                    Thread.sleep(100);
                    System.out.println("线程5执行完了............");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        System.out.println("执行结束");
    }
}

2.1.20、cyclicbarrier和countdownlatch的区别

CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

2.1.24 请你讲讲&和&&的区别?

&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true整个表达式的值才是true。&&之所以称为短路运算是因为,如果&&左边的表达式的值是false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用&&而不是&,例如在验证用户登录时判定用户名不是null而且不是空字符串,应当写为:username != null &&!username.equals(""),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的equals比较,否则会产生NullPointerException异常。

2.1.25 请你讲讲数组(Array)和列表(ArrayList)的区别?

Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。
Array大小是固定的,ArrayList的大小是动态变化的。
ArrayList提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等。

2.1.26 请说明重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。

重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载; 重载对返回类型没有特殊的要求。

重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。

2.1.27 请说明内部类可以引用他包含类的成员吗,如果可以,有没有什么限制吗?

一个内部类对象可以访问创建它的外部类对象的内容,内部类如果不是static的,那么它可以访问创建它的外部类对象的所有属性

内部类如果是sattic的,那么它只可以访问创建它的外部类对象的所有static属性

一般普通类只有public或package的访问修饰,而内部类可以实现static,protected,private等访问修饰。

当从外部类继承的时候,内部类是不会被覆盖的,它们是完全独立的实体,每个都在自己的命名空间内,如果从内部类中明确地继承,就可以覆盖原来内部类的方法。

2.1.28 请说明JAVA语言如何进行异常处理

Java 通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的接口。

在Java中,每个异常都是一个对象,当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并进行处理。

Java的异常处理是通过5个关键词来实现的:try、catch、throw、throws和finally。一般情况下是用try来执行一段程序,如果出现异常,系统会抛出(throws)一个异常,这时候你可以通过它的类型来捕捉(catch)它,或最后(finally)由缺省处理器来处理。

2.1.29 abstract class 和interface有什么区别?

含有abstract修饰符的class即为抽象类,abstract 类不能创建的实例对象。含有abstract方法的类必须定义为abstract class,abstract class类中的方法不必是抽象的。abstract class类中定义抽象方法必须在具体(Concrete)子类中实现,所以,不能有抽象构造方法或抽象静态方法。如果的子类没有实现抽象父类中的所有抽象方法,那么子类也必须定义为abstract类型。

接口(interface)可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的。接口中的方法定义默认为public abstract类型,接口中的成员变量类型默认为public static final。

2.1.30请解释一下String为什么不可变?

String 不可变是因为在 JDK 中 String 类被声明为一个 final 类,且类内部的 value 字节数组也是 final 的,只有当字符串是不可变时字符串池才有可能实现,字符串池的实现可以在运行时节约很多 heap 空间,因为不同的字符串变量都指向池中的同一个字符串;

因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子改变字符串指向的对象的值造成安全漏洞;

因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享,这样便不用因为线程安全问题而使用同步,字符串自己便是线程安全的;

因为字符串是不可变的所以在它创建的时候 hashcode 就被缓存了,不变性也保证了 hash码的唯一性,不需要重新计算,这就使得字符串很适合作为 Map 的键,字符串的处理速度要快过其它的键对象,这就是 HashMap 中的键往往都使用字符串的原因。

2.1.31 阐述ArrayList、Vector、LinkedList的存储性能和特性

ArrayList 和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以查询数据快而插入数据慢,Vector中的方法由于添加了synchronized修饰,因此Vector是线程安全的容器,但性能上较ArrayList差,

LinkedList使用双向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更高),按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。

由于ArrayList和LinkedListed都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类Collections中的synchronizedList方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强实现)。

2.1.34 JDK 和 JRE 有什么区别?

JDK 为java提供了编译环境和运行环境
JRE只为java提供了运行环境

2.2 设计模式

设计模式的目的:使用设计模式是为了让代码具有可扩展性,实现高聚合、低耦合的特性。

2.2.1 设计模式的分类

创建型:
Abstract Factory(抽象工厂模式)
Factory Method(工厂方法模式):用于创建不同类型的实例
Builder(建造者模式)
Singleton(单例模式):单例模式是一种创建型的模式,指某个类采用单例模式后,在这个类被创建后,只产生一个实例以供外部访问,且提供一个全局的访问点。

Prototype(原始模型模式)

结构型:
Adapter(适配器模式)
Decorator(装饰模式)
Proxy(代理模式)
Bridge(桥梁模式)
Composite(合成模式)
Flyweight(享元模式)

行为型:
Strategy(策略模式),
Template Method(模板方法模式)
Iterator(迭代子模式)
Observer(观察者模式)
Chain Of Responsibility(责任链模式)。
Command(命令模式)
Memento(备忘录模式)
State(状态模式)
Interpreter(解释器模式)

2.2.2 单例模式的实现方式:

①饱汉模式:

    package designpatterns.singleton;
     
    public class Singleton1 {
     
        private static Singleton1 instance;
     
        public static Singleton1 getInstance() {
            if (instance == null) {
                instance = new Singleton1();
            }
            return instance;
        }
    }

这种模式下,可以做到延迟化加载,但是在多线程模式下会产生多个实例,
此模式非线程安全,不推荐使用。

  1. 饱汉模式改进
    package designpatterns.singleton;
     
    /**
     * Created by Olive on 2017/12/8.
     */
    public class Singleton2 {
     
        private static Singleton2 instance;
     
        public static synchronized Singleton2 getInstance() {
            if (instance == null) {
                instance = new Singleton2();
            }
            return instance;
        }
    }

此模式将getInstance方法进行同步,看似解决了线程的安全问题,但是同步粒度太大,效率十分低下,不建议使用。

  1. 饿汉模式
    package designpatterns.singleton;
     
    /**
     * Created by Olive on 2017/12/8.
     */
    public class Singleton3 {
     
        private static final Singleton3 instance = new Singleton3();
     
        public static Singleton3 getInstance() {
            return instance;
        }
     
    }

此模式在类加载时就实例化对象,做到了线程安全,但是instance在类装载时就实例化。但是想象一种场景,如果实例化instance非常得消耗资源,或者实例的使用并非那么频繁,我们想让实例延迟加载,在我们需要使用的时候才被实例化,那这种时候此模式就很不合理了。这是一种十分耗费资源的粗暴的模式,不建议使用。

  1. 双重检查锁
package designpatterns.singleton;
 
/**
 * Created by Olive on 2017/12/8.
 */
public class Singleton4 {
 
    private volatile static Singleton4 instance;
 
    public static Singleton4 getInstance() {
        if (instance == null) {
            synchronized (Singleton4.class) {
                if (instance == null)
                    instance = new Singleton4();
            }
        }
        return instance;
    }
}

此模式将与第二种单例的模式有点类似,减少了同步的开销。相对来说是一个合理的单例写法,但是此模式也并不是完美的。我们知道类的实例化并不是一个简单的操作,内部其实主要包含了以下步骤:分配内存,初始化,实例指向内存。那么当我们在进入第二个if (instance == null)时,开始为实例分配内存,此时其他线程执行到第一个if (instance == null)时,instance并不为空,于是此线程将直接返回instance,然而此时的instance因为没有实例化结束,返回的实例并不是完整的。这就会导致错误的发生,这种模式需要慎用。

  1. 静态内部类
    package designpatterns.singleton;
     
    /**
     * Created by Olive on 2017/12/8.
     */
    public class InnerSingleton {
     
        // 静态内部类
        private static class GetInstance {
            private static final InnerSingleton INSTANCE = new InnerSingleton();
        }
     
        public static final InnerSingleton getInstance() {
            return GetInstance.INSTANCE;
        }
    }

此模式起到了延时加载的作用,只有显示调用getInstance方法时,才会显示装载GetInstance类,从而实例化INSTANCE。此方法线程安全,推荐使用。

2.2.3 工厂模式

用于创建不同类型的实例,如spring中创建bean就是利用此模式

2.3 java web

2.3.1 Spring 中依赖注入有几种方式?

•构造器注入。
•setter注入。
•接口注入。

2.3.2 SpringMVC 的这种 MVC 模式了解吗?用到了哪些设计模式?

MVC:模型-视图-控制器(MVC)是一个众所周知的以设计界面应用程序为基础的设计模式。它主要通过分离模型、视图及控制器在应用程序中的角色将业务逻辑从界面中解耦。
通常,模型负责封装应用程序数据在视图层展示。
视图仅仅只是展示这些数据,不包含任何业务逻辑。
控制器负责接收来自用户的请求,并调用后台服务(manager或者dao)来处理业务逻辑。

处理后,后台业务层可能会返回了一些数据在视图层展示。控制器收集这些数据及准备模型在视图层展示。MVC模式的核心思想是将业务逻辑从界面中分离出来,允许它们单独改变而不会相互影响。

工厂设计模式:
Spring使用工厂模式可以通过 BeanFactory 或 ApplicationContext 创建 bean 对象。

两者对比:

  • BeanFactory :延迟注入(使用到某个 bean 的时候才会注入),相比于BeanFactory 来说会占用更少的内存,程序启动速度更快。
  • ApplicationContext :容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory ,除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用ApplicationContext会更多。
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
 
public class App {
	public static void main(String[] args) {
		ApplicationContext context = new FileSystemXmlApplicationContext(
				"C:/work/IOC Containers/springframework.applicationcontext/src/main/resources/bean-factory-config.xml");
 
		HelloApplicationContext obj = (HelloApplicationContext) context.getBean("helloApplicationContext");
		obj.getMsg();
	}
}

单例设计模式:
Spring 中 bean 的默认作用域就是单例的

代理设计模式
Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理

工厂设计模式 : Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
代理设计模式 : Spring AOP 功能的实现。
单例设计模式 : Spring 中的 Bean 默认都是单例的。
模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配

2.3.3 为什么要使用 spring?

spring 提供 ioc 容器,容器会帮你管理依赖的对象,从而不需要自己创建和管理依赖对象了,更轻松的实现了程序的解耦。

spring 提供了事务支持,使得事务操作变的更加方便。

spring 提供了面向切片编程,这样可以更方便的处理某一类的问题。

更方便的框架集成,spring 可以很方便的集成其他框架,比如 MyBatis、hibernate 等。

2.3.4 解释一下什么是 aop和IOC?

aop 是面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。可运用在比如统一处理日志、异常等。

ioc(中文:控制反转)是 spring 的核心,对于 spring 框架来说,就是由 spring 来负责控制对象的生命周期和对象间的关系。

2.3.5 spring 中的 bean 是线程安全的吗?

spring 中的 bean 默认是单例模式,spring 框架并没有对单例 bean 进行多线程的封装处理。

实际上大部分时候 spring bean 无状态的(比如 dao 类),所有某种程度上来说 bean 也是安全的,但如果 bean 有状态的话(比如 view model 对象),那就要开发者自己去保证线程安全了,最简单的就是改变 bean 的作用域,把“singleton”变更为“prototype”,这样请求 bean 相当于 new Bean()了,所以就可以保证线程安全了。

有状态就是有数据存储功能。
无状态就是不会保存数据。

2.4mybatis

2.4.1 MyBatis 中 #{}和 ${}的区别是什么?

(1)mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值。

(2)mybatis在处理 时 , 就 是 把 {}时,就是把 {}替换成变量的值。

(3)使用#{}可以有效的防止SQL注入,提高系统安全性。原因在于:预编译机制。预编译完成之后,SQL的结构已经固定,即便用户输入非法参数,也不会对SQL的结构产生影响,从而避免了潜在的安全风险。

2.4.2 MyBatis 是否支持延迟加载?延迟加载的原理是什么?

MyBatis 支持延迟加载,设置 lazyLoadingEnabled=true 即可。

原理:简单的说是,生成代理对象,对象方法调用时执行查询语句

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值