阅读 778

就很哇塞,3万字Java面试题总结(2021版)

前言:

在经历了这个没钱、没假期的11月份,3天假期的元旦,我们来到了新年2021。经历了金九银十,经历了双十一,有的人跳槽成功有的人在备战金三银四,所以该为下一年做准备了!想在面试、工作中脱颖而出?想在最短的时间内快速掌握Java 的核心基础知识点?想要成为一位优秀的 Java 工程师?这个2021年面试题总结今天就分享给大家。

在这里插入图片描述

另外本人整理收藏了20年多家公司面试知识点整理 ,以及各种Java核心知识点免费分享给大家,我认为对面试来说是非常有用的,想要资料的话请点795983544进群 备注掘金。

1.Java 自动装箱与拆箱

装箱就是自动将基本数据类型转换为包装器类型(int-->Integer);调用方法:Integer 的 valueOf(int) 方法

拆箱就是自动将包装器类型转换为基本数据类型(Integer-->int)。调用方法:Integer 的 intValue 方法

在 Java SE5 之前,如果要生成一个数值为 10 的 Integer 对象,必须这样进行:

Integer i = new Integer(10);
复制代码

而在从 Java SE5 开始就提供了自动装箱的特性,如果要生成一个数值为 10 的 Integer 对象,只需要这

样就可以了:

Integer i = 10;
复制代码

2.重载和重写的区别

重写(Override)

从字面上看,重写就是 重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。

public class Father {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Son s = new Son();
        s.sayHello();
    }
    public void sayHello() {
        System.out.println("Hello");
    }
}
class Son extends Father{
    @Override
    public void sayHello() {
        // TODO Auto-generated method stub
        System.out.println("hello by ");
    }
}
复制代码

重写 总结:

(1)发生在父类与子类之间

(2)方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同

(3)访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)

(4)重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常

重载(Overload)

在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)

则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来

判断重载。

public static void main(String[] args) {
    // TODO Auto-generated method stub
    Father s = new Father();
    s.sayHello();
    s.sayHello("wintershii");
}
public void sayHello() {
    System.out.println("Hello");
}
public void sayHello(String name) {
    System.out.println("Hello" + " " + name);
}
}
复制代码

重载 总结:

(1)重载 Overload 是一个类中多态性的一种表现

(2)重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序)

(3)重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准

3.equals 与==的区别

== :

== 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。

(1)比较的是操作符两端的操作数是否是同一个对象。

(2)两边的操作数必须是同一类型的(可以是父子类之间)才能编译通过。

(3)比较的是地址,如果是具体的阿拉伯数字的比较,值相等则为 true,如:

int a=10 与 long b=10L 与 double c=10.0 都是相同的(为 true),因为他们都指向地址为 10 的堆。

equals:

equals 用来比较的是两个对象的内容是否相等,由于所有的类都是继承自 java.lang.Object 类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是 Object 类中的方法,而 Object 中的 equals 方法返回的却是==的判断。

总结:

所有比较是否相等时,都是用 equals 并且在对常量相比较时,把常量写在前面,因为使用 object 的 equals object 可能为 null 则空指针在阿里的代码规范中只使用 equals ,阿里插件默认会识别,并可以快速修改,推荐安装阿里插件来排查老代码使用“==”,替换成 equals

4. Hashcode 的作用

java 的集合有两类,一类是 List,还有一类是 Set。前者有序可重复,后者无序不重复。当我们在 set 中插入的时候怎么判断是否已经存在该元素呢,可以通过 equals 方法。但是如果元素太多,用这样的方法就会比较满。

于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储的那个区域。

hashCode 方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当集合要添加新的元素时,先调用这个元素的 hashCode 方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的 equals 方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。这样一来实际调用 equals 方法的次数就大大降低了,几乎只需要一两次。

5.String、String StringBuffer 和 StringBuilder 的区别是什么?

String 是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一个final 类型的字符数组,所引用的字符串不能被改变,一经定义,无法再增删改。每次对 String 的操作都会生成新的 String 对象。

private final char value[];
复制代码

每次+操作 : 隐式在堆上 new 了一个跟原字符串相同的 StringBuilder 对象,再调用 append 方法 拼接+后面的字符。

StringBuffer 和 StringBuilder 他们两都继承了 AbstractStringBuilder 抽象类,从 AbstractStringBuilder 抽象类中我们可以看到。

/**
* The value is used for character storage.
*/
char[] value;
复制代码

他们的底层都是可变的字符数组,所以在进行频繁的字符串操作时,建议使用 StringBuffer 和 StringBuilder 来进行操作。 另外 StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

6.ArrayList 和 linkedList 的区别

Array(数组)是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的。

