手把手教你如何玩转面试题(Java基础)

        下面的这些题目,主要是根据自己的亲身经历以及在学习的过程中碰到比较典型的内容,所以把这些进行整理,方便于更多的人进行学习和交流。  

下面是其他方面的知识点,欢迎大家进行浏览

Spring的精华:https://blog.csdn.net/cs_hnu_scw/article/details/78677502

Hibernate的精华:https://blog.csdn.net/cs_hnu_scw/article/details/78762294

计算机网络:https://blog.csdn.net/Cs_hnu_scw/article/details/79896621

Web方向:https://blog.csdn.net/Cs_hnu_scw/article/details/79896165

数据库:https://blog.csdn.net/Cs_hnu_scw/article/details/79896384

操作系统:https://blog.csdn.net/Cs_hnu_scw/article/details/79896500

数据结构:https://blog.csdn.net/Cs_hnu_scw/article/details/79896717

其余技术方面:https://blog.csdn.net/Cs_hnu_scw/article/details/79896876

一:Java基础

1:Object类中含有哪些方法,分别的作用是什么?

答:一共是有12个方法,可以分为如下几类:

(1)构造方法:Object()

(2)判断对象相等:hashCode()和equals(object)

(3)线程相关:wait(),wait(long),wait(long,int),notify(),notifyAll()

(4)复制对象:clone()

(5)垃圾回收:finalize()

(6)对象本身相关内容:toString() 和getClass()


2:对象重写equals方法需要注意什么?

答:(1)自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true

(2)对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true

(3)传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true

(4)一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。 对于任何非空引用值 x,x.equals(null) 都应返回 false

对于这个问题,我们用得比较多的就是String类了,它这里面就是对equals方法进行了重写;


温馨提示:一般如果重写了equals()方法,那么也最好将hashcode()方法进行重写;

3:HashMap中的容量为什么是以2的幂次大小?(默认是16)

答:这里其实主要是为了进行hash的时候能够更加的均匀,因为在这里面不是直接进行取模,而是利用了取容量大小的N位来进行“&”操作的,就比如,初始默认是16=2的4次方,所以进行hash的时候进行与“1111”进行“&”操作而得到的hash,这样的好处就使得hash更加均匀。

4:HashMap在1.8之后添加了红黑树的结构,而在1.7是以数组和链表的结构,这样的好处在于?

答:主要是为了解决链表hash冲突过多,这样的时间复杂度就是O(n),所以出现的使用红黑树来进行解决。

HashMap添加红黑树的原理和实现

5:HashMap中进行扩容为什么是扩展为原来大小的2倍?

答:其实这个与它本身的容量大小值和它的hash算法有关系;

(1)首先是容量大小:默认的时候是16,即2的4次方,即是2的幂次方的关系,那么进行扩容操作是满足2的倍数进行则更加好计算大小;

(2)Hash算法:因为在HashMap中,它进行hash判断索引的时候,是通过的与当前容量的2的幂次方的N位来进行“&”操作,所以这就要求扩展的容量必须是2的倍数,否则进行的hash取值就不能够进行最大化的均匀;就比如:初始值是16=2的4次方,所以后面就是与“1111”进行相“&”,而现在扩容之后是32=2的5次方,则进行的是与“11111”进行相“&”,所以如果不是以2的倍数进行扩容,那么就违背了它本身的hash运算的规律;

6:请说一下快排的原理和实现

答:时间复杂度:O(nlogN),是一种非稳定性的排序算法;

实现代码:

