Java基础中的面试题(二),你能接几招
-
线程池的构造方法中有哪些参数,代表什么含义
我们在使用线程池的时候经常会通过 Executors.newFixedThreadPool(), 或者 Executors.newCachedThreadPool() 这样的方法, 通过查看源码的方式,可以看出底层都是调用 new ThreadPoolExecutor(), 来实现的,并且在阿里巴巴的代码规范中,要求我们创建线程池的时候,要通过ThreadPoolExecutor的构造方法来实现,那么我们接下来就来看下他的构造方法
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {}以上给出了源码中该类的方法描述。接下来逐个介绍一下:
corePoolSize: 核心线程数,当线程池中的线程数目达到corePoolSize后,就会把达到的任务放到缓存队列中
maxmumPoolSize: 线程池中允许的最大线程数
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。
unit: 参数keepAliveTime的时间单位,有七种。 天,小时,分钟,秒,毫秒,微秒,纳秒
workQueue: 一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程差生重大影响。一般来说这里的阻塞队列有几种选择:ArrayBlockingQueue, LinkedBlockingQueue,SynchronousQueue
threadFactory: 线程工厂,主要用来创建线程,自带一个默认的线程工厂
handler:拒绝策略,有以下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务任务缓存队列及任务排队策略:
前面我们讲到当线程池超过corePoolSize时就会放入到缓存队列中那么下面我们就来说说任务的缓存队列。
1、ArrayBlockingQueue:基于数组的先进先出,创建时必须指定大小,超出直接corePoolSize个任务,则加入到该队列中,只能加该queue设置的大小,其余的任务则创建线程,直到(corePoolSize+新建线程)> maximumPoolSize。
2、LinkedBlockingQueue:基于链表的先进先出,无界队列。超出直接corePoolSize个任务,则加入到该队列中,直到资源耗尽。
3、synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
-
如何实现有返回值的多线程
一般我们使用多线程的时候就习惯使用Runnable, 实现Runnable接口后,要求实现里边 public void run(){}方法,这个方法明显是没有返回值的, 所以无法实现有返回值的多线程。要想实现又返回值的多线程我们应该使用Callable接口。下面通过代码给出一个简单的实现
public class Test{ public static void main(String[] args){ // 使用线程池调用Callable并获取返回值 ExecutorService pool = Executors.newFixedThreadPool(10); Future f = pool.submit(new Task()); String result = f.get(); } } // 有返回值的多线程任务 class Task<String> implements Callable<String>{ public String call(){ // do sth return "success"; } } -
如何对List进行排序
可以通过Collections.sort(List); list.sort(); 此种排序要求List中的元素必须实现Comparable接口,使其具备排序能力。重写compareTo方法,定义排序规则。
还有一种方式Collections.sort(Comparator c), list.sort(Comparator); 通过传入一个外部比较器,覆盖原来的排序内容,主要用于类已经无法继承Comparable, 或者想换一种实现方式,对于Comparable里边的实现方式不满意
接下来我们还是通过案例,来举例。 分别 用两种方式实现对Person集合中的元素按年龄升序和降序
public class Test{ public static void main(String[] args){ List<Person> list = new ArrayList<>(); list.add(new Person(15,"zhangsan")); list.add(new Person(18,"lisi")); list.add(new Person(14,"wangwu")); list.add(new Person(12,"zhaoliu")); // 方式1, 让Person类实现Comparable接口,重写compareTo方法,使用Collections.sort()排序 // 需改变类结构,实现一个接口 Collectios.sort(list);// 该方法只能对list排序 // 或者使用list调用 list.sort(); // 方式二,通过Comparator接口,一般用于不改变类结构,类可以不用实现Comparable接口 Collections.sort(list, new Comparator<Person>(){ public int compare(Person p1, Person p2){ return p2.age - p1.age; } }); // 或者list直接调用 list.sort(new Comparator<Person>(){ public int compare(Person p1, Person p2){ return p2.age - p1.age; } }); } } @Data class Person implements Comparable<Person>{ private int age; private String name; public Person(int age, String name){ this.age = age; this.name = name; } public int compareTo(Person p){ // 升序: 降序用p.age-this.age; return this.age - p.age; } } -
JVM如何加载一个类并介绍双亲委派模型
类加载的过程:加载,验证(验证阶段作用是保证Class文件的字节流包含的信息符合JVM规范,不会给JVM造成危害),准备(准备阶段是为变量分配内存并设置类变量的初始化), 解析(解析过程是将常量池内的符号引用替换成直接引用)、初始化。
双亲委派模型: 双亲委派是如果一个类加载器收到了一个类加载的请求,不会自己先尝试加载,先去找父类加载器去完成,当顶层启动类加载器表示无法加载这个类的时候,子类才会尝试自己去加载。当回到最初的发起者加载器还无法加载时,并不会向下找,而是抛出ClassNotFound异常。
-
JVM中的结构
jvm中包含栈,堆,方法区,程序计数器,本地方法栈
-
栈:
java栈中只保存基本数据类型和自定义对象的引用,注意只是对象的引用,而不是对象本身,对象本身保存在堆中。像String,Integer,等包装类是存放于堆中的。栈是先进后出类型的,栈内的数据在超出其作用域后,会被自动释放掉,不由JVM GC管理。每一个线程都包含一个栈区,每个栈中的数据都是私有的,其他栈不能访问。每个线程都会创建一个操作栈,每个栈又包含了若干个栈帧,每个栈帧对应着每个方法的每次调用,每个栈帧包含了三个部分:
局部变量区(方法内基本类型变量、变量对象指针),操作数栈区(存放方法执行过程中产生的中间结果),运行环境区(动态连接、正确的方法返回相关信息、异常捕捉)
-
本地方法栈
本地方法栈的功能和jvm栈得非常类似,用于存储本地方法的局部变量表,本地方法的操作数栈等信息。本地方法栈是在程序调用或jvm调用本地方法接口(native)时候启用。本地方法都不是使用java语言编写的,比如使用C语言编写的本地方法,本地方法也不由jvm去运行,所有本地方法的运行不受jvm的管理。hotspot vm将本地方法栈和jvm栈合并了。
-
堆
线程共享。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。从结构上来分,可以分为新生代和老年代。而新生代又可以分为Eden 空间、From Survivor 空间(s0)、To Survivor 空间(s1)。 所有新生成的对象首先都是放在新生代的。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制过来的对象,而复制到老年代的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。
当java创建一个类的对象或者数组时,都在堆中为新的对象分配内存,虚拟机中只有一个堆,程序中所有的线程都共享它。堆占用的控件是最多的,堆的存取类型为管道类型,先进先出。在程序运行中,可以动态分配堆内存的大小。
-Xms设置堆的最小空间大小。-Xmx设置堆的最大空间大小。-XX:NewSize设置新生代最小空间大小。-XX:MaxNewSize设置新生代最小空间大小。
-
程序计数器
在jvm概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支,循环,跳转 执行的是一个native方法,那么程序计数器的值则为空(undefined). 程序计数器是唯一一个在jvm规范中没有规定任何oom的区域。
-
方法区
在jvm中,类型信息和类静态变量都保存在方法区中,类型信息是由类加载器在类加载的过程中从类文件中提取出来的,需要注意一点的是,常量池也存放于方法区中。
程序中所有的线程共享一个方法区,所以访问方法区的信息必须确保线程是安全的。如果有两个线程同时去加载一个类,那么只能有一个线程被允许去加载这个类,另一个必须等待。
方法区也是可以被垃圾回收,但是条件肺炎严苛,必须在该类没有任何引用的情况下。
类型信息包括:类型全名,类型的父类型全名,接口还是类,类型修饰符,父接口全名列表,类型的字段信息,类型的方法信息,所有的静态变量,指向类加载器的引用,指向Class的引用,基本类型常量池
1.8以后叫元空间。
-
-
简述java中的强引用,弱引用,软引用和虚引用
从jdk1.2开始,对象的引用别划分为4中级别,从而使程序能够更加灵活地控制对象的生命周期,这4中级别从高到底分别为: 强引用,软引用,弱引用和虚引用
强引用:之前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。比如下面这段代码中的object和str都是强引用。如果一个对象具有强引用,那就类似于必不可少的物品,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
比如: Object o = new Object();
软引用:软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
比如: SoftReference sr = new SoftReference(obj);弱引用:弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。所以被软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。
比如: WeakReference sr = new WeakReference(new String(“hello world”));虚引用: 虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收的活动
虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
ReferenceQueue queue = new ReferenceQueue();
PhantomReference pr = new PhantomReference(new String(“hello”), queue);
System.out.println(pr.get()); -
什么情况下会产生死锁
死锁发生的四个必要条件是: 1.资源互斥使用。 2.多个进程保持一定的资源,但又请求新的资源。 3.资源不可被剥夺。 4.多个进程循环等待。 一般死锁的应对策略有: 1.死锁预防。如进程需要的所有资源,在一开始就全部申请好得到之后再开始执行。 2.死锁避免。如进程每次申请申请资源的时候,根据一定的算法,去看该请求可能不可能造成死锁,如果可能,就不给它分配该资源。 3.死锁处理。破坏四个必要条件的其中一个,比如kill掉一个进程。 4.死锁忽略。不管死锁,由用户自行处理,比如重启电脑。一般的系统其实都采取这种策略。
-
同步和异步的区别
同步和异步最大的区别就在于。一个需要等待,一个不需要等待。同步可以避免出现死锁,读脏数据的发生,一般共享某一资源的时候用,如果每个人都有修改权限,同时修改一个文件,有可能使一个人读取另一个人已经删除的内容,就会出错,同步就会按顺序来修改。
同步:从时间上强调处理事情的结果,强调结果意味着对结果的迫不及待,不管结果如何,反正你要立即给我一个结果响应,一直处于等待状态。
异步:调用者发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时调用者在等待结果过程中浪费时间是极其难受的,这个时候我们可以处理其他的请求,被调用者通常依靠事件、回调等机制来通知调用者其返回结果。
-
介绍UDP协议和TCP协议的区别
TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接。 一个TCP连接必须要经过三次“对话”才能建立起来,其中的过程非常复杂, 只简单的描述下这三次对话的简单过程:
1)主机A向主机B发出连接请求数据包:“我想给你发数据,可以吗?”,这是第一次对话;
2)主机B向主机A发送同意连接和要求同步 (同步就是两台主机一个在发送,一个在接收,协调工作)的数据包 :“可以,你什么时候发?”,这是第二次对话;
3)主机A再发出一个数据包确认主机B的要求同步:“我现在就发,你接着吧!”, 这是第三次对话。
三次“对话”的目的是使数据包的发送和接收同步, 经过三次“对话”之后,主机A才向主机B正式发送数据。
UDP(User Data Protocol,用户数据报协议)
1、UDP是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、 计算机的能力和传输带宽的限制; 在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
2、 由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等, 因此一台服务机可同时向多个客户机传输相同的消息。
3、UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包的额外开销很小。
4、吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、 源端和终端主机性能的限制。
5、UDP使用尽最大努力交付,即不保证可靠交付, 因此主机不需要维持复杂的链接状态表(这里面有许多参数)。
6、UDP是面向报文的。发送方的UDP对应用程序交下来的报文, 在添加首部后就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界, 因此,应用程序需要选择合适的报文大小。
总结:
1、基于连接与无连接;
2、对系统资源的要求(TCP较多,UDP少);
3、UDP程序结构较简单;
4、流模式与数据报模式 ;
5、TCP保证数据正确性,UDP可能丢包;
6、TCP保证数据顺序,UDP不保证。
- BIO, NIO 的区别
BIO (同步阻塞I/O模式)
数据的读取写入必须阻塞在一个线程内等待其完成。
这里使用那个经典的烧开水例子,这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。
NIO(同步非阻塞)
同时支持阻塞与非阻塞模式,但这里我们以其同步非阻塞I/O模式来说明,那么什么叫做同步非阻塞?如果还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。
AIO (异步非阻塞I/O模型)
异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。
- & 和 && 的区别
& 可以作为逻辑运算符和位运算符使用,作为逻辑运算符是,前后连接布尔值,或最终结果为布尔值的表达式,该运算符不会短路,就是即使 & 前面的表达式结果已经为false,& 后面的表达式依旧会执行。
& 作为位运算符,一般两个数字,计算的时候需要将两个数组转为二进制,逐位进行运算,两个都是1结果是1, 有一个是0结果为0, 最后再将计算后的二进制结果转成10进制。
&&:逻辑运算符, 短路, 和& 唯一的差别就是,如果前面的结果已经可以确定最后的结果,&&后面的表达式不再执行。 需注意 && 不能做为位运算符连接数字
- short s1 = 1; s1 = s1 + 1;有什么错? short s1 = 1; s1 += 1;有什么错
这是一道关于类型转换的经典面试题。 第一个写法是错误的, 分析 等号右边 s1+ 1 一个short类型和一个int类型相加,结果是int类型,把一个int类型赋值给等号左边的short类型是会报错误的。 因为short有可能装不下。 第二种方式是正确的。 S1 += 1; 这个+= 自带了一个强制类型转换的功能。相当于 s1 = (short)(s1 +1);
- error和 exception的区别
Error 和 Exception都是 Throwable的子类.
Error(错误)是系统中的错误,程序员是不能改变的和处理的,是在程序编译时出现的错误,只能通过修改程序才能修正。一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。
Exception(异常)表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。
Exception又分为两类
CheckedException:(编译时异常) 需要用try——catch显示的捕获,对于可恢复的异常使用CheckedException。
UnCheckedException(RuntimeException):(运行时异常)不需要捕获,对于程序错误(不可恢复)的异常使用RuntimeException。
-
介绍常见的垃圾回收算法
-
根搜索算法(可达性分析): 从GCROOT开始,寻找对应的引用节点,找到这个节点后,继续寻找这个节点的引用节点。当所有的引用节点寻找完毕后,剩余的节点则被认为是没有被引用到的节点,及无用的节点。目前java中可以作为GCroot的对象有: 虚拟机栈中引用的对象(本地变量表),方法区中静态属性引用的对象,方法区中常量引用的对象,本地方法栈中引用的对象(native)
-
标记-清除算法:
标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,在扫描整个空间中未标记的对象进行直接回收。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活的对象比较多的情况下极为高效,但是由于标记-清除算法直接回收不存活的对象,并没有对存活的对象进行整理,因此会导致内存碎片。
-
复制算法:
复制算法将内存划分为两个区间,使用此算法时,所有的动态分配的对象都只能分配在其中一个区间,而另一个区间是闲置的。复制算法采用从根集合扫描,将存活对象复制到空闲区间,当扫描完毕活动区间后,会将活动区间一次性全部回收,此时原本的空闲区间变成了活动区间,下次gc的时候会重复刚才的操作,以此循环。复制算法在存活对象较少的时候,极为高效,但是带来的成本是牺牲一半的内存空间用于对象的移动,所以复制算法使用的场景,必须是对象的存活率非常低才行。
-
标记-整理算法:
标记-整理算法采用和标记-清除算法一样的方式进行对象的标记,清除,但是在回收不存活对象占用的空间后,会见给所有的存活的对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题,
JVM 为了优化内存得回收,是用来分代回收的方式,对于新生代的内存回收,主要采用复制算法,而对于老年代的回收,大多采用标记整理算法。

被折叠的 条评论
为什么被折叠?