Array 获取数据的时间复杂度是 O(1),但是要删除数据却是开销很大,因为这需要重排数组中的所有数据,(因为删除数据以后, 需要把后面所有的数据前移)

缺点: 数组初始化必须指定初始化的长度, 否则报错

例如:

int[] a = new int[4];
//推介使用 int[] 这种方式初始化
int c[] = {23,43,56,78};
//长度:4,索引范围:[0,3]
复制代码

List—是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式,它继承 Collection。

List 有两个重要的实现类:ArrayList 和 LinkedList

ArrayList: 可以看作是能够自动增长容量的数组

ArrayList 的 toArray 方法返回一个数组

ArrayList 的 asList 方法返回一个列表

ArrayList 底层的实现是 Array, 数组扩容实现

LinkList 是一个双链表,在添加和删除元素时具有比 ArrayList 更好的性能.但在 get 与 set 方面弱于

ArrayList.当然,这些对比都是指数据量很大或者操作很频繁。

7.HashMap 和 HashTable 的区别

(1)两者父类不同

HashMap 是继承自 AbstractMap 类,而 Hashtable 是继承自 Dictionary 类。不过它们都实现了同时实现了 map、Cloneable(可复制)、Serializable(可序列化)这三个接口。

(2)对外提供的接口不同

Hashtable 比 HashMap 多提供了 elments() 和 contains() 两个方法。

elments() 方法继承自 Hashtable 的父类 Dictionnary。elements() 方法用于返回此 Hashtable 中的 value 的枚举。

contains()方法判断该 Hashtable 是否包含传入的 value。它的作用与 containsValue()一致。事实上,contansValue() 就只是调用了一下 contains() 方法。

(3)对 null 的支持不同

Hashtable:key 和 value 都不能为 null。

HashMap:key 可以为 null,但是这样的 key 只能有一个,因为必须保证 key 的唯一性;可以有多个 key 值对应的 value 为 null。

(4)安全性不同

HashMap 是线程不安全的,在多线程并发的环境下,可能会产生死锁等问题,因此需要开发人员自己处理多线程的安全问题。

Hashtable 是线程安全的,它的每个方法上都有 synchronized 关键字,因此可直接用于多线程中。虽然 HashMap 是线程不安全的,但是它的效率远远高于 Hashtable,这样设计是合理的,因为大部分的使用场景都是单线程。当需要多线程操作的时候可以使用线程安全的 ConcurrentHashMap。

ConcurrentHashMap 虽然也是线程安全的,但是它的效率比 Hashtable 要高好多倍。因为

ConcurrentHashMap 使用了分段锁,并不对整个数据进行锁定。

(5)计算 hash 值的方法不同

8.Collection 包结构,与 Collections 的区别

Collection 是集合类的上级接口,子接口有 Set、List、LinkedList、ArrayList、Vector、Stack、Set;Collections 是集合类的一个帮助类, 它包含有各种有关集合操作的静态多态方法,用于实现对各种集合的搜索、排序、线程安全化等操作。此类不能实例化,就像一个工具类,服务于 Java 的 Collection 框架。

9.Java 的四种引用,强弱软虚

强引用

强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用方式:

String str = new String("str");
复制代码

软引用

软引用在程序内存不足时,会被回收,使用方式:

// 注意:wrf 这个引用也是强引用,它是指向 SoftReference 这个对象的,
// 这里的软引用指的是指向 new String("str")的引用,也就是 SoftReference 类中 T
SoftReference<String> wrf = new SoftReference<String>(new String("str"));
复制代码

可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM 就会回收早先创建的对象。

弱引用

弱引用就是只要 JVM 垃圾回收器发现了它,就会将之回收,使用方式:

WeakReference<String> wrf = new WeakReference<String>(str);
复制代码

可用场景: Java 源码中的 java.util.WeakHashMap 中的 key 就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM 会自动帮我处理它,这样我就不需要做其它操作。

虚引用

虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue 中。注意哦,其它引用是被 JVM 回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有 ReferenceQueue,使用

例子:

PhantomReference<String> prf = new PhantomReference<String>(new
String("str"), new ReferenceQueue<>());
复制代码

可用场景: 对象销毁前的一些操作,比如说资源释放等。** Object.finalize()虽然也可以做这类动作,但是这个方式即不安全又低效。

10.a=a+b 与 a+=b 有什么区别吗?

操作符会进行隐式自动类型转换,此处 a+=b 隐式的将加操作的结果类型强制转换为持有结果的类型,

byte a = 127;
byte b = 127;
b = a + b;
// 报编译错误:cannot convert from int to byte
b += a;
复制代码

以下代码是否有错,有的话怎么改?

short s1= 1;
s1 = s1 + 1;
复制代码