/**
	 * 快速排序
	 * @param number 排序数组
	 * @param geshu  数组个数-1为数组的最后一个索引位置
	 * @return
	 */
	private static int[] quicksortWay(int[] number, int geshu) {
		quickSort(number , 0 , geshu -1 );
		return number;
	}
	/**
	 * 进行快速排序的方法
	 * @param number  排序数组
	 * @param low     排序数组的起始位置索引
	 * @param hight   排序数组的终止位置索引
	 */
	private static void quickSort(int[] number, int low, int hight) {
		int begin = 0;
		int end = 0 ;
		if(low > hight){
			return ;
		}
		begin = low ;
		end = hight ;
		int index = number[low];  //获取到需要排序的第一个位置的内容为基准值
		while(begin < end){
			while(begin < end && number[end] >= index){  //找到比基准小的数
				end-- ;
			}
			if(begin < end){
				number[begin++] = number[end]; //将小于基准的位置的内容换到第一个位置,然后后面的从第二个位置继续开始排序
			}
			while(begin < end && number[begin] < index){
				begin++;      //一直找到不小于基准数的位置,这样的话,前面的都是小于基准的值
			}
			if(begin < end){
				number[end--] = number[begin] ;
			}
		}
		number[begin] = index;
		quickSort(number, low, begin-1);
		quickSort(number, begin+1, hight);
		
	}

7:请说一下堆排序的原理和实现

答:时间复杂度O(nlogN),是一种非稳定性的排序算法;

实现代码:

//堆排序
	private static int[] duiNumberWay(int[] number, int geshu) {
		int suoyin=geshu-1;   //数组的最大下标
        for(int i=0;i<geshu;i++){                    //(优化)其实排序的次数为i<geshu-1就可以了,因为最后一趟其实都不用排了
        	creatMaxHead(number,suoyin-i);  				//得到每次的最大堆的排序
        	getOrderArray(number,0,suoyin-i);               //得到每次最大的数都和之前无序的数组的最后一个无序数组的位置的索引进行替换
        }
		return number;
	}

	/*
	 * 将无序的数组逐次变成有序的,每次找到一个最大的,则将最大的放到无序数组的最后一个(这样从后面的就是一个有序的,从大到小的顺序)
	 * 参数:start表示的是,因为每次找到的堆中都是数组0的值最大
	 *      end表示的是最后一个无序数组的索引
	 *      (这个的方法作用和swapMaxVaule的其实是一样都是交换最大的值,只是这样写区分一下,那是对每个小树的值的交换)
	 */
    private static void getOrderArray(int[] number, int start, int end) {
		int temp=number[end];       
		number[end]=number[start];
		number[start]=temp;		
	}

	/*
     * 得到每次堆排序的最大数的值,并且都放在索引为0的位置(这是堆排序的精髓的地方)
     */
	private static void creatMaxHead(int[] number, int lastIndex) {
		int currentIndex=0;   //保存当前的索引下标
		int bigMaxIndex=0;
		for(int i=(lastIndex-1)/2;i>=0;i--){
			currentIndex=i;        //当前根的下标
			if((currentIndex*2+1)<=lastIndex){  //判断当前结点是否有子节点
				bigMaxIndex=currentIndex*2+1;    //左结点的下标
				if(bigMaxIndex<lastIndex){     //表示有右结点
					if(number[bigMaxIndex]<number[bigMaxIndex+1]){  //左结点小于右结点的值
						bigMaxIndex=bigMaxIndex+1;
					}
				}
				if(number[currentIndex]<number[bigMaxIndex]){      //用左右结点的大值和根的值进行比较
						swapMaxVaule(currentIndex,bigMaxIndex,number);       //根结点的值小于左右结点中大的点
						currentIndex=bigMaxIndex;
				}
			}
		}
		
	}
    /*
     * 堆排序中,得到每一个小树的最大的值
     */
	private static void swapMaxVaule(int currentIndex, int bigMaxIndex,int[] number) {
		int temp=number[currentIndex];       
		number[currentIndex]=number[bigMaxIndex];
		number[bigMaxIndex]=temp;			
	}

8:Java中的线程的类型?

答:用户线程和守护线程(Daemon);----------注意一点:线程是JVM级别的,而静态变量是属于ClassLoader级别,所以在Web应用停止的时候,静态变量会被移除,但是线程并不是,所以线程的生命周期和Web程序的生命周期并不是一致的;所以这个也是需要守护线程的一个原因;

