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

 下面的这些题目,主要是根据自己的亲身经历以及在学习的过程中碰到比较典型的内容,所以把这些进行整理,方便于更多的人进行学习和交流。  内容有点多,可能你会很反感,但是,我相信,如果你能认真的看完我这些,当你回头再回想整个Java内容的时候,你就会清晰很多。因为,这是自己的学习经历,我相信有很多的人跟自己都一样,所以,给点信心,别怕多,这么多学习干货,为什么要回避呢?

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

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

Hibernate的精华:https://blog.csdn.net/cs_hnu_scw/article/details/7876229​​​​​​​44

计算机网络: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/82900559

操作系统: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

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

https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html

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
线程池按以下行为执行任务:(需要注意第二点和第三点)
当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满
若线程数小于最大线程数,创建线程
若线程数等于最大线程数,抛出异常,拒绝任务

Java线程池的类型:

newCacheThreadPool()----缓冲个数的线程池
newFixedThreadPool()-----固定个数的线程池
newSingleThreadPool()------单一线程池,即只有一个线程(注意:如果这里面的那个线程死了,就马上会创建一个新的线程继续工作)
newScheduledThreadPool()------定时任务的线程池

线程池的shutDown()和shutDownNow()方法的区别:

两者都是销毁线程池,前者是当线程池中所有的线程任务执行完成之后,就会把线程池进行销毁,而后者当执行到这句话之后,就会把线程池销毁,而不会关注到线程池中的线程任务是否已经执行完成。

线程池的excute()和submit()方法的区别:

两者都是添加线程,前者在添加完一个线程之后,没有带返回值,而后者在添加之后有返回值,返回的是一个Future对象,通过这个对象的相关方法可以判断当前线程是否执行完成或者如果线程执行过程中出现异常,通过这个对象也可以查看到出现异常的原因。

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中的原子性,可见性和顺序性的理解?(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也是一样的道理;

扩展知识点1:Happen-Before规则

(1)程序顺序原则:一个线程内保证语义的串行性 a = 1 ; b = a+1
(2)volatile规则:valatile变量的写,先发生于读,这保证了volatile变量的可见性
(3)锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
(4)传递性:A先于B,B先于C,那么A必然先于C
(5)线程的start()方法先于它的每一个动作
(6)线程的所有操作先于线程的终结(Thread.join())
(7)线程的中断(interrupt())先于被中断线程的代码

(8)对象的构造函数执行结束先于finalize()方法

扩展知识点2:fail-fast机制

定义: 当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
产生原因(源码角度):通过查看迭代器Iterator的源码可以看到,当modCount != expectedModCount的时候,会抛出并发修改异常.expectedModCount是在创建迭代器对象的时候,将modCount赋值初始化来的,所以当 list在创建迭代器之后,再次发生modCount变化的时候就会出现与期望的count不一致的情况。基本是add,remove,clear的时候会出现modCount的变化,于是
    1,在单线程操作不符合规则的时候,list加入元素,迭代器不知道,modCount发生变化,与期望不等
    2,多线程操作的时候,线程1在获取到当前modCount的时候,线程2进行了一些涉及元素个数变化的操作使得modCount发生了变化,与期望的count不等然后会抛出ConcurrentModificationException异常,产生fail-fast机制。
解决的方法(归根到底就是利用同步的形式来解决非同步的处理):
1:在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
2:使用CopyOnWriteArrayList来替换ArrayList。
CopyOnWriteArrayList是java.util.concurrent包下的类,支持多线程操作。其底层实现和ArrayList一样也是数组实现,同样有add remove等操作方法。

41:JVM的调式工具有用过什么?(JVM模块知识)
(1)jps:用来查看基于HotSpot JVM里面所有进程的具体状态, 包括进程ID,进程启动的路径等等。使用jps时,不需要传递进程号做为参数。
Jps也可以显示远程系统上的JAVA进程,这需要远程服务上开启了jstat服务,以及RMI注及服务,不过常用都是对本对的JAVA进程的查看。
命令格式
jps [ options ] [ hostid ]
(2)jstak:Jstat是JDK自带的一个轻量级小工具。它位于java的bin目录下,主要利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控。详细查看堆内各个部分的使用量,以及加载类的数量。使用时,需加上查看进程的进程id,和所选参数,它主要是用来显示GC及PermGen相关的信息。
命令格式
jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ]
(3)jstack:jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息,如果是在64位机器上,需要指定选项"-J-d64",
命令格式
jstack [ option ] pid
jstack [ option ] executable core
jstack [ option ] [server-id@]remote-hostname-or-IP
(4)jmap:打印出某个java进程(使用pid)内存内的,所有对象的情况(如:产生那些对象,及其数量)。可以输出所有内存中对象的工具,甚至可以将VM 中的heap,以二进制输出成文本。
(5)jconsole:jconsole:一个java GUI监视工具,可以以图表化的形式显示各种数据。并可通过远程连接监视远程的服务器VM。用java写的GUI程序,用来监控VM,并可监控远程的VM,非常易用,而且功能非常强。命令行里打 jconsole,选则进程就可以了。需要注意的就是在运行jconsole之前,必须要先设置环境变量DISPLAY,否则会报错误,Linux下设置环境变量如下:export DISPLAY=:0.0。(另外还有一个jvisualvm类似功能,只是界面好看一点)
命令格式
SYNOPSIS
jmap [ option ] pid
jmap [ option ] executable core
jmap [ option ] [server-id@]remote-hostname-or-IP

42:AtomicInteger,AtomicIntegerArray,AtomicIntegerFieldUpdate三者之间的差别(并发模块知识)
答:相同点:它们三者的操作都是属于原子性的操作,对于多线程的操作中都是安全的,可以不需要同步方法,因为内容进行了CAS的处理。
(1)AtomicInteger:是对单个Integer类型的变量进行了原子性操作
(2)AtomicIntegerArray:是对Integer类型数组的相关原子性操作
(3)AtomicIntegerFieldUpdate:是对于对象类型中的某个Integer域变量的原子性操作

43:请说说你对于回调的理解
答:所谓回调:就是A类中调用B类中的某个方法C,然后B类中反过来调用A类中的方法D,D这个方法就叫回调方法。

回调的实现步骤:

Class A实现接口CallBack callback——背景1
class A中包含一个class B的引用b ——背景2
class B有一个参数为callback的方法f(CallBack callback) ——背景3
A的对象a调用B的方法 f(CallBack callback) ——A类调用B类的某个方法 C
然后b就可以在f(CallBack callback)方法中调用A的方法 ——B类调用A类的某个方法D
44:请说说对于Java并发编程中的死锁,活锁以及饥饿的含义?(并发模块知识)
死锁:发生在一个线程需要获取多个资源的时候,这时由于两个线程互相等待对方的资源而被阻塞,死锁是最常见的活跃性问题。
“死锁”的例子1:如果线程A锁住了记录R1并等待记录R2,而线程B锁住了记录R2并等待记录R1,这样两个线程A和B就发生了死锁现象。
“死锁”的例子2:两个山羊过一个独木桥,两只羊同时走到桥中间,一个山羊等另一个山羊过去了然后再过桥,另一个山羊等这一个山羊过去,结果两只山羊都堵在中间动弹不得。
饥饿:指的线程无法访问到它需要的资源而不能继续执行时,引发饥饿最常见资源就是CPU时钟周期。虽然在Thread API中由指定线程优先级的机制,但是只能作为操作系统进行线程调度的一个参考,换句话说就是操作系统在进行线程调度是平台无关的,会尽可能提供公平的、活跃性良好的调度,那么即使在程序中指定了线程的优先级,也有可能在操作系统进行调度的时候映射到了同一个优先级。通常情况下,不要区修改线程的优先级,一旦修改程序的行为就会与平台相关,并且会导致饥饿问题的产生。在程序中使用的Thread.yield或者Thread.sleep表明该程序试图客服优先级调整问题,让优先级更低的线程拥有被CPU调度的机会。
实例:资源在其中两个或以上线程或进程相互使用,第三方线程或进程始终得不到。想像一下三个人传球,其中两个人传来传去,第三个人始终得不到
活锁:指的是线程不断重复执行相同的操作,但每次操作的结果都是失败的。尽管这个问题不会阻塞线程,但是程序也无法继续执行。活锁通常发生在处理事务消息的应用程序中,如果不能成功处理这个事务那么事务将回滚整个操作。解决活锁的办法是在每次重复执行的时候引入随机机制,这样由于出现的可能性不同使得程序可以继续执行其他的任务。
实例:个人在一个很宅的胡同里。 一次只能并排过两个人。 两人比较礼貌,都要给对方让路。 结果一起要么让到左边,要么让到右边,结果仍然是谁也过不去。 类似于原地踏步或者震荡状态

45:请说说在多线程编程中,了解过ABA问题吗?(并发模块知识)
ABA问题即是:线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题。
解决办法:AtomicStampedRerence增加一个时间戳,进行CAS操作时不仅需要维护对象值,还需要维护时间戳。对象值和时间戳都必须满足期望值,才能更新新值。

46:为什么说局部变量是线程安全的呢?(JVM模块知识)
答:这个问题就是也是考察对JVM内存模型的了解程度。

      JVM在执行Java程序时,会根据其数据用途把管理的内存划分为若干数据区域,包括方法区,堆,栈(JVM栈、本地方法栈),程序计数器。其中前两者是有所有java线程所共有的,而后两者是每个线程所独有的,因此,栈是线程私有的,一个线程一个栈。并且栈由一系列栈帧组成,栈帧保存了一个方法的局部变量表(包括参数和局部变量)、操作数栈、常量池指针等,每一次方法的调用实际上是创建一个栈帧,并且压栈。
       所以方法的调用实际是栈帧在栈中入栈和出栈的操作。因为栈是线程私有的,所以每个栈之间是独立的,所以栈帧对于多个线程栈来说不存在共享问题,也就不会存在线程安全问题了。

47:请说说你对volatile关键字的理解和工作原理?(Java可见性知识)
答:这个问题其实考察的就是Java中程序指令的原子性,可见性和顺序性以及JVM的内存模型的知识点。
      根据JVM规范的规定,volatile变量依然有工作内存的拷贝,但是由于它特殊的操作顺序规定,所以看起来如同直接在主内存中读写一般,因此这里的描述对于volatile也没有例外。
volatile具体实现细节如下: 
      如果对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀的指令,将这个变量所在工作内存的数据写回到主内存。但是,就算写回到主内存,如果其他线程工作内存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存(缓存中的值来至于工作内存)是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存里。

48:说说关于JVM中四种对象引用的特点和应用场景?(JVM模块知识)
1:强引用
特点:我们平常编码的Object obj = new Object()中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用。当JVM内存不足,JVM宁愿抛出OOM异常,使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,就是可以被垃圾收集的了,具体回收时机还是要看垃圾收集策略。
2:软引用
特点:软引用通过SoftReference类师兄。软引用的生命周期比强引用短一些。只有当JVM认为内存不足时,才会去试图回收软引用指向的对象:即JVM会确保在抛出OOM之前,清理软引用指向的对象。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。
场景:软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉。这样就保证了使用缓存的同时,不会耗尽内存。
3:弱引用
特点:弱引用通过WeakReference类师兄。弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
场景:弱引用同样可用于内存敏感的缓存。
4:虚引用
特点:虚引用也叫做幻想引用,通过PhantomReference类来实现。无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用。来了解呗引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动。
场景:可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知。

49:JVM中的方法区会被进行GC机制的处理吗?(JVM模块知识)
     JVM规范中确实明确说过可以不要求JVM在方法区实现GC,而且在方法区进行GC的“性价比”一般比较低,相对于在堆内存中,尤其是在新生代中,常规应用进行一次GC一般可以回收70% ~ 95%的空间,而方法区(HotSpot中又称为永久代)的GC效率远低于此;但是,此部分内存区域也是可以被回收的。
方法区的垃圾回收主要有两种,分别是对废弃常量的回收和对无用类的回收。
     当一个常量对象不再任何地方被引用的时候,则被标记为废弃常量,这个常量可以被回收。
方法区中的类需要同时满足以下三个条件才能被标记为无用的类:
1:Java堆中不存在该类的任何实例对象
2:加载该类的类加载器已经被回收
3:该类对应的java.lang.Class对象不在任何地方被引用,且无法在任何地方通过反射访问该类的方法
    当满足上述三个条件的类才可以被回收,但是并不是一定会被回收,需要参数进行控制,例如HotSpot虚拟机提供了-Xnoclassgc参数进行控制是否回收。