有错误.short 类型在进行运算时会自动提升为 int 类型,也就是说 s1+1 的运算结果是 int 类型,而 s1 是 short 类型,此时编译器会报错.

正确写法:

short s1= 1;
s1 += 1;
复制代码

+=操作符会对右边的表达式结果强转匹配左边的数据类型,所以没错.

11.try catch finally,try 里有 return,finally 还执行么?

执行,并且finally 的执行早于 try 里面的 return

结论:

(1)不管有木有出现异常,finally 块中代码都会执行;

(2)当 try 和 catch 中有 return 时,finally 仍然会执行;

(3)finally 是在 return 后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally 中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally 执行前确定的;

(4)finally 中最好不要包含 return,否则程序会提前退出,返回值不是 try 或 catch 中保存的返回值。、

12.Java 线程实现/创建方式

继承 Thread 类

Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。

 public class MyThread extends Thread { 
         public void run() { 
             System.out.println("MyThread.run()"); 
         } 
    } 
        MyThread myThread1 = new MyThread(); 
        myThread1.start();
复制代码

实现 Runnable 接口

如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个Runnable 接口。

public class MyThread extends OtherClass implements Runnable { 
        public void run() { 
             System.out.println("MyThread.run()"); 
         } 
    } 
    //启动 MyThread
    MyThread myThread = new MyThread(); 
    Thread thread = new Thread(myThread); 
    thread.start(); 
    target.run()
    public void run() { 
     if (target != null) { 
     target.run(); 
     } 
    }
复制代码

13.线程池原理

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。主要特点为:线程复用;控制最大并发数;管理线程。

线程复用 一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run 方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写 Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。

线程池的组成 一般的线程池主要分为以下 4 个组成部分:

(1)线程池管理器:用于创建并管理线程池。 (2)工作线程:线程池中的线程。 (3)任务接口:每个任务必须实现的接口,用于工作线程调度其运行。 (4)任务队列:用于存放待处理的任务,提供一种缓冲机制。

Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。

ThreadPoolExecutor 的构造方法如下:

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime,
            TimeUnit unit, BlockingQueue<Runnable> workQueue) {
            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), defaultHandler);
复制代码

corePoolSize:指定了线程池中的线程数量。

maximumPoolSize:指定了线程池中的最大线程数量。

keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多次时间内会被销毁。

unit:keepAliveTime 的单位。

workQueue:任务队列,被提交但尚未被执行的任务。

threadFactory:线程工厂,用于创建线程,一般用默认的即可。

handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。

拒绝策略 线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。

JDK 内置的拒绝策略如下:

AbortPolicy : 直接抛出异常,阻止系统正常运行。

CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。

DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。

DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。

以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。

Java 线程池工作过程 (1)线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

(2)当调用 execute() 方法添加一个任务时,线程池会做如下判断:

a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务; b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列; c) 如果这时候队列满了,而且正在运行的线程数量小maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务; d) 如果队列满了,而且正在运行的线程数量大于或等maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。

(3)当一个线程完成任务时,它会从队列中取下一个任务来执行。

(4)当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

14.Java 常用算法

1. 快速排序算法 快速排序的原理:选择一个关键值作为基准值。比基准值小的都在左边序列(一般是无序的),比基准值大的都在右边(一般是无序的)。一般选择序列的第一个元素。一次循环:从后往前比较,用基准值和最后一个值比较,如果比基准值小的交换位置,如果没有继续比较下一个,直到找到第一个比基准值小的值才交换。找到这个值之后,又从前往后开始比较,如果有比基准值大的,交换位置,如果没有继续比较下一个,直到找到第一个比基准值大的值才交换。直到从前往后的比较索引>从后往前比较的索引,结束第一次循环,此时,对于基准值来说,左右两边就是有序的了。

 public void sort(int[] a,int low,int high){
         int start = low;
         int end = high;
         int key = a[low]; 
         while(end>start){
         //从后往前比较
         while(end>start&&a[end]>=key) 
        //如果没有比关键值小的,比较下一个,直到有比关键值小的交换位置,然后又从前往后比较
         end--;
         if(a[end]<=key){
             int temp = a[end];
             a[end] = a[start];
             a[start] = temp;
         }
         //从前往后比较
         while(end>start&&a[start]<=key)
        //如果没有比关键值大的,比较下一个,直到有比关键值大的交换位置
         start++;
         if(a[start]>=key){
             int temp = a[start];
             a[start] = a[end];
             a[end] = temp;
         }
         //此时第一次循环比较结束,关键值的位置已经确定了。左边的值都比关键值小,右边的值都比关键值大,但是两边的顺序还有可能是不一样的,进行下面的递归调用
     }
         //递归
        if(start>low) sort(a,low,start-1);//左边序列。第一个索引位置到关键值索引-1
         if(end<high) sort(a,end+1,high);//右边序列。从关键值索引+1 到最后一个
         }
 }
