Java最新面试问答整理
Q:JDK和JRE区别?
JDK是整个JAVA的核心,包括了Java运行环境JRE,一堆Java工具和Java基 础的类库。通过JDK开发人员将源码文件(java文件)编译成字节码文件(class文 件)。JRE是Java运行环境,不含开发环境,即没有编译器和调试器。将class文件 加载到内存准备运行。
Q:final关键字,抽象类可以使用final修饰吗?
1.用来修饰数据,包括成员变量和局部变量,该变量只能被赋值一次且它的 值无法被改变。对于成员变量来讲,必须在声明时或者构造方法中对它赋值; 2.修饰方法,表示该方法无法被重写; 3.修饰类,表示该类无法被继承。抽象类是被用于继承的,final修饰代表不可修改、不可继承的。所以不能用 final修饰抽象类。
Q:JAVA容器
ArrayList底层数组实现,封装了常见的增删改查操作,并且支持动态扩容。适 合查找多的场合。LinkedList基于链表实现的列表。适合增删情况较多的场合。TreeSet,基于二叉排序树(红黑树)实现的。TreeSet里最典型的就是它用到 了两种排序方式,即基于元素对象自身的实现的Comparable接口的自然排序, 以及基于更为灵活不与单个元素绑定的Comparator接口的客户化排序。自己在 构造的时候传入一个比较器即可。HashMap是用来存储键值对的映射关系,底层是用数组+链表实现的。结合put 操作讲一下。HashSet其实就是基于HashMap实现的,只不过将值固定为一个固定的值。LinkedHashMap,支持按照插入顺序排序。PriorityQueue优先级队列,一个基于优先级堆的无界优先级队列
Q:线程安全体现在哪些方面?JAVA怎么保证线程安全?锁在项目中具体怎么使用?
一、线程安全在三个方面体现 1.原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized); 2.可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile); 3.有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。 二、如何保证线程安全 1.保证原子性: 锁和同步 常用的保证Java操作原子性的工具是锁和同步方法(或者同步代码块)。使 用锁,可以保证同一时间只有一个线程能拿到锁,也就保证了同一时间只有一个 线程能执行申请锁和释放锁之间的代码。与锁类似的是同步方法或者同步代码块。使用非静态同步方法时,锁住的是 当前实例;使用静态同步方法时,锁住的是该类的Class对象;使用静态代码块 时,锁住的是synchronized关键字后面括号内的对象。无论使用锁还是synchronized,本质都是一样,通过锁来实现资源的排它 性,从而实际目标代码段同一时间只会被一个线程执行,进而保证了目标代码段 的原子性。这是一种以牺牲性能为代价的方法。 2.保证可见性: Java提供了volatile关键字来保证可见性。由于JMM是基于共享内存实现线 程通信的,所以会存在缓存一致性的问题。当使用volatile修饰某个变量时,它 会保证对该变量的修改会立即被更新到内存中,并且将其它缓存中对该变量的缓 存设置成无效,因此其它线程需要读取该值时必须从主内存中读取,从而得到最 新的值。 3.保证有序性: 编译器和处理器对指令进行重新排序时,会保证重新排序后的执行结果和代 码顺序执行的结果一致,所以重新排序过程并不会影响单线程程序的执行,却可 能影响多线程程序并发执行的正确性。Java中可通过volatile在一定程序上保证顺序性,另外还可以通过 synchronized和锁来保证顺序性。synchronized和锁保证顺序性的原理和保证原子性一样,都是通过保证同 一时间只会有一个线程执行目标代码段来实现的。除了从应用层面保证目标代码段执行的顺序性外,JVM还通过被称为 happens-before原则隐式地保证顺序性。两个操作的执行顺序只要可以通过 happens-before推导出来,则JVM会保证其顺序性,反之JVM对其顺序性不作 任何保证,可对其进行任意必要的重新排序以获取高效率。 4.其它保证线程安全的方法: (1)尽可能避免引起非线程安全的条件——共享变量。如果能从设计上避免 共享变量的使用,即可避免非线程安全的发生,也就无须通过锁或者 synchronized以及volatile解决原子性、可见性和顺序性的问题。 (2)不可变对象 可以使用final修饰的对象保证线程安全,由于final修饰的引用型变量(除String外)不 可变是指引用不可变,但其指向的对象是可变的,所以此类必须安全发布,即不能对外提供 可以修改final对象的接口。
Q:JAVA怎么避免死锁?
参考:http://ifeve.com/deadlock-prevention/ 导致死锁的原因: 1、加锁顺序 当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。 2、加锁时限 另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在 尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的 时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机 的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应 用在没有获得锁的时候可以继续运行(译者注:加锁超时后可以先继续运行干点其它事情, 再回头来重复之前加锁的逻辑)。 3、死锁检测 死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记 下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。那么当检测出死锁时,这些线程该做些什么呢?一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的 加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者 注:原因同超时类似,不能从根本上减轻竞争)。一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就 像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同 一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。 顺便复习一下操作系统死锁: 死锁预防:限制申请方式; 互斥:原来独占的资源变得共享,可能会造成程序不确定性。 占用并等待:必须保证当一个进程请求一个资源的时候,它不持有任何其他资源。(要么全部拿到,要么一点也不占有)它开始执行之前需要进程请求并分配其所有的资源,允许进程请求资源当且仅当进程没有占有任何资源的时候资源利用率低,可能发生饥饿无抢占如果进程占有某些资源,并请求其他不能被立即分配的资源,则释放当前正占有的资源,被抢占资源添加到资源列表中,只有当它能够获得旧的资源以及它请求的新的资源,进程可以得到执行。 循环等待:对所有资源类型进行排序,并要求每个进程按照资源的顺序进行申请 死锁避免:银行家算法,如果发现分配了资源之后就可能死锁,就不分配资源了。 死锁检测:允许进入死锁状态,主要是通过检测算法看看是否产生了死锁,然后让相应线程进行回滚。 死锁恢复:杀死所有进程,或者根据优先级杀死部分进程,从而解除死锁。
Q:ThreadLocal具体怎么使用?使用在什么场景?
参考: (1)https://www.cnblogs.com/ldq2016/p/9041856.html (2)https://www.jianshu.com/p/98b68c97df9b 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供 独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其 它线程所对应的副本。从线程的角度看,目标变量就象是线程的本地变量,这也是类名 中“Local”所要表达的意思。ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思 路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本, Map中元素的键为线程对象,而值对应线程的变量副本。ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每 一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因 为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。个人理解:每一个ThreadLocal内部有一个静态内部 类:ThreadLocalMap,Map里面存储线程本地线程对象(key)和线程的变量副 本(value)但是,Thread内部的Map是由ThreadLocal维护的,由 ThreadLocal负责向map获取和设置线程的变量值。所以对于不同的线程,每次 获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离, 互不干扰。 使用场景:还记得Hibernate的session获取场景吗? private static final ThreadLocal<Session> threadLocal = new ThreadLocal<S ession>(); 23 //获取Session 4 public static Session getCurrentSession(){ 5 Session session = threadLocal.get(); 6 //判断Session是否为空,如果为空,将创建一个session,并设置到本地线程变量中 7 try { 8 if(session ==null&&!session.isOpen()){ 9 if(sessionFactory==null){ 10 rbuildSessionFactory(); // 创建Hibernate的SessionFactory 11 }else{ 12 session = sessionFactory.openSession(); 13 } 14 } 15 threadLocal.set(session); 16 } catch (Exception e) { 17 // TODO: handle exception18 } 19 20 return session; 21 } 为什么每个线程访问数据库都应当是一个独立的Session会话?如果多个线 程共享同一个Session会话,有可能其他线程关闭连接了,当前线程再执行提交 时就会出现会话已关闭的异常,导致系统异常。此方式能避免线程争抢 Session,提高并发下的安全性。使用ThreadLocal的典型场景正如上面的数据库连接管理,线程会话管理等 场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并 发下使用。自己使用的一个场景: @Component public class HostHolder { private static ThreadLocal<User> users = new ThreadLocal<User>(); public User getUser() { return users.get(); } public void setUser(User user) { users.set(user); } public void clear() { 11 users.remove();; 12 } 13 } 主要是用来判断当前用户是否登录。即在某些页面比如发帖等页面需要判断 当前用户是否登录,若没有登录则需要跳转到登录页面。 总结 每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能 够保存多个副本以上,就需要创建多个ThreadLocal。ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的 风险。适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。如果 如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案。
Q:了解反射吗?怎么用?用在哪里?
Java反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属 性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它 的属性。总结说:反射就是把java类中的各种成分映射成一个个的Java对象,并 且可以进行操作。Java反射的原理:java类的执行需要经历以下过程:编译:.java文件编译后生成.class字节码文件 加载:类加载器负责根据一个类的全限定名来读取此类的二进制字节流到 JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对 应的java.lang.Class对象实例 链接 验证:格式(class文件规范) 语义(final类是否有子类) 操作 准备:静态变量赋初值和内存空间,final修饰的内存空间直接赋 原值,此处不是用户指定的初值。解析:符号引用转化为直接引用,分配地址 初始化:根据程序员通过程序指定的主观计划去初始化类变量和其他资源, 或者可以从另一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过 程。执行静态方法代码块为静态变量赋值。Java的反射就是利用上面第二步加载到jvm中的.class文件来进行操作 的。.class文件中包含java类的所有信息,当你不知道某个类具体信息时,可以 使用反射获取class,然后进行各种操作。 怎么用: 1、通过class.forName(),加载某个类。 2、在运行时构造任意一个类的对象。1 Class cls = Class.forName("com.jdk"); 2 jdk jdkobj = cls.newInstance(); 3、在运行时判断任意一个类所具有的成员变量和方法。 1 Class cls = Class.forName("com.jdk"); 2 Methods methods[]= cls.getDecliedMethod(); 3 Fields fields[] = cls.getDeclieredFields(); 使用场景: Class.forName();数据库注册驱动的时候。编译器智能提示该类有哪些方法可供调用。AOP动态代理。经典的就是在xml文件或者properties里面写好了配置,然后在Java类里面解析 xml或properties里面的内容,得到一个字符串,然后用反射机制,根据这个字 符串获得某个类的Class实例,这样就可以动态配置一些东西,不用每一次都要 在代码里面去new或者做其他的事情,以后要改的话直接改配置文件,代码维护 起来就很方便了,同时有时候要适应某些需求,Java类里面不一定能直接调用另 外的方法,这时候也可以通过反射机制来实现。注解(Annontation)是Java5引入的一种代码辅助工具,它的核心作用是对类、方法、变 量、参数和包进行标注,通过反射来访问这些标注信息,以此在运行时改变所注解对象的行为。
Q:动态代理为什么使用反射而不使用继承?
这个问题有坑啊,动态代理可以用继承和反射都可以实现。 JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用 InvokeHandler来处理。 CGlib动态代理:利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。 区别:JDK代理只能对实现接口的类生成代理;CGlib是针对类实现代理,对指定的类生成 一个子类,并覆盖其中的方法,这种通过继承类的实现方式,不能代理final修饰的类。 总结: 1.JDK代理使用的是反射机制实现aop的动态代理,CGLIB代理使用字节码处理框架asm, 通过修改字节码生成子类。所以jdk动态代理的方式创建代理对象效率较高,执行效率较 低,cglib创建效率较低,执行效率高; 2.JDK动态代理机制是委托机制,具体说动态实现接口类,在动态生成的实现类里面委托 hanlder去调用原始实现类方法,CGLIB则使用的继承机制,具体说被代理类和代理类是继 承关系,所以代理类是可以赋值给被代理类的,如果被代理类有接口,那么代理类也可以赋值给接口。 参考:https://blog.csdn.net/lz1170063911/article/details/79835248 JDK的动态代理(依赖于接口) 1. 在Java的动态代理机制中,有两个重要的类或接口,一个是InvocationHandler接 口,另一个是Proxy类。 2. InvocationHandler接口是给动态代理类实现的,负责处理被代理对象的操作。 3. Proxy类是用来创建动态代理类实例对象的,只有得到这个对象,才能调用需要代 理的方法。 4. 动态代理的代理类是在静态代理类上进行修改,将动态代理类实现 InvocationHandler接口,重写Invoke方法,Invoke方法通过传入的被代理类方法和 参数来执行。 JDK动态代理和Cglib动态代理的区别: 1. JDK动态代理是实现了被代理对象的接口,Cglib是继承了被代理对象。 2. Cglib因为是继承机制,所以无法代理被final修饰的方法。 3. JDK和Cglib都是在运行期间生产字节码,JDK是直接写class字节码,Cglib使用 ASM框架写class字节码;cglib代理实现更复杂,生成代理类比JDK效率低。 4. JDK调用代理方法,是通过反射实现机制调用,cglib是通过Fashclass机制直接调 用方法,效率更高。Fastcalss机制:为代理类和被代理类个生成一个class,这个class会为代理类或被代理类的方法分配一个 index。这个index当做一个入参,Fashclass就可以直接定位要调用的方法,并直接进行调用。这样省去了反射调用,所以效率高。
Q:设计模式中简单工厂和抽象工厂的区别?
简单工厂模式:
虽然某种程度不符合开闭原则,但是实际实用很多。工厂方法模式:不修改已有类的情况下,通过增加新的工厂类实现扩展。但是容易导致工厂 类泛滥。
抽象工厂模式:
不可以增加产品,可以增加产品族。增加新产品需要修改很多地方。
Q:HTTP状态码?3XX和4XX区别,404是啥?
3XX重定向:客户端需要做些额外工作才能得到所需要的资源。它们通常用于 GET请求。他们通常告诉客户端需要向另一个URI发送GET请求,才能得到所需 的表示。那个URI就包含在Location响应报头里。 301:永久重定向,比如更换了新的IP,服务端就就告诉客户端以后你访问我的 新IP 302:暂时重定向 4XX客户端错误:这些响应代码表明客户端出现错误。不是认证信息有问题,就 是表示格式或HTTP库本身有问题。客户端需要自行改正。 400: 包含语法错误,无法被服务器解析 403: 服务器已经接收请求,但是拒绝执行 404: 请求失败,请求所希望得到的资源未在服务器上发现 5XX服务端错误:这些响应代码表明服务器端出现错误。一般来说,这些代码意味着服务器处于不能执行客户端请求的状态,此时客户端应稍后重试。 500: 服务器内部错误,无法处理请求
Q:数据库为什么建立索引?如何编程实现数据库抢锁?
这是因为,创建索引可以大大提高系统的性能。 优点: 第一,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 第二,可以大大加快数据的检索速度,这也是创建索引的最主要的原因。 第三,可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别 有意义。 第四,在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组 和排序的时间。 第五,通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。也许会有人要问:增加索引有如此多的优点,为什么不对表中的每一个列创建一个索引呢?这种想法固然有其合理性,然而也有其片面性。虽然,索引有许多优点,但是,为表中的每一个列都增加索引,是非常不明智的。这是因为,增加索引也有许多不利的一个方面, 缺点: 第一,创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。 第二,索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一 定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。 第三,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这 样就降低了数据的维护速度。 哪些适合建立索引哪些不适合? 适合的:经常需要搜索的列上,经常需要范围查询的,主键等。 不适合的:经常不用来查询的,大字段的比如text段等。 如何编程实现数据库抢锁? 行锁的释放是需要事务提交之后自动释放 共享锁:SELECT ... LOCK IN SHARE MODE; 解释:MySQL会对查询结果集中每行都添加共享锁。锁申请前提:当前没有线程对该结果集中的任何行使用排他锁,否则申请会阻塞。 排他锁:SELECT ... FOR UPDATE; MySQL会对查询结果集中每行都添加排他锁,在事物操作中,任何对记录的更新与删除操 作会自动加上排他锁。锁申请前提:当前没有线程对该结果集中的任何行使用排他锁或共享锁,否则申请会阻塞。 表锁:lock tables ... read/write 释放表锁:对应线程执行 unlock tables 即可 参考:https://www.cnblogs.com/zqyanywn/p/5922656.html
Q:Redis数据结构基础,项目中具体怎么用的?如果把数据都存储在Redis中会不会丢失数据?Redis分布式锁了解吗?
单机Redis会丢失,集群的话不会。 分布式锁:请参考https://www.cnblogs.com/seesun2012/p/9214653.html
Q:大数据问题:硬盘里一个50G大小的文件和另一个100G文件,里面存储着不同的名 字,如何在一个内存很小的电脑上实现两个文件的交集运算?
方法一: 分桶+组内Hash索引或者组内使用位图 O(n) 使用哈希切分的方法,将一个大文件里的数据使用一个哈希函数进行切分为 许多小的文件,这样相同的数据一定会进入同一个文件当中去,并进行文件编 号。对另外一个文件也是用相同的哈希函数进行切分为相同数目的小文件,这样 我们只需要将相同编号里的文件进行比较。这样其时间复杂度就会降低为 O(n)。相同的文件查找时可以先对一个文件建立hash索引(桶+链表),然后对另 一个文件依次按照索引进行查找。若hash值相同在进行进一步比较即可。 方法二: 位图 O(n) 这有个前提是文件中必须存储的是数字。那么根据位图,我们可以将第一个 文件中所有数据映射到位图中去。然后再不断导入第二个文件,如果发现某个数 字已经存储在位图中,就说明这是两个文件的交集。 方法三: 近似解-布隆过滤器 O(n) 将A文件每个数据经过多个Hash函数映射到一个位图上,然后第二个文件同 样的做法,如果全部命中,说明相同。否则说明不存在。但是这个有一定的错误 率。方法四:多路归并排序 Onlog(n)+O(n) 先将文件划分为很多等量的小文件。然后对每个小文件导入内存进行内部排 序。这样就有了很多有序的小文件。然后对很多有序的小文件进行多路归并排序,然后不断写入大文件即可。(Onlog(n))最终就得到了一个有序的大文件。最后对两个有序的大文件进行 查找相同的值即可(O(n))。
Q:大数据问题:BBS上很多帖子,发帖最多的人被删除掉了,剩下3个人的帖子数目均超 过1/4,如何找出这三个人?
1.首先统计出数据出现的次数。这个可以采用hash。然后用最小堆便可以求出 出现次数最高的N个数。 2.如果这个文件非常大。可以采取分治法。假设此文件大小为n。而内存中能处 理的为k。则分m次读取。m=n/k+1。每次读取k大小。然后采用1的方法可以 得出k大小中的N个频率最高的。m次后得到m个堆。将此m个堆合并。便可得 到频率最高的N个。 3.堆的维护的代价为lgN;我想还是比较快的。思路大致如下:每一个帖子和用户是一一对应的关系。先根据HashMap<用户ID,帖子数 目>遍历每一个帖子,得到每个用户的发帖子的数目。假设此时用户数目为m, 即Map的大小。然后进行堆排序,由于我们只需要排名前三的人,所有只需维 护一个大小为3的小顶堆即可。时间复杂度:遍历所有帖子时间复杂度为O(n), 堆排序时间复杂度为O(mlog3)。由于三个人的帖子数目均超过了总帖子的 1/4。可以认为m的数目远远小于n。最终复杂度约等于O(n)。