50:说说对于CAS原理的理解?(并发模块知识)
首先,CAS是无锁处理的一种方式,如下图:

CAS的原理:它包含了3个参数CAS(V,E,N),V表示要更新的值,E表示预期值,N表示新值,当且仅当,V值等于E值时,才会将V的值设置为N,如果V值和E值不同,则说明有其他的线程做了更新,则当前线程就什么都不做,最后,CAS返回当前V的真实值,CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作,当多个线程同时使用CAS操作一个变量时,只有一个线程会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
CAS的CPU指令:对于CAS来说,主要就是通过一条语句来进行实现,即cmpxchg(比较交换命令)。

51:说说对于Java并发包下类的一些理解?(并发模块知识)
答:这个其实我在上面的知识点中,都进行了一些阐述和对比,大家可以根据下面的这个图来进行回顾和复习,这样加深印象。

52:说说锁优化的思路和方法?(并发模块知识)
(1)减少锁持有时间
比如将synchoronized关键字修饰的方法,修改为synchoronized修饰的代码块;
(2)减小锁粒度
主要思路就是将大对象拆成小对象,大大增加并行度,降低锁竞争。比如,我们都知道hashmap是非线程安全的,我们就可以采取用Collections.synchronizedMap(Map<K,V> m)的形式将其变为线程安全,但是这并不是很好的,因为其底层就不过是把所有方法都加了synchoronized关键字修饰处理。而更好的方法就是使用ConcurrentHashMap的形式,因为其内部是通过分段锁来进行的,这样使得锁的范围尽可能小,提供了并行处理。
(3)锁分离
比如,对于读读操作其实是没有影响的,而如果我们对读读都加锁,那么就会影响效率,所以,我们可以利用读写锁的方式来进行优化;
(4)锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是,如果对同一个锁不停的进行请求,同步和释放,其本身也会消耗系统资源,反而不利于性能的优化。比如,在一个循环中,其内部都是要进行同步处理(用synchoronized修饰),其实就可以把同步处理直接加在循环的外层(将synchoronized放在循环外层包裹循环),这样免得多次申请锁资源和释放锁资源。
(5)锁消除
在即时编译器时,如果发现不可能被共享的对象,则可以消除对这些对象的锁操作。因为,本身就不存在共享,反而加锁,这样只会让锁占用资源。
另外的话,在虚拟机内的锁优化存在着偏向锁,轻量级锁,重量级锁和自旋锁(这个在上面知识点23中已经说过)。

53:说说对于CMS垃圾回收器的理解?(JVM模块知识)
      CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者 B/S 系统的服务端上的 Java 应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。
工作流程
       CMS 收集器工作的整个流程分为以下4个步骤: 
初始标记(CMS initial mark)仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要“Stop The World”。----需要停顿
并发标记(CMS concurrent mark):进行 GC Roots追溯所有对象的过程,在整个过程中耗时最长
重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。----需要停顿
并发清除(CMS concurrent sweep)-----不需要停顿
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作;所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
收集特点
(1)CMS 是一款优秀的收集器,它的主要优点在名字上已经体现出来了——并发收集、低停顿,因此 CMS 收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)。
(2)对 CPU 资源非常敏感。其实,面向并发设计的程序都对 CPU 资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说 CPU 资源)而导致应用程序变慢,总吞吐量会降低。CMS 默认启动的回收线程数是(CPU数量+3)/4,也就是当 CPU 在4个以上时,并发回收时垃圾收集线程不少于25%的 CPU 资源,并且随着 CPU 数量的增加而下降。但是当 CPU 不足4个时(比如2个),CMS 对用户程序的影响就可能变得很大,如果本来 CPU 负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。
(3)无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次 Full GC 的产生。由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS 无法再当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此 CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运行使用
(4)标记-清除算法导致的空间碎片。CMS 是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象
应用场景
       当你的应用程序需要有较短的应用程序暂停,而可以接受垃圾收集器与应用程序共享应用程序时,你可以选择 CMS 垃圾收集器。典型情况下,有很多长时间保持 live 状态的数据对象(一个较大的老年代)的应用程序,和运行在多处理上的应用程序,更适合使用 CMS 垃圾收集器。例如 Web 服务器。若应用程序需要有较短的暂停时间的话,也可以考虑 CMS 垃圾收集器。

54:说说JVM中的各种垃圾收集器的特点?(JVM模块知识)


55:Java实现生产者和消费者的方法有哪些?
(1)通过阻塞队列(ArrayBlockingQueue或者LinkedBlockingQueue):因为Java中的阻塞队列实现了阻塞方法,但其实内部也是一种生产者和消费者的实现(通过两个condition和一个reentrantLock实现)。
(2)通过Object对象的wait()和notify()方法:核心原理就是通过并发锁保证仓库集合的原子性,同时通过 wait()/notify() 保证并发协作的有序性
(3)通过Lock锁的condition对象的await()和signal()方法:核心原理类似第(2)种方法
(4)通过 PipedInputStream/PipedOutputStream 管道缓冲流实现,这种方式基于流,所以对于生产消费者的产品需要首先包装成流,所以不易封装,故而实用性不强,
(5)通过并发包下面的CountDownLaunch对象和阻塞队列来实现:这种方式的话,有一个缺点就是只能初始化时候就需要指定固定的生产和消费的个数,因为CountDownLaunch是不能被复用的。

56:请说说你在开发中遇到的OOM的情况和实例以及原因分析?(百度二面题)
情形一:HeapSize  OOM(就是堆溢出)

实例1:
public static void main(Sting[] args){
    List<String> list = new ArrayList<String>();
    while(true){
        list.add("内存溢出呀");
    }
}
原因:代码中只有add操作,而没有remove操作,其实是一种间接的“内存泄露”。由于堆内存中的old区域剩余的内存不够,已经无法满足将要晋升到Old区域的对象大小,所以就会报 java.lang.OutOfMemoryError:java heap space错误。

实例2:
public final static byte[] DEFAULT_BYTES = new byte[12*1024*1024];
public static void main(Sting[] args){
    List<byte[]> temp = new ArrayList<byte[]>();
    while(true){
        temp.add(new byte[1024*1024]);
        if(temp .size() > 3){
            temp.clear();
        }
    }
}
原因:上面的代码只是模拟一下用掉很多Old区域的内存空间,以至于让full GC不断的进行,而当平均FUll GC时间达到一定比例时,就会报错:java.lang.OutOfMemoryError:GC over head limit exceeded错误