复制代码

2 .冒泡排序算法 (1)比较前后相邻的二个数据,如果前面数据大于后面的数据,就将这二个数据交换。

(2)这样对数组的第 0 个数据到 N-1 个数据进行一次遍历后,最大的一个数据就“沉”到数组第N-1 个位置。

(3)N=N-1,如果 N 不为 0 就重复前面二步,否则排序完成。

public static void bubbleSort1(int [] a, int n){
         int i, j;
         for(i=0; i<n; i++){//表示 n 次排序过程。
             for(j=1; j<n-i; j++){
                 if(a[j-1] > a[j]){//前面的数字大于后面的数字就交换
                //交换 a[j-1]和 a[j]
                int temp;
                temp = a[j-1];
                a[j-1] = a[j];
                a[j]=temp;
                }
            }
         }
    }
复制代码

15.Spring Beans

什么是Spring beans?

Spring beans 是那些形成Spring应用的主干的java对象。它们被Spring IOC容器初始化,装配,和管理。这些beans通过容器中配置的元数据创建。比如,以XML文件中 的形式定义。

一个 Spring Bean 定义 包含什么?

一个Spring Bean 的定义包含容器必知的所有配置元数据,包括如何创建一个bean,它的生命周期详情及它的依赖。

如何给Spring 容器提供配置元数据?Spring有几种配置方式

这里有三种重要的方法给Spring 容器提供配置元数据。

XML配置文件。 基于注解的配置。 基于java的配置。

Spring配置文件包含了哪些信息

Spring配置文件是个XML 文件,这个文件包含了类信息,描述了如何配置它们,以及如何相互调用。

Spring基于xml注入bean的几种方式

Set方法注入; 构造器注入:①通过index设置参数的位置;②通过type设置参数类型; 静态工厂注入; 实例工厂;

16.什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?

Java 虚拟机是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件。

Java 被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。

Java 虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。

17.JDK和JRE的区别是什么?

JDK: java开发工具包,包含了JRE、编译器和其它工具(如:javaDOc、java调试器)

JRE: java运行环境,包含java虚拟机和java程序所需的核心类库。

如果只是想跑java程序,那么只需安装JRE,如果要写java程序并且运行,那就需要JDK了。

18."static"关键字是什么意思?Java中是否可以覆盖一个private或者是static的方法?

如果一个类的变量或者方法前面有static修饰,那么表明这个方法或者变量属于这个类,也就是说可以在不创建对象的情况下直接使用

当父类的方法被private修饰时,表明该方法为父类私有,对其他任何类都是不可见的,因此如果子类定了一个与父类一样的方法,这对于子类来说相当于是一个新的私有方法,且如果要进行向上转型,然后去调用该“覆盖方法”,会产生编译错误

class Parent {
    private fun() {
        ...
    }    
}
class Child extends Parent {
    private fun() {
        ...
    }
}
class Test {
    public static void main(String[] args) {
        Parent c = new Child();
        c.fun(); //编译出错
    }
}
复制代码

static方法时编译时静态绑定的,属于类,而覆盖是运行时动态绑定的(动态绑定的多态),因此不能覆盖.

19.Java支持的基本数据类型有哪些?什么是自动拆装箱?

java支持的基本数据类型有以下9种:byte,shot,int,long,float,double,char,boolean,void.

自动拆装箱是java从jdk1.5引用,目的是将原始类型自动的装换为相对应的对象,也可以逆向进行,即拆箱。这也体现java中一切皆对象的宗旨。

所谓自动装箱就是将原始类型自动的转换为对应的对象,而拆箱就是将对象类型转换为基本类型。java中的自动拆装箱通常发生在变量赋值的过程中,如:

    Integer object = 3; //自动装箱
    int o = object; //拆箱
复制代码

在java中,应该注意自动拆装箱,因为有时可能因为java自动装箱机制,而导致创建了许多对象,对于内存小的平台会造成压力。

20. 覆盖和重载是什么?

覆盖也叫重写,发生在子类与父类之间,表示子类中的方法可以与父类中的某个方法的名称和参数完全相同,通过子类创建的实例对象调用这个方法时,将调用子类中的定义方法,这相当于把父类中定义的那个完全相同的方法给覆盖了,这也是面向对象编程的多态性的一种表现。

重载是指在一个类中,可以有多个相同名称的方法,但是他们的参数列表的个数或类型不同,当调用该方法时,根据传递的参数类型调用对应参数列表的方法。当参数列表相同但返回值不同时,将会出现编译错误,这并不是重载,因为jvm无法根据返回值类型来判断应该调用哪个方法。