关于用户线程就是平常写的比较多的继承Thread和实现runnable接口的方式,对于守护线程可以看看这篇博文守护线程到底是个什么东西?

9:Java中的队列有哪些?哪些是线程安全的?

答:队列主要是实现了Queue接口,有ArrayBlocakingQueue,LinkedBlockingQueue,ConcurrentLinkedQueue,PriorityBlockingQueue,(这 四个是线程安全的)PriorityQueue,SynchronousQueue。

注意一点:另外还有个接口就是Deque,这是一个双向队列。

10:Java中的内部类有哪些?各自的特点是什么?

答:(1)静态内部类:

特点:1:静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象

2:创建静态内部类对象的一般形式为:  外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()

(2)成员内部类:成员内部类是最普通的内部类,它的定义为位于另一个类的内部,

特点:1:成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。这个原因在于:成员内部类是依赖于外部类的,之所以,能够访问外部类的内容是因为,在编译的时候,会默认的给成员内部类添加一个有参的构造函数(即使,自己定义了一个无参的构造器),而这个参数也正是外部类的对象的引用,所以,就能够引用外部类的成员变量和方法了。

2:内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限

3:成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象

4:当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:

外部类.this.成员变量
外部类.this.成员方法
5:创建成员内部类对象的一般形式为:  外部类类名.内部类类名 xxx = 外部类对象名.new 内部类类名()

(3)局部内部类:局部内部类是定义在一个方法或者一个作用域里面的类

特点:局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的

(4)匿名内部类:

特点:1:匿名内部类也是不能有访问修饰符和static修饰符的

2:匿名内部类是唯一一种没有构造器的类

3:匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写

常见的关于内部类的题目~!

一:为什么成员内部类可以无条件访问外部类的成员?

答:这个答案在我介绍成员内部类的特点中已经进行了讲解;

二:为什么局部内部类和匿名内部类只能访问局部final变量?

答:比如一个例子:

     如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。
  反编译上面的代码之后,可以看到,在run方法中访问的变量a根本就不是test方法中的局部变量a。这样一来就解决了前面所说的 生命周期不一致的问题。但是新的问题又来了,既然在run方法中访问的变量a和test方法中的变量a不是同一个变量,当在run方法中改变变量a的值的话,会出现什么情况?

  对,会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题,java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。所以就必须使用final进行修饰;

三:静态内部类有特殊的地方吗?

答:静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的,这个读者可以自己尝试反编译class文件看一下就知道了,是没有Outter this&0引用的。

https://blog.csdn.net/a1259109679/article/details/48156407

11:Java中导致JVM持久区发生溢出的原因有?导致JVM年老代发生溢出的原因有?

答:JVM中的堆分为持久代和年老代,

导致持久代发生溢出的原因:动态加载大量的java类

导致年老代发生溢出的原因:1:循环上万次的字符串处理2:创建上千万个对象 3:在一段代码内申请上百M甚至上G的内存

12:Java中的多态性是什么?

答:Java中的多态性有三个形式:

(1)方法的重载:(2)通过继承实现的方法的重写(3)通过实现接口的方法

Java中多态的条件:

(1)要有继承(2)要有重写(3)父类引用纸箱子类---也就是向上转型

Java中多态的分类:

(1)静态多态:其中编译 时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编译之后会变成两个不同的函数,在运行时谈不上多态。

(2)动态多态:它是通过动态绑定来实现的,也就是我们平常所说的多态性

13:Java中复制数组的方法和效率是如何?

答:方法和效率如下顺序:

System.arraycopy>clone>Arrays.copyOf>for循环遍历

14:Java中面向对象的设计原则有哪些?

答:七个基本原则: 
(1)单一职责原则(Single-Resposibility Principle):一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。 
(2)开放封闭原则(Open-Closed principle):软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。 
(3)里氏替换原则(Liskov-Substituion Principle):子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。 
(4)依赖倒置原则(Dependecy-Inversion Principle):依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。 