情形二:PermGen(永久代) OOM

实例1:
public static void main(Sting[] args){
    int i = 0 ;
    List<String> list = new ArrayList<String>();
    while(true){
        list.add(("内存溢出呀" + i++).intern());
    }
}
原因:因为上面调用了string类的intern()方法,而这个方法是去常量池中寻找是否存在相同的值的字符串(通过equels方法进行比较),如果没有的话,就会在常量池中进行拷贝一份,而不断的通过这样的循环的话,那么常量池就会爆满,以至于会发生java.lang.OutOfMemoryError:PermGen space错误。

实例2:就是一次性加载动态加载过度的class类。(比如通过cglib无限循环的创建代理对象进行调用)

原因:因为类的信息也是存放在永久代中的,而不断的利用动态代理创建的类也是如此,所以也会发现如下的错误java.lang.OutOfMemoryError:PermGen space

情形三:DirecBuffer OOM

DirectBuffer区域:Java的普通的I/O采用输入/输出流的方式实现,输入流都会经历从客户端到直接内存再到JVM的过程,输出流就是反过来的,这中间其实有多次内核与JVM之间的内存拷贝。有些时候为了提高速度,就会想办法利用直接内存。而DirectBuffer区域是java里面的,而这区域不是java的heap,而是C heap的一部分,但是大小有限制,通常在FULL GC会被回收。

实例1:
public static void main(String[] args){
     ByteBuffer.allocateDirect(257*1024*1024);
    }
原因:这也是由于一次性分配的内存超过了其最大的容量,所以就出现了java.lang.OutOFMemoryError:Direct buffer memory 错误。。但是,如果是先分配256M内存,再分配1M内存,就不会出现这个错误,那是由于在分配的时候(看java.nio.Bits类的reserveMemory方法源码),进行了system.gc()操作,而导致有内存被回收,所以不会超过最大容量。

情形四:StackOverflowError

实例1:
public void testStackOverflow(){
    testStackOverflow();
}
原因:因为由于死递归,这样会不断的创建栈帧而导致线程栈无休止的增长,而最后导致java.lang.StackOverflowError错误出现。
注意:死循环并不一定会导致该错误,死循环和死递归是两回事。死循环类似于while(true)的操作,但是它的栈空间使用是不会递增的。而死递归由于需要记录退回的路径,就必须记住递推过程中的方法调用过程,以及每个方法运行过程中的本地变量,这个我们称为上下文信息,随着内容增加,就会占用很多的内存空间,JVM是为了控制它的无限增长,才做了安全监测的处理。

情形五:其他的一些内存溢出现象

1:unable to create new native thread
原因:反复申请了大量的线程并让他们处于运行状态,那么这些线程所占用的Native Memory空间就会很多,如果物理内存不够用,或者操作系统限制了单个进程使用的最大内存,那么在大量线程分配时就有可能抛出该错误。
2:request{}byte for {} out of swap
原因:由于地址空间不够用,因为java堆是初始分配的,如果那个时候操作系统的地址空间不够用,那么JVM是无法启动的,java还会在本地内存区域继续分配。但是这个情况,一般只出现在32位机器,在64位的情况不会出现。
3:IOException:too many open files
原因:由于文件数量打开太多,或者是本地的socket打开大多,而没有关闭。

57:请说说对于JDK中Arrays.sort(Integer i)的排序方法,它里面是如何进行处理的呢?(百度二面)
结构图如下所示:

从上面的图我们可以大致的得到一个的处理过程:
(1)首先,是先检查数组的数组大小,如果其数组大小< QUICKSORT_THRESHOLD(这个常量也就是286),那么就会采用改进版的快排的排序方法;如果数组的数组大小 >=QUICKSORT_THRESHOLD,那么还需要判断其数组的无序程度。
无序程度的话,主要是把数组分成多个小的子数组,然后判断各个子数组中的序列程度(只判断是递增或者递减,这样的都叫有序)。判断各个子数组中有序的有多少。
1:如果判断有序数组个数 >= MAX_RUN_COUNT (这个常量也就是67),那么就会采用改进版的归并排序(就是利用二路归并排序)。
2:如果判断有序数组个数 < MAX_RUN_COUNT ,那么就会采用改进版的快速排序;
(2)上面有两种情况都会采用改进版的快速排序,但是,这里的改进的快速排序还是要继续分情况的。当数组的个数小于<INSERTION _SORT_THRESHOLD ,那么就采用插入排序(这种插入排序也是改进版的,一次性是能够进行排序两个元素的,又叫做双插入排序);如果元素的个数是 >= INSERTION _SORT_THRESHOLD ,那么就会采用改进版的快速排序(这种快速排序也是有进行改进的,它是通过双枢轴快速排序,大体思想就是:如果数据量较大,那么确定数组的5个分位点,选择一个或两个分位点作为“枢轴”,然后根据快速排序的思想进行排序。);
        这上面就说明了java.util.Arrays中的排序算法的源码,这个大家可以跟进去好好看一下,毕竟是大牛们写的,还是非常不错的,可以看出来很多的东西。

58:Java NIO 中关键的技术点是什么?其中的Chanel有哪几种?
(1)Selector:选择器
(2)channel:通道
(3)buffer:缓冲区
其中,Channel有如下几种:
(1)FileChannel:文件读写的通道,但是它并不支持Selector的非阻塞模式。
(2)SocketChannel:网络通信中的TCP通道
(3)ServerSocketChannel:监听网络中新建的TCP链接,通过它调用accept()可创建SocketChannel与客户端正式连接,就像ServerSocket创建Socket一样。
(4)DatagramChannel:从UDP中读写网络包数据