21.Java支持多继承么?如果不支持,如何实现?

在java中是单继承的,也就是说一个类只能继承一个父类。

java中实现多继承有两种方式,一是接口,而是内部类.

//实现多个接口 如果两个接口的变量相同 那么在调用该变量的时候 编译出错
interface interface1 {
    static String field = "dd"; 
    public void fun1();
    }
interface interface2 {
static String field = "dddd";
    public void fun2();
    }
class child implements interface1,interface2 {
    static String field = "dddd";
    @Override
    public void fun2() {
    }

    @Override
    public void fun1() {
    }    
}

//内部类 间接多继承
class Child {
class Father {
    private void strong() {
        System.out.println("父类");
    }
}
class Mother {
    public void getCute() {
        System.out.println("母亲");
    }
}
public void getStrong() {
    Father f = new Father();
    f.strong();
    }
public void getCute() {
    Mother m = new Mother();
    m.getCute();
    }
}
复制代码

22.什么是值传递和引用传递?java中是值传递还是引用传递,还是都有?

值传递 就是在方法调用的时候,实参是将自己的一份拷贝赋给形参,在方法内,对该参数值的修改不影响原来实参,常见的例子就是刚开始学习c语言的时候那个交换方法的例子了。

引用传递 是在方法调用的时候,实参将自己的地址传递给形参,此时方法内对该参数值的改变,就是对该实参的实际操作。

在java中只有一种传递方式,那就是值传递.可能比较让人迷惑的就是java中的对象传递时,对形参的改变依然会影响到该对象的内容。

下面这个例子来说明java中是值传递.

    public class Test {
    public static void main(String[] args) {
        StringBuffer sb = new StringBuffer("hello ");
        getString(sb);
        System.out.println(sb);
    }
    public static void getString(StringBuffer s) {
        //s = new StringBuffer("ha");
        s.append("world");
    }
}
复制代码

在上面这个例子中,当前输出结果为:hello world。这并没有什么问题,可能就是大家平常所理解的引用传递,那么当然会改变StringBuffer的内容。但是如果把上面的注释去掉,那么就会输出:hello.此时sb的值并没有变成ha hello. 假如说是引用传递的话,那么形参的s也就是sb的地址,此时在方法里new StringBuffer(),并将该对象赋给s,也就是说s现在指向了这个新创建的对象.按照引用传递的说法,此时对s的改变就是对sb的操作,也就是说sb应该也指向新创建的对象,那么输出的结果应该为ha world.但实际上输出的仅是hello.这说明sb指向的还是原来的对象,而形参s指向的才是创建的对象,这也就验证了java中的对象传递也是值传递。

23.接口和抽象类的区别是什么?

不同点在于:

  • 接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
  • 类可以实现很多个接口,但是只能继承一个抽象类
  • 类如果要实现一个接口,它必须要实现接口声明的所有方法。但是,类可以不实现抽象类声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。
  • 抽象类可以在不提供接口方法实现的情况下实现接口。
  • Java 接口中声明的变量默认都是 final 的。抽象类可以包含非 final 的变量。
  • Java 接口中的成员函数默认是 public 的。抽象类的成员函数可以是 private,protected 或者是 public 。
  • 接口是绝对抽象的,不可以被实例化(java 8已支持在接口中实现默认的方法)。抽象类也不可以被实例化,但是,如果它包含 main 方法的话是可以被调用的。

24.构造器(constructor)是否可被重写(override)?

构造方法是不能被子类重写的,但是构造方法可以重载,也就是说一个类可以有多个构造方法。

25.Math.round(11.5) 等于多少? Math.round(-11.5)等于多少?

Math.round(11.5)= =12 Math.round(-11.5)==-11 round 方法返回与参数 最接近的长整数,参数加 1/2 后求其 floor.

26. String, StringBuffer StringBuilder的区别。

tring 的长度是不可变的;

StringBuffer的长度是可变的,如果你对字符串中的内容经常进行操作,特别是内容要修改时,那么使用 StringBuffer,如果最后需要 >String,那么使用 StringBuffer 的 toString() 方法;线程安全;

StringBuilder 是从 JDK 5 开始,为StringBuffer该类补充了一个单个线程使用的等价类;通常应该优先使用 StringBuilder 类,因>为它支持所有相同的操作,但由于它不执行同步,所以速度更快。 使用字符串的时候要特别小心,如果对一个字符串要经常改变的话,就一定不要用String,否则会创建许多无用的对象出来.

来看一下比较