(5)接口隔离原则(Interface-Segregation Principle):使用多个小的专门的接口,而不要使用一个大的总接口

(6)迪米特原则:一个对象对于其他的对象保存尽量少的了解

(7)组合/聚合原则:类间关系尽量使用关联关系(组合,聚合),而少使用继承;

15:Java中常见的OOM和导致OOM的原因有哪些?

答:常见的OOM类型有如下几种:

(1)堆内存溢出

(2)虚拟机栈和本地方法栈溢出

(3)运行时常量池溢出

(4)方法区溢出

常见的OOM以及解决思路:

(1) java.lang.OutOfMemoryError: unable to create new native thread
当调用new Thread时,如已创建不了线程了,则会抛出此错误,如果是JDK内部必须创建成功的线程,那么会造成Java进程退出,如果是用户线程,则仅抛出OOM,创建不了的原因通常是创建了太多线程,耗尽了内存,通常可通过减少创建的线程数,或通过-Xss调小线程所占用的栈大小来减少对Java 对外内存的消耗。
(2)java.lang.OutOfMemoryError: request bytes for . Out of swap space?
当JNI模块或JVM内部进行malloc操作(例如GC时做mark)时,需要消耗堆外的内存,如此时Java进程所占用的地址空间超过限制(例如windows: 2G,linux: 3G),或物理内存、swap区均使用完毕,那么则会出现此错误,当出现此错误时,Java进程将会退出。
(3)java.lang.OutOfMemoryError: Java heap space(堆溢出) ,这是最常见的OOM错误
【解决思路】 
a.增加Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的大小 
b.检查是否发生内存泄漏 
c.看是否有死循环或不必要地重复创建大量对象 
(4) java.lang.OutOfMemoryError: GC overhead limit execeeded
当通过new创建对象或数组时,如Java Heap空间不足,且GC所使用的时间占了程序总时间的98%,且Heap剩余空间小于2%,则抛出此错误,以避免Full GC一直执行,可通过UseGCOverheadLimit来决定是否开启这种策略,可通过GCTimeLimit和GCHeapFreeLimit来控制百分比。
(5) java.lang.OutOfMemoryError: PermGen space(方法区或者运行时常量池溢出)
【解决思路】 
a.增加java虚拟机中的XX:PermSize和XX:MaxPermSize参数的大小 
b.频繁使用CGLib,动态代理,反射GeneratedConstructorAccessor需要强大的方法区来支撑 
(6)java.lang.StackOverflowError 虚拟机栈和本地方法栈溢出 
说明:对于以上几种OOM错误,其中容易造成严重后果的是Out of swap space这种,因为这种会造成Java进程退出,而其他几种只要不是在main线程抛出的,就不会造成Java进程退出

导致OOM的原因:(1)内存泄漏(连接未关闭,单例类中不正确引用了对象)

(2)代码中存在死循环或循环产生过多重复的对象实体,即会发生java.lang.stackoverflow异常
(3)Space大小设置不正确,即会导致outofMemory:permgenspace的这种方法区溢出
(4)内存中加载的数据量过于庞大,如一次从数据库取出过多数据,即会导致outofMemory:permgenspace的这种方法区溢出

(5)集合类中有对对象的引用,使用完后未清空,使得JVM不能回收,即会发生内存泄露

(6)无限递归次数,会导致线程栈溢出,即发生java.lang.stackoverflow异常

(7)程序加载的类过多,或者使用反射和cglib技术产生过多的类,即会导致outofMemory:permgenspace的这种方法区溢出

16:请问,你有进行过JVM调优吗?

答:一般主要回答一下:JVM的内存结构;堆的划分;GC的清除方法,GC的回收器;程序异常的类型(Error和Exception);常见的OOM的处理;等等信息

17:ConcurrentHashMap中的jdk1.7和jdk1.8的区别