59:在Java中,线程状态有哪些?
线程状态的获取,主要是通过调用线程的Thread.currentThread().getStatue()方法而返回得到一个Thread内部的枚举(为什么是枚举?这就看源码就可以发现)。
(1)NEW状态:表示当前线程是刚创建,但还是处于非运行状态,在等待CPU等相关资源的获取。
(2)RUNNABLE状态:通常是进行调用了start()方法之后,当前已经在运行的线程肯定处于运行状态。但是,注意一点:并不是说返回这个结果就一定表示线程是处于运行状态,也有可能当前的线程是处于阻塞等待的。比如,在BIO模型中,线程正阻塞在网络等待时,看到的线程状态也是RUNNABLE状态,但是在底层中实际是被阻塞的。
(3)BLOCKED状态:表示当前线程是处于阻塞状态,再等待进入临界区。常常表现为一个线程想进入临界区,但是临界区还存在其他的线程在使用,那么这时候该线程就必须等待临界区内的线程释放资源才能够进入。
(4)WAITING状态:通常是指一个线程拥有对象锁后进入到相应的代码区域后,调用相应的“锁对象”的wait()方法操作后产生的一种结果。注意:这个要区分一下和BLOCKED状态。
(5)TIMED_WAITING状态:通常是由于线程调用sleep()方法或者是wait(int timeout)方法超时的时候,返回的一个状态。
(6)TERMINATED状态:通常是指线程运行完run()方法。注意一点:这只是java层面的返回状态,而实际在底层中OS中,该线程可能已经被操作系统给注销了。

60:为什么线程中的wait()和notify()方法必须要使用synchronized进行修饰呢?
(1)一般的话,我们对于这两个方法的调用的时候都是需要把其放在sychronized关键字修饰的地方,可以是修饰的方法或者是代码块。
(2)首先,要明确,wait()和notify()的实现基础是基于对象的存在的。那为什么要基于对象存在呢?既然是要等,就要考虑等什么,这里等待的就是一个对象发出的信号,所以要基于对象而存在。
(3)既然是基于对象的,因此它不得不用一个数据结构来存放这些特征的线程,而且这个数据结构应当是该对象绑定的(看C++代码,发现其实用的是一个双向链表),此时在这个对象上可能同时多个线程调用wait()/notify()方法。所以,为了避免出现并发的问题,就需要进行同步控制。

61:对于volatile关键字,它的作用是什么?
(1)volatile关键字用于修饰变量上,主要是对于可能存在并发操作的变量进行相关的处理
(2)volatil修饰变量的第一个作用在于保证变量时可见性(这个在前面知识点有说)。通过volatile关键字修饰的变量,当有线程进行修改其变量的值之后,是会被更新到主存中的,这样,当其他线程访问同变量的话,就会从主存中去重新读取,从而避免出现数据不一致的发生。
(3)volatile修饰变量的第二个作用在于其可以保证Java程序中的变量的顺序性,防止相关代码出现重排序(这个知识点,前面说了)。
如果是一条对volatils变量进行赋值操作的代码,那么在该代码前面的任何代码不能与这个赋值操作进行交换顺序。
如果是一条读取volatile变量的代码,那么在它后面的操作不允许与它交换顺序,当然其之后的动作之间是可以进行交换顺序的,这是不会发生在它前面进行处理。
(4)volatile修饰变量的第三个作用在于解决了4字节赋值问题(在JVM中允许对一个非volatile的64位(8字节)变量赋值时,分解为两个32位(4字节)来完成,但并不是必须要一次性完成,因为从Java角度来理解,在虚指令中对变量的操作都以slot为单位,每个slot就是4字节。所以,问题就出来了,如果变量时long,double类型的,在赋值某个32位后,正好被其他线程所读取,那么它读取出来的数据就可能是不可预见的错误数据)。对于volatile修饰的变量,必须一次性赋值。(它实现的原理我通过看汇编代码发现,就是它主要就是通过在赋值的时候,在汇编赋值语句前面添加了lock锁)

63:请说说对于偏向锁,轻量级锁,重量级锁的理解。
首先,偏向锁,轻量级锁都是属于乐观锁,而重量级锁时属于悲观锁。
对于理解这个锁的概念,还需要知道当synchronize会在对象的头部打标记,一般就称为mark区。
第一:偏向锁的实现步骤:
(1)一个线程去竞争时,如果没有其他线程征用,则会尝试CAS去修改mark workd 中的一个标记为偏向(mark word单独有1个bit表示是否可偏向,记录锁的位置依然为01),这个CAS的动作同时会修改mark word部分bit以保留线程的ID值(真实的应用场景中线程数是很少的)。
(2)当线程不断发生冲入时,判定就会非常简单,只需要判断头部的线程ID是不是当前线程,若是当前线程,则不用做其他任何的CAS动作了。
(3)如果同一个对象存在另一个线程发起了访问请求,则首先会判定该对象是否已经被锁定。如果已经被锁定,则会将锁修改为轻量级锁(00),也就是锁粒度会上升;而如果没有锁定,则会将对象的是否可偏向的位置设置为不可偏向。
总结:自然的,在后续的操作中,如果锁定了这个对象,就会很少使用轻量级锁,因为轻量级锁的征用上升依然会进入悲观锁。也就是说,偏向锁只是解决了没有任何锁征用的场景,当出现锁征用后它便没什么用途了。
第二:轻量级锁的实现步骤:
(1)在栈当中分配一块空间用来做一份对象头部mark word的拷贝,在mark word中将对象锁的二进制位设置为“未锁定”(在32位的JVM中有2位应用于存储锁标记,未锁定的标记为01),这个动作是方便等到释放锁的时候讲这份数据拷贝到对象头部。
(2)通过CAS尝试将头部的二进制位修改为“线程私有栈对mark区域拷贝存放的地址”,如果成功,则会将最后2位设置为00,代表已经被轻量级锁锁住了。
(3)如果没有成功,则判定对象头部是否已经指向了当前线程所在的栈当中,如果成立则代表当前线程已经是拥有者,可以继续执行。
(4)如果不是拥有者,则说明有多个线程在征用,那么此时会将锁升级为悲观锁,线程进入BLOCKED状态。
第三:重量级锁
其实重量级就理解为悲观锁就好了,其除了拥有锁的线程外,其余的线程都被阻塞。

通过上面三者的分析,可以有一个锁的粗化程度的过程:偏向锁--》轻量级锁---》重量级锁