String s = "hello"+"world"+"i love you";
StringBuffer Sb = new StringBuilder("hello").append("world").append("i love you");
复制代码

这个时候s有多个字符串进行拼接,按理来说会有多个对象产生,但是jvm会对此进行一个优化,也就是说只创建了一个对象,此时它的执行速度要比StringBuffer拼接快.再看下面这个:

String s2 = "hello";  
String s3 = "world";  
String s4 = "i love you";  
String s1 = s2 + s3 + s4;
复制代码

上面这种情况,就会多创建出来三个对象,造成了内存空间的浪费.

27.JVM内存分哪几个区,每个区的作用是什么?

java虚拟机主要分为以下一个区:

1.方法区:

  • 有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类型的卸载
  • 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。
  • 该区域是被线程共享的。
  • 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。

2.虚拟机栈:

  • 虚拟机栈也就是我们平常所称的栈内存,它为java方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。

  • 虚拟机栈是线程私有的,它的生命周期与线程相同。

  • 局部变量表里存储的是基本数据类型、returnAddress类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定

  • 操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式

  • 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。

3.本地方法栈

本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务。

4.堆

java堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。

5.程序计数器

内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个java虚拟机规范没有规定任何OOM情况的区域。

28.如和判断一个对象是否存活?(或者GC对象的判定方法)

判断一个对象是否存活有两种方法:

1.引用计数法

所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.

引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。 2.可达性算法(引用链法)

该算法的思想是:从一个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。

在java中可以作为GC Roots的对象有以下几种:

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

虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象并不一定会被回收。当一个对象不可达GC Root时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记

如果对象在可达性分析中没有与GC Root的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者已被虚拟机调用过,那么就认为是没必要的。

如果该对象有必要执行finalize()方法,那么这个对象将会放在一个称为F-Queue的对队列中,虚拟机会触发一个Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果finalize()执行缓慢或者发生了死锁,那么就会造成F-Queue队列一直等待,造成了内存回收系统的崩溃。GC对处于F-Queue中的对象进行第二次被标记,这时,该对象将被移除"即将回收"集合,等待回收。

29.简述java垃圾回收机制?

在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

30.java中垃圾收集的方法有哪些?

1.标记-清除:

这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:1.效率不高,标记和清除的效率都很低;2.会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作。

2.复制算法:

为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一般的内存。

于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分,较大那份内存交Eden区,其余是两块较小的内存区叫Survior区。每次都会优先使用Eden区,若Eden区满,就将对象复制到第二块内存区上,然后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对象通过分配担保机制复制到老年代中。(java堆又分为新生代和老年代)

3.标记-整理

该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。

分代收集

现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。

31.java内存模型

java内存模型(JMM)是线程间通信的控制机制.JMM定义了主内存和线程之间抽象关系。线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:

在这里插入图片描述

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

  • 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  • 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

32.java类加载过程?

java类加载需要经历一下7个过程:

加载

加载时类加载的第一个过程,在这个阶段,将完成一下三件事情:

1.通过一个类的全限定名获取该类的二进制流。 2.将该二进制流中的静态存储结构转化为方法去运行时数据结构。 3.在内存中生成该类的Class对象,作为该类的数据访问入口。

验证

验证的目的是为了确保Class文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成以下四钟验证:

1.文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型. 2.元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。 3.字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。 4.符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。

准备

准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

    public static int value=123;//在准备阶段value初始值为0 。在初始化阶段才会变为123 。
复制代码

解析

该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。

初始化

初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

33. 简述java类加载机制?

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。

34. 类加载器双亲委派模型机制?

当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。

35.什么是类加载器,类加载器有哪些?

实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。

主要有一下四种类加载器:

1.启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。 2.扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。 3.系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。 4.用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。

36.简述java内存分配与回收策率以及Minor GC和Major GC

  1. 对象优先在堆的Eden区分配。
  2. 大对象直接进入老年代.
  3. 长期存活的对象将直接进入老年代.

当Eden区没有足够的空间进行分配时,虚拟机会执行一次Minor GC.Minor Gc通常发生在新生代的Eden区,在这个区的对象生存期短,往往发生Gc的频率较高,回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,但是通过配置,可以在Full GC之前进行一次Minor GC这样可以加快老年代的回收速度。

37.HashMap的工作原理是什么?

HashMap内部是通过一个数组实现的,只是这个数组比较特殊,数组里存储的元素是一个Entry实体(jdk 8为Node),这个Entry实体主要包含key、value以及一个指向自身的next指针。HashMap是基于hashing实现的,当我们进行put操作时,根据传递的key值得到它的hashcode,然后再用这个hashcode与数组的长度进行模运算,得到一个int值,就是Entry要存储在数组的位置(下标);当通过get方法获取指定key的值时,会根据这个key算出它的hash值(数组下标),根据这个hash值获取数组下标对应的Entry,然后判断Entry里的key,hash值或者通过equals()比较是否与要查找的相同,如果相同,返回value,否则的话,遍历该链表(有可能就只有一个Entry,此时直接返回null),直到找到为止,否则返回null。