答:jdk1.7版本:HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

      那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount变量,在put , remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。

   底层是由:数组和链表实现的;

jdk1.8版本的改进:

1、不采用segment而采用node,锁住node来实现减小锁粒度。 
2、设计了MOVED状态 当resize的中过程中 线程2还在put数据,线程2会帮助resize。 
3、使用3个CAS操作来确保node的一些操作的原子性,这种方式代替了锁。 
4、sizeCtl的不同值来代表不同含义,起到了控制的作用。

 5.底层是由:数组和链表+红黑树实现的;

18:CopyOnWriteArraylist的底层是什么?适用什么情况?与Collections.synchrnizedlist的区别

答:CopyOnWriteArrayList这是一个ArrayList的线程安全的变体,其原理大概可以通俗的理解为:初始化的时候只有一个容器,很常一段时间,这个容器数据、数量等没有发生变化的时候,大家(多个线程),都是读取(假设这段时间里只发生读取的操作)同一个容器中的数据,所以这样大家读到的数据都是唯一、一致、安全的,但是后来有人往里面增加了一个数据,这个时候CopyOnWriteArrayList 底层实现添加的原理是先copy出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。

优点:

1.解决的开发工作中的多线程的并发问题。2:用于读多写少的并发场景

缺点:
1.内存占有问题:很明显,两个数组同时驻扎在内存中,如果实际应用中,数据比较多,而且比较大的情况下,占用内存会比较大,针对这个其实可以用ConcurrentHashMap来代替。

2.数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器

不同点:CopyOnWriteArrayList和Collections.synchronizedList是实现线程安全的列表的两种方式。两种实现方式分别针对不同情况有不同的性能表现,其中CopyOnWriteArrayList的写操作性能较差,而多线程的读操作性能较好。而Collections.synchronizedList的写操作性能比CopyOnWriteArrayList在多线程操作的情况下要好很多,而读操作因为是采用了synchronized关键字的方式,其读操作性能并不如CopyOnWriteArrayList。因此在不同的应用场景下,应该选择不同的多线程安全实现类。

19:Java线程中调用的方法的状态转变


20:Reentrantlock和Sychronized的区别?

答:(1)等待可中断:前者能够对在等待的线程,当等待足够长的时间后可以进行可中断的操作;而后者不可以,必须一直等待拥有资源的线程进行释放资源;

(2)是否可设置公平锁:前者能够在进行构造函数的时候,传入true(默认是false,非公平锁),表示进行的是公平锁,也就是说对于先进行等待的线程先执行,而不是像后者一样进行随机的选择执行的线程;

(3)是否可以绑定多个Condition:前者是能够对多个condition进行绑定的,而后者则不行

(4)实现的层次:前者是属于JDK中的,其底层就是通过自旋锁,而后者是属于JVM来进行实现的;

(5)是否需要手动释放:前者是通过lock()方法进行加锁,一般是要在finily()方法进行unlock()方法的释放,而后者一般是不需要手动进行释放锁;

21:序列化和反序列的含义和底层原理?