64:java中的sleep()和wait()方法的区别?
(1)sleep() 不释放同步锁,wait() 释放同步锁。
(2)sleep(milliseconds) 可以用时间指定来使他自动醒过来,如果时间没到则只能调用 interreput() 方法来强行打断(不建议,会抛出 InterruptedException),而 wait() 可以用 notify() 直接唤起。
(3)sleep() 是 Thread 的静态方法,而 wait() 是 Object 的方法。
(4)wait()、notify()、notifyAll() 方法只能在同步控制方法或者同步控制块里面使用,而 sleep() 方法可以在任何地方使用。

65:Java中的Timer类和ScheduledExcutorService之间的区别?
(1)Timer 计时器具备使任务延迟执行以及周期性执行的功能,但是 Timer 天生存在一些缺陷,所以从 JDK 1.5 开始就推荐使用 ScheduledThreadPoolExecutor(ScheduledExecutorService 实现类)作为其替代工具。
(2)首先 Timer 对提交的任务调度是基于绝对时间而不是相对时间的,所以通过其提交的任务对系统时钟的改变是敏感的(譬如提交延迟任务后修改了系统时间会影响其执行);而 ScheduledThreadExecutor 只支持相对时间,对系统时间不敏感。
(3)接着 Timer 的另一个问题是如果 TimerTask 抛出未检查异常则 Timer 将会产生无法预料的行为,因为 Timer 线程并不捕获异常,所以TimerTask 抛出的未检查异常会使 Timer 线程终止,所以后续提交的任务得不到执行;而 ScheduledThreadPoolExecutor 不存在此问题。

66:Java中在IO流中,涉及到哪些设计策略和设计模式?
(1)首先 Java 的 IO 库提供了一种链接(Chaining)机制,可以将一个流处理器跟另一个流处理器首尾相接,以其中之一的输出作为另一个的输入而形成一个流管道链接,譬如常见的 new DataInputStream(new FileInputStream(file)) 就是把 FileInputStream 流当作 DataInputStream 流的管道链接。
(2)其次,对于 Java IO 流还涉及一种对称性的设计策略,其表现为输入输出对称性(如 InputStream 和 OutputStream 的字节输入输出操作,Reader 和 Writer 的字符输入输出操作)和字节字符的对称性(InputStream 和 Reader 的字节字符输入操作,OutputStream 和 Writer 的字节字符输出操作)。
(3)此外,对于 Java IO 流在整体设计上还涉及装饰者(Decorator)和适配器(Adapter)两种设计模式。

对于 IO 流涉及的装饰者设计模式例子如下:

//把InputStreamReader装饰成BufferedReader来成为具备缓冲能力的Reader。
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
对于 IO 流涉及的适配器设计模式例子如下:

//把FileInputStream文件字节流适配成InputStreamReader字符流来操作文件字符串。
FileInputStream fileInput = new FileInputStream(file); 
InputStreamReader inputStreamReader = new InputStreamReader(fileInput);
设计模式的总结:
装饰者模式就是给一个对象增加一些新的功能,而且是动态的,要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例(各种字符流间装饰,各种字节流间装饰)。
适配器模式就是将某个类的接口转换成我们期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题(字符流与字节流间互相适配)。

67:Java中,为什么不能通过方法的返回类型来判断是重载呢?
(1)因为调用时不能指定返回类型信息,编译器不知道你是需要调用具体的哪一个方法。
比如:我用下面的代码来进行说明。

float max(int x ,int y);
int max (int x , int y);
分析:当调用max(1,2)时就无法确定调用的是哪个,单从这一点来说,仅仅通过返回类型不同的重载就是不允许的。
(2)因为我们在调用某个方法的时候,可能出现获取不需要返回类型的情况,那么这样也是不允许返回类型重载的。
比如:同样用下面的代码进行说明。

void f();
int f();
分析:若编译器可以根据上下文明确判断出含义,比如 int x = f()中,那么这样就没问题,但是我们也可能调用一个方法,同时忽略返回值;我们通常把这称为:“它的副作用去调用一个方法”。因为我们关系的不是返回值,而是方法里面执行的逻辑。所以,比如我们进行这样的调用:f();Java怎样判断具体的调用方式呢?而且别人如何识别并理解代码呢?

总结:函数的返回值只是作为函数运行之后的一个“状态”,他是保持方法的调用者与被调用者进行通信的关键。并不能作为某个方法的“标识”。

68:在Java中,能不能用一个char型存储一个汉字?为什么?
在Java中,char类型占2个字节,而且Java默认采用Unicode编码,一个Unicode码是16位,所以一个Unicode码占两个字节,Java中无论汉字还是英文字母都是用Unicode编码来表示的。所以,在Java中,char类型变量可以存储一个中文汉字。
额外补充点知识::使用Unicode意味着字符在JVM内部和外部有不同的表现形式, 在 JVM内部都是Unicode,当这个字符被从JVM内部转移到外时(例如存入文件系统中)。所以Java中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如InputStreamReader和OutputStreamReader,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务。而对于C程序来说,这样必须依赖于union(联合体)共享内存的方式来进行实现。

69:在Java中,对于阻塞队列的方法的理解? 
分析:要注意每个不同的API的区别。


70:在Java中,为什么说hashmap是一种线程非安全的集合?那么会发生什么不安全的问题?原因是什么?
(1)对于第一问,可以对比hashTable以及concurrentHashmap进行对比它们的内在数据结构进行阐述。
(2)不安全的原因,最主要的就是会出现环形结构。
(3)原因:就是就是在进行扩容操作的时候,由于多线程的同时扩容而导致,本有顺序出现混乱,从而使得链表的next节点出现了环形回路的问题。(主要的回答点在于hashmap的结构是通过数组+链表,并且扩容机制是当当前的元素个数大于hashmap的length(开始默认是16,注意不是size,length是总的长度,size是当前个数)* 负载因子(默认是0,。75)时发生,并且是多线程进行同时扩容操作)。