HashMap之所以在每个数组元素存储的是一个链表,是为了解决hash冲突问题,当两个对象的hash值相等时,那么一个位置肯定是放不下两个值的,于是hashmap采用链表来解决这种冲突,hash值相等的两个元素会形成一个链表。

38.HashMap与HashTable的区别是什么?

  1. HashTable基于Dictionary类,而HashMap是基于AbstractMap。Dictionary是任何可将键映射到相应值的类的抽象父类,而AbstractMap是基于Map接口的实现,它以最大限度地减少实现此接口所需的工作。(在java 8中我查看源码发现Hashtable并没有继承Dictionary,而且里面也没有同步方法,是不是java 8中Hashtable不在同步的了?有没有人解释一下?)

  2. HashMap的key和value都允许为null,而Hashtable的key和value都不允许为null。HashMap遇到key为null的时候,调用putForNullKey方法进行处理,而对value没有处理;Hashtable遇到null,直接返回NullPointerException。

  3. Hashtable是同步的,而HashMap是非同步的,但是我们也可以通过Collections.synchronizedMap(hashMap),使其实现同步。

39.CorrentHashMap的工作原理?

jdk 1.6版: ConcurrenHashMap可以说是HashMap的升级版,ConcurrentHashMap是线程安全的,但是与Hashtablea相比,实现线程安全的方式不同。Hashtable是通过对hash表结构进行锁定,是阻塞式的,当一个线程占有这个锁时,其他线程必须阻塞等待其释放锁。ConcurrentHashMap是采用分离锁的方式,它并没有对整个hash表进行锁定,而是局部锁定,也就是说当一个线程占有这个局部锁时,不影响其他线程对hash表其他地方的访问。

具体实现:ConcurrentHashMap内部有一个Segment<K,V>数组,该Segment对象可以充当锁。Segment对象内部有一个HashEntry<K,V>数组,于是每个Segment可以守护若干个桶(HashEntry),每个桶又有可能是一个HashEntry连接起来的链表,存储发生碰撞的元素。

每个ConcurrentHashMap在默认并发级下会创建包含16个Segment对象的数组,每个数组有若干个桶,当我们进行put方法时,通过hash方法对key进行计算,得到hash值,找到对应的segment,然后对该segment进行加锁,然后调用segment的put方法进行存储操作,此时其他线程就不能访问当前的segment,但可以访问其他的segment对象,不会发生阻塞等待。

jdk 1.8版 在jdk 8中,ConcurrentHashMap不再使用Segment分离锁,而是采用一种乐观锁CAS算法来实现同步问题,但其底层还是“数组+链表->红黑树”的实现。

40.遍历一个List有哪些不同的方式?

    List<String> strList = new ArrayList<>();
    //for-each
    for(String str:strList) {
        System.out.print(str);
    }

    //use iterator 尽量使用这种 更安全(fail-fast)
    Iterator<String> it = strList.iterator();
    while(it.hasNext) {
        System.out.printf(it.next());
    }
复制代码

41.fail-fast与fail-safe有什么区别?

Iterator的fail-fast属性与当前的集合共同起作用,因此它不会受到集合中任何改动的影响。Java.util包中的所有集合类都被设计为fail->fast的,而java.util.concurrent中的集合类都为fail-safe的。当检测到正在遍历的集合的结构被改变时,Fail-fast迭代器抛出ConcurrentModificationException,而fail-safe迭代器从不抛出ConcurrentModificationException。

42.Array和ArrayList有何区别?什么时候更适合用Array?

  • Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
  • Array是指定大小的,而ArrayList大小是固定的

43.哪些集合类提供对元素的随机访问?

ArrayList、HashMap、TreeMap和HashTable类提供对元素的随机访问。

44.HashSet的底层实现是什么?

通过看源码知道HashSet的实现是依赖于HashMap的,HashSet的值都是存储在HashMap中的。在HashSet的构造法中会初始化一个HashMap对象,HashSet不允许值重复,因此,HashSet的值是作为HashMap的key存储在HashMap中的,当存储的值已经存在时返回false。

45.LinkedHashMap的实现原理?

LinkedHashMap也是基于HashMap实现的,不同的是它定义了一个Entry header,这个header不是放在Table里,它是额外独立出来的。LinkedHashMap通过继承hashMap中的Entry,并添加两个属性Entry before,after,和header结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。LinkedHashMap定义了排序模式accessOrder,该属性为boolean型变量,对于访问顺序,为true;对于插入顺序,则为false。一般情况下,不必指定排序模式,其迭代顺序即为默认为插入顺序。