答:含义(底层原理):
(1)Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程;
(2)序列化:对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建。
(3)反序列化:客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
(4)本质上讲,序列化就是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重建对象,恢复对象状态。
作用:
(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中; 
(2)通过序列化以字节流的形式使对象在网络中进行传递和接收; 
(3)通过序列化在进程间传递对象;
实现的方式:(三种)
假定一个User类,它的对象需要序列化,可以有如下三种方法:
(1)若User类仅仅实现了Serializable接口,则可以按照以下方式进行序列化和反序列化
ObjectOutputStream采用默认的序列化方式,对User对象的非transient的实例变量进行序列化。 
ObjcetInputStream采用默认的反序列化方式,对对User对象的非transient的实例变量进行反序列化。
(2)若User类仅仅实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject
(ObjectOutputSteam out),则采用以下方式进行序列化与反序列化。
ObjectOutputStream调用User对象的writeObject(ObjectOutputStream out)的方法进行序列化。 
ObjectInputStream会调用User对象的readObject(ObjectInputStream in)的方法进行反序列化。
(3)若User类实现了Externalnalizable接口,且User类必须实现readExternal(ObjectInput in)和writeExternal
(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。
ObjectOutputStream调用User对象的writeExternal(ObjectOutput out))的方法进行序列化。 
ObjectInputStream会调用User对象的readExternal(ObjectInput in)的方法进行反序列化。
注意事项:
(1)要进行序列化的类,必须实现Serialazable接口(相比实现Externalnalizable接口好)
(2)序列化时,只对对象的状态进行保存,而不管对象的方法;
(3)当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
(4)当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
(5)声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态,transient代表对象的临时数据。
(6)序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途:
        在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
        在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。
(7)如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决深拷贝的重要原因;

22:Java同步框架中的AQS?

答:https://blog.csdn.net/qq_14927217/article/details/72802089

23:Synchronized的底层原理?

答:使用的形式有三种:
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象。
实现的原理:
synchronized是基于Monitor来实现同步的。
Monitor从两个方面来支持线程之间的同步:
互斥执行
协作
1、Java 使用对象锁 ( 使用 synchronized 获得对象锁 ) 保证工作在共享的数据集上的线程互斥执行。
2、使用 notify/notifyAll/wait 方法来协同不同线程之间的工作。
3、Class和Object都关联了一个Monitor。
Monitor 的工作机理
1:线程进入同步方法中。
2:为了继续执行临界区代码,线程必须获取 Monitor 锁。如果获取锁成功,将成为该监视者对象的拥有者。任一时刻内,监视者对象只属于一个活动线程(The Owner)
3:拥有监视者对象的线程可以调用 wait() 进入等待集合(Wait Set),同时释放监视锁,进入等待状态。
4:其他线程调用 notify() / notifyAll() 接口唤醒等待集合中的线程,这些等待的线程需要重新获取监视锁后才能执行 wait() 之后的代码。
5:同步方法执行完毕了,线程退出临界区,并释放监视锁
synchronized的锁优化

锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。其中主要就是偏向锁,轻量级锁,重量级锁。


24:Java中造成内存泄露的情况有哪些?

答:参考本篇博文:https://blog.csdn.net/wwd0501/article/details/50544222

关于Java中的内存溢出的知识点,请参考上面的第15个知识点

25:CyclicBarrier和CountDownLatch的区别

答:两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:
(1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行
(2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务
(3)CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了

26:什么是线程安全?

答:线程安全也是有几个级别的:
(1)不可变
像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
(2)绝对线程安全
不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
(3)相对线程安全
相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
(4)线程非安全
这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类

27:为什么需要线程池?线程池的构造参数有哪些?分别的意思代表什么?

答:线程池的作用:(1)减少创建和销毁线程的次数,每个工作线程可以多次使用
(2)可根据系统情况调整执行的线程数量,防止消耗过多内存

(3)方便对线程进行管理

ThreadPoolExecutor类线程池的参数:
(1)corePoolSize:核心线程数,核心线程会一直存活,即使没有任务需要处理。当线程数小于核心线程数时,即使现有的线程空闲,线程池也会优先创建新线程来处理任务,而不是直接交给现有的线程处理
(2)maxPoolSize:当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。
(3)keepAliveTime:当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0。
(4)allowCoreThreadTimeout:是否允许核心线程空闲退出,默认值为false
(5)queueCapacity:任务队列容量。从maxPoolSize的描述上可以看出,任务队列的容量会影响到线程的变化,因此任务队列的长度也需要恰当的设置,一般的话都是使用无边界的阻塞队列,比如LinkedBlockQueue
线程池按以下行为执行任务:(需要注意第二点和第三点)
当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满
若线程数小于最大线程数,创建线程
若线程数等于最大线程数,抛出异常,拒绝任务

28:Java编写一个会死锁的程序?

答:可以参考这篇博文:https://blog.csdn.net/xidianliuy/article/details/51568073

29:Hashtable的size()方法中明明只有一条语句"return count",为什么还要做同步?

答:主要原因有两点:
(1)同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完毕之后才可以调用,这样就保证了线程安全性
(2)CPU执行代码,执行的不是Java代码,这点很关键,一定得记住。Java代码最终是被翻译成机器码执行的,机器码才是真正可以和硬件电路交互的代码。即使你看到Java代码只有一行,甚至你看到Java代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句"return count"假设被翻译成了三句汇编语句执行,一句汇编语句和其机器码做对应,完全可能执行完第一句,线程就切换了。

30:高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

答:(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
(2)并发不高、任务执行时间长的业务要区分开看:
  a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
  b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦

31:单例模式和静态方法的区别(使用情景)?

答:这个问题是在腾讯二面的时候被问到的,被深层次的怼了,所以,自己就好好整理了下;

(1)单例模式的创建方式,五种,饿汉,懒汉,双重检查,静态内部类,枚举(Effective Java推荐这种);----要掌握

(2)单例类可以实现接口和继承类,这样能够进行更多的业务和功能扩展,而对于静态方法来说,你每要进行扩展一个功能,那么就需要进行添加;

(3)生命周期:对于单例模式产生的那一个唯一实例,是不会被GC(因为单例中的那个变量是static的,是不会被回收)只有当JVM停止之后,才会被回收;静态方法里面的变量,当静态方法执行完后,都会被回收,所以对于会重复进行初始化使用的对象的话,这样当调用一次就要进行初始化一次,并且静态方法的类是在代码被编译的时候就会被加载;

(4)内存:单例模式在执行的时候需要new一个对象出来存储在堆栈中,可以被延迟初始化;而静态方法是不需要的,它不依赖于对象,而可以通过类进行直接调用,它是以代码块的形式进行存储;

(5)单例模式是一种面向对象的编程模式,而静态方法则是一种面向过程的模式;

(6)单例模式保证了其中的对象只会存在一个实例对象;

32:GC中利用可达性分析方法中,能够作为GC Root的有哪些对象?

答:在《深度理解Java 虚拟机》书中,主要就是提到下面这几种:

虚拟机栈中的引用对象
方法区中类静态属性引用的对象
方法区中常量引用对象
本地方法栈中JNI引用对象

33:说说类加载机制和双亲委派模型?

(1)类加载的过程:加载,连接(验证->准备->解析),初始化,使用,卸载;

具体的每个步骤可以参考这篇文章:https://blog.csdn.net/world6/article/details/52041857

(2)类加载器的种类(预定义三种 + 一种自定义):

1、Bootstrap ClassLoader:启动类加载器,也叫根类加载器,它负责加载Java的核心类库,加载如(%JAVA_HOME%/lib)目录下的rt.jar(包含System、String这样的核心类)这样的核心类库。根类加载器非常特殊,它不是java.lang.ClassLoader的子类,它是JVM自身内部由C/C++实现的,并不是Java实现的。
2、Extension ClassLoader:扩展类加载器,它负责加载扩展目录(%JAVA_HOME%/jre/lib/ext)下的jar包,用户可以把自己开发的类打包成jar包放在这个目录下即可扩展核心类以外的新功能。
3、System ClassLoader\APP ClassLoader:系统类加载器或称为应用程序类加载器,是加载CLASSPATH环境变量所指定的jar包与类路径。一般来说,用户自定义的类就是由APP ClassLoader加载的。
4、自定义的类加载器,主要就是通过继承ClassLoad类,然后进行重写里面的findClass()方法

(3)如何判断两个类是否为同一个类:

1、两个类来自同一个Class文件
2、两个类是由同一个虚拟机加载
3、两个类是由同一个类加载器加载
所以,在JVM中,判断两个类是否是相等的,就需要判断 类加载器 + 类名 的形式

(4)双亲委派模型(重点):当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

(5)双亲委派模型的作用:

(1)主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
(2)同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类。

(6)Class.forName()和ClassLoader.loadClass()的区别
   Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;(它也有可以控制static块是否执行的forName()函数);
   ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

可以看看这篇文章的例子,进行更深的理解:https://blog.csdn.net/cjm812752853/article/details/53956122

34:Tomcat的类加载与JVM的类加载有什么不同?(很重要的知识)

答:最主要的就是Tomcat的类加载不是采用双亲委派模型,这个是非常重要的,而原因是为什么主要就是下面的:

好好参考看一下这两篇文章,分别从简单到详细的介绍:

https://blog.csdn.net/dreamcatcher1314/article/details/78271251

https://blog.csdn.net/zjcjava/article/details/79465709

35:Java线程实现同步的方式有哪些?

答:https://blog.csdn.net/pdw2009/article/details/52373947

36:Java线程进行通信的方式有哪些?

答:https://blog.csdn.net/u011514810/article/details/77131296

37:Java单例模式的实现方式有哪些?各自的特点又有什么?

答:http://www.runoob.com/design-pattern/singleton-pattern.html

38:Java中的BIO,NIO,AIO的含义和特点?

答:https://blog.csdn.net/u013068377/article/details/70312551

39:子类覆盖父类方法的注意事项有哪些?(这个问题很多人并不全了解)

答:(1)覆盖的方法名,参数,返回类型必须一致

(2)子类不能缩小父类方法的访问权限

(3)子类不能抛出比父类大的异常

(4)方法覆盖只发生在子类和父类,而同一个类中只会发生方法重载

(5)父类的静态方法不能被子类覆盖为非静态方法

(6)父类的非静态方法可以被子类覆盖为静态方法

(7)子类可以定义和父类的静态方法同名的静态方法----这时候就要注意使用的是父类还是子类的实例对象了,这时候就是一种动态绑定,看左边

(8)父类的私有方法不能被子类覆盖-------因为私有方法是不会被子类继承的

(9)父类的抽象方法可以被子类通过两种途径覆盖;其一:通过实现抽象方法;其二:通过将子类作为抽象类,重新声明父类的抽象方法

(10)父类的非抽象方法可以被子类覆盖为抽象方法

40:请说说你对Java中的原子性,可见性和顺序性的理解

答:

(1)原子性

         原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。

(2)可见性

定义:当一个线程对共享变量进行了修改,但是还没有进行更新到内存的时候,此时,另一个线程,对该变量进行了操作,然而,由于还没有进行更新,所以读取的还是初始的变量的值,从而会发现变量不一致,出现覆盖的情况;而这个正是由于对可见性的一种体现。

解决方法:1:对操作方法进行同步,使用Sychonize关键字(2)对方法进行加锁处理,比如ReentLock(3)使用volatile关键字修改共享变量,但是注意,当且仅当是对该共享变量进行的原子性操作,这个方法才有效,而对于非原子性操作同样无法保证可见性;

(3)顺序性

定义:对于程序中的代码顺序,如果不具有相关性约束,那么在程序进行解析执行的时候,并不一定需要按照顺序执行,但是一定需要保证前后执行的结果是一致;主要就是为了提高代码的执行效率

比如代码:int a = 1; int b = a+2; int c = 3; int d = c+1;

解析:对于上面的代码,虽然int c =3,是在第三句话,但是,由于与前面的没有约束关系,所以,这句代码并不一样在前面两句代码后面执行,但是一定保证,int d 在 int c的后面,因为d中用到了c的变量,同理对于int b也是一样的道理;

41:

这并不是终点,而是我刚刚的开始,我会一直不断将知识点进行更新,欢迎大家进行关注和阅读!!!

猜你喜欢

转载自blog.csdn.net/cs_hnu_scw/article/details/79635874