71:在Java中,堆和栈有什么区别?(重大理解,百度二面)
(1)申请方式
栈:由系统自动分配。例如,声明函数中的一个局部变量 int a = 1;系统自动在栈中为a开辟空间。
堆:需要程序员申请,并指明大小。这个对于C语言来说很明显就是用malloc进行,而java中new object()即是分配大小。
(2)申请后的系统反应
栈:只要栈的剩余空间大于所申请的空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录内存地址的链表,当系统收到程序的申请时,会遍历链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动将多余的那部分重新放入空闲链表中。
(3)申请大小的限制
栈:栈是向低地址扩展的数据结构,是一块连续的内存的区域。意思就是栈的地址和栈的最大容量是系统预先定好的,在Windows下,栈的大小一般是2M,如果申请的空间超过剩余空间,那么就会发生StackOverflow异常。因此,栈的空间一般较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是系统用链表来存储的空间地址的,自然是不连续的,而链表的遍历方向是由低向高。堆的大小受限于计算机系统中的有效虚拟内存。因此,堆的空间比较灵活,也比较大。
(4)申请效率的比较
栈:由系统控制,速度较快,但程序员无法控制的。
堆:由new分配的内存,一般速度较慢,而且容易产生内存碎片,不过用起来方便。
(5)堆和栈的存储内容
栈:在函数调用的时候,第一个进栈的是主函数中后的下一条指令地址,然后是函数的各个参数,在大多数C编译器中,参数是由右到左入栈的,然后是函数中的局部变量。注意:静态变量是不会入栈的,是存储在方法区中的。在本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
(6)数据结构
堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第一个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。

72:在Java中,类什么时候会被初始化?
(1)创建类的实例,即进行new的时候
(2)访问某个类或者接口中的静态变量
(3)对某个类或者接口中的静态变量进行赋值操作
(4)调用某个类的静态方法
(5)进行反射
(6)初始化子类(这时候会将父类进行初始化)
(7)JVM启动标明启动类。即文件名和类名相同的那个类

73:Java语言是一种类型安全语言,那么它有哪些机制进行安全机制来保证语言安全性?
(1)类加载机制(双亲委派机制)
(2).class文件检验器
(3)内置于Java虚拟机(及语言)的安全特性。
(4)安全管理器及Java API

74:请说说你对Java中的GC机制的理解?
分析:主要是自己基本是基于下面的知识点进行一步步说的,这样比较有条理。

75:说说死锁和饥饿的区别?
(1)从进程状态考虑,死锁进程都处于等待状态,忙等待(处于运行或就绪状态)的进程并非处于等待状态,但却可能被饿死;
(2)死锁进程等待永远不会被释放的资源,饿死进程等待会被释放但却不会分配给自己的资源,表现为等待时限没有上界(排队等待或忙式等待);
(3)死锁一定发生了循环等待,而饿死则不然。这也表明通过资源分配图可以检测死锁存在与否,但却不能检测是否有进程饿死;
(4)死锁一定涉及多个进程,而饥饿或被饿死的进程可能只有一个;
(5)在饥饿的情形下,系统中有至少一个进程能正常运行,只是饥饿进程得不到执行机会,而死锁则可能会最终使整个系统陷入死锁并崩溃;

76:将JVM中的堆内存的最小值和最大值设置为一样有什么好处?
好处:(1)最小值的设置方式为:-Xms,最大值的设置为:-Xmx。JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的堆内存由-Xmx指定,默认是物理内存的1/4。
(2)默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;
(3)空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。
(4)因此服务器一般设置-Xms、-Xmx 相等以避免在每次GC 后调整堆的大小。
说明:如果-Xmx 不指定或者指定偏小,应用可能会导致java.lang.OutOfMemory错误,此错误来自JVM,不是Throwable的,无法用try...catch捕捉。

77:说收对于GC中的STW机制的理解。
(1)STW:也就是Stop The World ,它的含义就是停止其他的线程(让其他线程保持静止状态),而此时只运行GC线程。
(2)出现的原因:当虚拟机完成两次标记后,便确认了可以回收的对象。但是,垃圾回收并不会阻塞我们程序的线程,他是与当前程序并发执行的。所以问题就出在这里,当GC线程标记好了一个对象的时候,此时我们程序的线程又将该对象重新加入了“关系网”中,当执行二次标记的时候,该对象也没有重写finalize()方法,因此回收的时候就会回收这个不该回收的对象。
(3)解决方式:为了解决(2)中的问题,所以出现了STW机制。也就是说,当GC进行回收的时候,它会在某些特定的指令位置设置“安全点”进行停止当前运行的线程,这时候就会重新根据 “GC ROOT”进行重新组建回收对象,进而进行执行标记和清除的GC操作。
(4)STW的时机:
    1:循环的末尾
    2:方法临返回前/调用方法的call指令后
    3:可能抛出异常的位置

78:经典的问题
关于锁的实现知识点:
(1)java中有哪些锁?
回答:独享锁/共享锁,可重入锁,乐观锁,偏向锁,轻量级锁,重量级锁,读写锁
(2)Synchronized的实现原理是什么?
(回答关键点:Monitor锁 ,偏向锁,轻量级锁和重量级锁)
(3)ReentrantLock的实现原理是什么?
回答关键点:
1.AQS 
2.unsafe中的CAS修改states变量,CAS的原理以及缺点(ABA问题),解决的话就采用AtomicStampedReference这个原子引用类添加个时间戳
3.FIFO队列中的具体内容(每个节点都是一个node,有前驱和后继节点))
关于concurrentHashmap的点。
(1)实现原理
回答:sengment ,lock,分段锁
(2)1.7和1.8的区别
回答点:添加了红黑树以及采用node和CAS机制取代segment
(3)红黑树的特点
回答;特点以及发生左旋和右旋的情况
(4)对于concurrentHashmap中的size的处理方式
回答点:采用了volitile变量修饰变量(而不是用的并发原子类IntegerAtomic,因为这个性能并不好)

79:Java实现生产者和消费者模型的方法?(Java)
https://mp.weixin.qq.com/s/HX-IUgsVfXbYFhguSTZOFg

和https://blog.csdn.net/jingsuwen1/article/details/52056940

80:常见的设计模式的理解?
(1)单例模式:上面的知识点有说明
(2)代理模式:https://mp.weixin.qq.com/s/K5fH9aNsL06nQ8VPMYTCgw
(3)装饰模式:https://mp.weixin.qq.com/s/Ap6DoW2n-RcMgbxoJDk4DA
(4)观察者模式:https://mp.weixin.qq.com/s/BtvfaRl8n3ckMWLz5kB47A
(5)策略模式:https://mp.weixin.qq.com/s/xvt3TF5IsBRSfr4NgKMvJA
(6)工厂模式:这个比较好理解(对比下策略模式)