46.LinkedList和ArrayList的区别是什么?

  • ArrayList是基于数组实现,LinkedList是基于链表实现
  • ArrayList在查找时速度快,LinkedList在插入与删除时更具优势

47.什么是线程?进程和线程的关系是什么?

线程可定义为进程内的一个执行单位,或者定义为进程内的一个可调度实体。 在具有多线程机制的操作系统中,处理机调度的基本单位不是进程而是线程。一个进程可以有多个线程,而且至少有一个可执行线程。

打个比喻:进程好比工厂(计算机)里的车间,一个工厂里有多个车间(进程)在运转,每个车间里有多个工人(线程)在协同工作,这些工人就可以理解为线程。

线程和进程的关系:

  1. 线程是进程的一个组成部分.
  2. 进程的多个线程都在进程地址空间活动.
  3. 系统资源是分配给进程的,线程需要资源时,系统从进程的资源里分配给线程.
  4. 处理机调度的基本单位是线程.

48.Thread 类中的start() 和 run() 方法有什么区别?

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。

49.什么是线程安全?

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为。

线程安全的核心是“正确性”,也就是说当多个线程访问某个类时,能够得到预期的结果,那么就是线程安全的。

50.Java中有哪几种锁?

自旋锁: 自旋锁在JDK1.6之后就默认开启了。基于之前的观察,共享数据的锁定状态只会持续很短的时间,为了这一小段时间而去挂起和恢复线程有点浪费,所以这里就做了一个处理,让后面请求锁的那个线程在稍等一会,但是不放弃处理器的执行时间,看看持有锁的线程能否快速释放。为了让线程等待,所以需要让线程执行一个忙循环也就是自旋操作。

在jdk6之后,引入了自适应的自旋锁,也就是等待的时间不再固定了,而是由上一次在同一个锁上的自旋时间及锁的拥有者状态来决定

偏向锁: 在JDK1.之后引入的一项锁优化,目的是消除数据在无竞争情况下的同步原语。进一步提升程序的运行性能。 偏向锁就是偏心的偏,意思是这个锁会偏向第一个获得他的线程,如果接下来的执行过程中,改锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。偏向锁可以提高带有同步但无竞争的程序性能,也就是说他并不一定总是对程序运行有利,如果程序中大多数的锁都是被多个不同的线程访问,那偏向模式就是多余的,在具体问题具体分析的前提下,可以考虑是否使用偏向锁。

轻量级锁: 为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率,下文会详细分析

51.synchronized内置锁

java中以synchronize的形式,为防止资源冲突提供了内置支持。当任务要执行被synchronize关键字保护的代码段时,它将检查锁是否可用,然后获取锁——执行代码——释放锁。

所有对象都自动含有单一的锁。当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法,但可以访问非synchronized方法。因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。

当在某个线程中执行这段代码块,该线程会获取对象synObject的锁,从而使得其他线程无法同时访问该代码块。synObject可以是this,代表获取当前对象的锁,也可以是类中的一个属性,代表获取该属性的锁。

针对每一个类,也有一个锁,所以static synchronize 方法可以在类的范围内防止对static数据的并发访问。如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。

对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。

52.ThreadLocal理解

ThreadLocal是一个创建线程局部变量的类。通常情况下我们创建的变量,可以被多个线程访问并修改,通过ThreadLocal创建的变量只能被当前线程访问。

ThreadLocal内部实现

ThreadLocal提供了set和get方法. set方法会先获取当前线程,然后用当前线程作为句柄,获取ThreadLocaMap对象,并判断该对象是否为空,如果为空则创建一个,并设置值,不为空则直接设置值。

    public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    }
复制代码

ThreadLocal的值是放入了当前线程的一个ThreadLocalMap实例中,所以只能在本线程中访问,其他线程无法访问。

ThreadLocal并不会导致内存泄露,因为ThreadLocalMap中的key存储的是ThreadLocal实例的弱引用,因此如果应用使用了线程池,即便之前的线程实例处理完之后出于复用的目的依然存活,也不会产生内存泄露。

53.为什么wait, notify 和 notifyAll这些方法不在thread类里面?

这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在Object类里是有意义的,还有不把它放在Thread类里的原因。一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

最后:

针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。

下面的面试题答案都整理成文档笔记。也还整理了一些面试资料&最新2020收集的一些大厂的面试真题(都整理成文档,小部分截图),有需要的可以点击进入暗号掘金

在这里插入图片描述

文章分类
后端
文章标签