81:为什么线程中的sleep()方法是静态的?(类比wait()方法之间的区别)
(1)因为sleep操作的是当前线程,使当前线程进行短暂性休眠(不释放获得的锁),所以,针对的是类而不是实例
(2)为了安全,因为如果是实例方法,那么就能够获取实例的引用,那么线程就会产生使其他的线程进行睡眠的操作,这样能够避免混乱
注意一点:sleep()之后的线程是处于线程的等待状态的,当sleep时间到之后,会根据CPU的调度算法进行选择线程的执行,所以,sleep方法会存在一定的延迟。

82:请说说UTF-8和GBK的区别(贝壳找房,二面)
(1)UTF-8:UnicodeTransformationFormat-8bit,允许含BOM,但通常不含BOM。是用以解决国际上字符的一种多字节编码,它对英文使用8位(即一个字节),中文使用24为(三个字节)来编码。
(2)UTF-8包含全世界所有国家需要用到的字符,是国际编码,通用性强。UTF-8编码的文字可以在各国支持UTF8字符集的浏览器上显示。如,如果是UTF8编码,则在外国人的英文IE上也能显示中文,他们无需下载IE的中文语言支持包。
(3)GBK是国家标准GB2312基础上扩容后兼容GB2312的标准。GBK的文字编码是用双字节来表示的,即不论中、英文字符均使用双字节来表示,为了区分中文,将其最高位都设定成1。
(4)GBK包含全部中文字符,是国家编码,通用性比UTF8差,不过UTF8占用的数据库比GBD大。
(5)GBK、GB2312等与UTF8之间都必须通过Unicode编码才能相互转换

83:Hashmap底层是通过数组+链表的结构,请问为什么hashmap的时间复杂度为O(1)?(37互娱二面)
(1)当hashmap的各个链表中,不存在着hash冲突,也就是每个链表只有一个元素节点,那么时间复杂度为O(1)
(2)hashmap的时间复杂度O(1)是最理想的情况,而当出现了hash冲突的时候,就必然会存在着遍历链表的O(n)复杂度
(3)综合上述,如果想让hashmap的时间复杂度为O(1),有两种理想的情况
    第一:hash算法要非常的好,因为主要还是通过对Key进行的hash来找数组索引
    第二:要么给予足够的空间容量,使得不同的元素可以对应不同的数组索引位置

84:读写锁实现读读共享,读写互斥,写读互斥的详细情况(37互娱二面)
https://blog.csdn.net/yanyan19880509/article/details/52435135

85:手写一个阻塞队列(贝壳找房二面)
https://blog.csdn.net/h525483481/article/details/80347485

86:Java序列化对象中都有一个UUID,它的作用是什么?不同的UUID又有什么作用?(贝壳找房二面)
作用:
(1)决定是否能够成功序列化,因为序列化机制是通过在运行时判断类的UID来验证版本一致性;
(2)如果类中没有定义一个序列化ID,那么Java序列化机制会根据编译时的class自动生成一个UID作为序列化版本的比较。只有同一次编译生成的class才会生成相同的serialVersionUID。如果后续该类进行了修改而添加了字段内容,所以就会导致进行序列化失败。
不同的UUID的作用:
进行不同版本的类的兼容性控制。如果不想之前序列化的类兼容当前系统中的类,那么就可以将UID进行修改为不同的值。如果需要保证兼容性,那么UID就应该保持一样。

87:多线程程序相关的问题(苏宁一面+58到家二面)
(1)情况一:两个线程,进行访问一个类的不同的方法,会发生资源竞争阻塞吗?

答案:不会

(2)情况二:两个线程,进行访问一个类的不同的synchronized方法,会发生资源竞争阻塞吗?

答案:会

(3)情况三:两个线程,一个访问一个类的synchoronized方法,一个访问非synchoronized方法,会发生资源竞争阻塞吗?

答案:不会

(4)情况四:两个线程,进行访问一个类的static修饰的synchoronized方法,会发生资源竞争阻塞吗?

答案:会

(5)情况五:两个线程,一个访问static修饰的synchoronized方法,一个访问static的非synchoronized方法,会发生资源竞争吗?

答案:不会

分析:(1)因为对于同一个类来说,不同的synchoronized方法都是获取当前的对象实例,所以,某一个时刻只能由一个线程进行访问。(2)对于同一个类来说,不同的static修饰的synchoronized方法都是对于当前的类对象,而JVM中只存在一个类模板,所以,某一个时刻也只能由一个线程进行访问。

public class Test {
    public static void main(String[] args){
        A a = new A();
        Thread a1 = new Thread(new Runnable() {
            @Override
            public void run() {
                a.computeFirst();
            }
        } , "a1");
 
        Thread a2 = new Thread(new Runnable() {
            @Override
            public void run() {
                a.computeSecond();
            }
        }, "a2");
        a1.start();
        a2.start();
        System.out.println("main");
    }
}
 
 
class A{
    public    void computeFirst(){
        int i = 5;
        while( i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
 
            } catch (InterruptedException ie) {
 
            }
        }
    }
 
    public   void computeSecond(){
        int i = 5;
        while( i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
 
            } catch (InterruptedException ie) {
 
            }
        }
    }
}
88:线程中sleep()方法和yeild()方法的区别
1.sleep()方法会给其他线程运行的机会,而不管其他线程的优先级,因此会给较低优先级的线程运行的机会;yeild()方法只会给优先
  级相同的或者比自己高的线程运行的机会.

2.sleep()方法声明抛出InterruptionException异常,而yeild()方法没有声明抛出任何异常.

3.sleep()方法比yeild()方法具有更高的可移植性.

4.sleep()方法使线程进入阻塞状态,而yeild()方法使线程进入就绪状态.

5.sleep可以指定睡眠的时间,而yeild不行。也就是说yeild调用之后,很可能马上又会回到运行状态。
注意一点:只有在线程启动前(即调用start()方法前),才能把它设置成后台线程.如果线程启动后,再调用这个线程的setDaemon()方法,则会抛出异常.

89:说说Java中的内存屏障(三七互娱一面)
https://blog.csdn.net/bjo2008cn/article/details/53900445

90:请说说对于并发包中的LockSupport的理解(区别:Object对象中的wait()和notify()机制)
https://blog.csdn.net/secsf/article/details/78560013​​​​​​​

91:请实现一个LRU的cache
https://blog.csdn.net/pingnanlee/article/details/40585941

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值