什么时候会触发full gc
-
System.gc()方法的调用
-
老年代空间不足
-
永生区空间不足(JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据)
-
GC时出现promotion failed和concurrent mode failure
-
统计得到的Minor GC晋升到旧生代平均大小大于老年代剩余空间
-
堆中分配很大的对象
可以作为root的对象:
-
类中的静态变量,当它持有一个指向一个对象的引用时,它就作为root
-
活动着的线程,可以作为root
-
一个Java方法的参数或者该方法中的局部变量,这两种对象可以作为root
-
JNI方法中的局部变量或者参数,这两种对象可以作为root
例子:下述的Something和Apple都可以作为root对象。
public AClass{
public static Something;
public static final Apple;
''''''
}
Java方法的参数和方法中的局部变量,可以作为root.
public Aclass{
public void doSomething(Object A){
ObjectB b = new ObjectB;
}
}
新生代转移到老年代的触发条件
-
长期存活的对象
-
大对象直接进入老年代
-
minor gc后,survivor仍然放不下
-
动态年龄判断 ,大于等于某个年龄的对象超过了survivor空间一半 ,大于等于某个年龄的对象直接进入老年代
G1和CMS的区别
-
G1同时回收老年代和年轻代,而CMS只能回收老年代,需要配合一个年轻代收集器。另外G1的分代更多是逻辑上的概念,G1将内存分成多个等大小的region,Eden/ Survivor/Old分别是一部分region的逻辑集合,物理上内存地址并不连续。
-
CMS在old gc的时候会回收整个Old区,对G1来说没有old gc的概念,而是区分Fully young gc和Mixed gc,前者对应年轻代的垃圾回收,后者混合了年轻代和部分老年代的收集,因此每次收集肯定会回收年轻代,老年代根据内存情况可以不回收或者回收部分或者全部(这种情况应该是可能出现)。
双亲委派模型中有哪些方法。用户如何自定义类加载器 。怎么打破双亲委托机制
-
双亲委派模型中用到的方法:
-
findLoadedClass(),
-
loadClass()
-
findBootstrapClassOrNull()
-
findClass()
-
defineClass():把二进制数据转换成字节码。
-
resolveClass()
自定义类加载器的方法:继承 ClassLoader 类,重写 findClass()方法 。
-
继承ClassLoader覆盖loadClass方法 原顺序
-
findLoadedClass
-
委托parent加载器加载(这里注意bootstrap加载器的parent为null)
-
自行加载 打破委派机制要做的就是打乱2和3的顺序,通过类名筛选自己要加载的类,其他的委托给parent加载器。
即时编译器的优化方法
字节码可以通过以下两种方式转换成合适的语言:
-
解释器
-
即时编译器 即时编译器把整段字节码编译成本地代码,执行本地代码比一条一条进行解释执行的速度快很多,因为本地代码是保存在缓存里的
编译过程的五个阶段
-
第一阶段:词法分析
-
第二阶段:语法分析
-
第三阶段:词义分析与中间代码产生
-
第四阶段:优化
-
第五阶段:目标代码生成
java应用系统运行速度慢的解决方法
问题解决思路:
-
查看部署应用系统的系统资源使用情况,CPU,内存,IO这几个方面去看。找到对就的进程。
-
使用jstack,jmap等命令查看是JVM是在在什么类型的内存空间中做GC(内存回收),和查看GC日志查看是那段代码在占用内存。首先,调节内存的参数设置,如果还是一样的问题,就要定位到相应的代码。
-
定位代码,修改代码(一般是代码的逻辑问题,或者代码获取的数据量过大。)
内存溢出是什么,什么原因导致的
内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。为了解决Java中内存溢出问题,我们首先必须了解Java是如何管理内存的。Java的内存管理就是对象的分配和释放问题。在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用GC函数来释放内存,因为不同的JVM实现者可能使用不同的算法管理GC,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是中断式执行GC。但GC只能回收无用并且不再被其它对象引用的那些对象所占用的空间。Java的内存垃圾回收机制是从程序的主要运行对象开始检查引用链,当遍历一遍后发现没有被引用的孤立对象就作为垃圾回收。
引起内存溢出的原因有很多种,常见的有以下几种:
-
内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
-
集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
-
代码中存在死循环或循环产生过多重复的对象实体;
-
使用的第三方软件中的BUG;
-
启动参数内存值设定的过小;
内存溢出的解决
内存溢出虽然很棘手,但也有相应的解决办法,可以按照从易到难,一步步的解决。
第一步,就是修改JVM启动参数,直接增加内存。这一点看上去似乎很简单,但很容易被忽略。JVM默认可以使用的内存为64M,Tomcat默认可以使用的内存为128MB,对于稍复杂一点的系统就会不够用。在某项目中,就因为启动参数使用的默认值,经常报“OutOfMemory”错误。因此,-Xms,-Xmx参数一定不要忘记加。
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。在一个项目中,使用两个数据库连接,其中专用于发送短信的数据库连接使用DBCP连接池管理,用户为不将短信发出,有意将数据库连接用户名改错,使得日志中有许多数据库连接异常的日志,一段时间后,就出现“OutOfMemory”错误。经分析,这是由于DBCP连接池BUG引起的,数据库连接不上后,没有将连接释放,最终使得DBCP报“OutOfMemory”错误。经过修改正确数据库连接参数后,就没有再出现内存溢出的错误。
查看日志对于分析内存溢出是非常重要的,通过仔细查看日志,分析内存溢出前做过哪些操作,可以大致定位有问题的模块。
第三步,找出可能发生内存溢出的位置。重点排查以下几点:
-
检查代码中是否有死循环或递归调用。
-
检查是否有大循环重复产生新对象实体。
-
检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
-
检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
第四步,使用内存查看工具动态查看内存使用情况。某个项目上线后,每次系统启动两天后,就会出现内存溢出的错误。这种情况一般是代码中出现了缓慢的内存泄漏,用上面三个步骤解决不了,这就需要使用内存查看工具了。
内存查看工具有许多,比较有名的有:Optimizeit Profiler、JProbe Profiler、JinSight和Java1.5的Jconsole等。它们的基本工作原理大同小异,都是监测Java程序运行时所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。开发人员可以根据这些信息判断程序是否有内存泄漏问题。一般来说,一个正常的系统在其启动完成后其内存的占用量是基本稳定的,而不应该是无限制的增长的。持续地观察系统运行时使用的内存的大小,可以看到在内存使用监控窗口中是基本规则的锯齿形的图线,如果内存的大小持续地增长,则说明系统存在内存泄漏问题。通过间隔一段时间取一次内存快照,然后对内存快照中对象的使用与引用等信息进行比对与分析,可以找出是哪个类的对象在泄漏。
通过以上四个步骤的分析与处理,基本能处理内存溢出的问题。当然,在这些过程中也需要相当的经验与敏感度,需要在实际的开发与调试过程中不断积累。
总体上来说,产生内存溢出是由于代码写的不好造成的,因此提高代码的质量是最根本的解决办法。有的人认为先把功能实现,有BUG时再在测试阶段进行修正,这种想法是错误的。正如一件产品的质量是在生产制造的过程中决定的,而不是质量检测时决定的,软件的质量在设计与编码阶段就已经决定了,测试只是对软件质量的一个验证,因为测试不可能找出软件中所有的BUG。
JAVA 线程状态转换图示
synchronized 的底层怎么实现
-
同步代码块(Synchronization)基于进入和退出管程(Monitor)对象实现。每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
-
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
-
如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
-
如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
-
被 synchronized 修饰的同步方法并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成
讲一下CAS
CAS,compare and swap的缩写,中文翻译成比较并交换。乐观锁用到的机制就是CAS,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试。
原理:
-
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
JDK文档说cas同时具有volatile读和volatile写的内存语义。
缺点:
-
ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化
-
循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
-
只能保证一个共享变量的原子操作。对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
线程池
Executor线程池框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。
ThreadPoolExecutor执行的策略
-
线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
-
线程数量达到了corePools,则将任务移入队列等待
-
队列已满,新建线程(非核心线程)执行任务
-
队列已满,总线程数又达到了maximumPoolSize,就会由(RejectedExecutionHandler)抛出异常
新建线程 -> 达到核心数 -> 加入队列 -> 新建线程(非核心) -> 达到最大数 -> 触发拒绝策略
常见四种线程池
-
CachedThreadPool():可缓存线程池。
-
线程数无限制
-
有空闲线程则复用空闲线程,若无空闲线程则新建线程
-
一定程序减少频繁创建/销毁线程,减少系统开销
-
FixedThreadPool():定长线程池。
-
可控制线程最大并发数(同时执行的线程数)
-
超出的线程会在队列中等待
-
ScheduledThreadPool():定时线程池。
-
支持定时及周期性任务执行。
-
SingleThreadExecutor():单线程化的线程池。
-
有且仅有一个工作线程执行任务
-
所有任务按照指定顺序执行,即遵循队列的入队出队规则
四种拒绝策略
-
AbortPolicy:拒绝任务,且还抛出RejectedExecutionException异常,线程池默认策略
-
CallerRunPolicy:拒绝新任务进入,如果该线程池还没有被关闭,那么这个新的任务在执行线程中被调用
-
DiscardOldestPolicy: 如果执行程序尚未关闭,则位于头部的任务将会被移除,然后重试执行任务(再次失败,则重复该过程),这样将会导致新的任务将会被执行,而先前的任务将会被移除。
-
DiscardPolicy:没有添加进去的任务将会被抛弃,也不抛出异常。基本上为静默模式。
为什么要用线程池
-
减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
-
运用线程池能有效的控制线程最大并发数,可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
-
对线程进行一些简单的管理,比如:延时执行、定时循环执行的策略等,运用线程池都能进行很好的实现
-
对象锁用于对象实例方法,
-
类锁用于类的静态方法或一个类的class对象。
-
类的对象实例可以有很多,不同对象实例的对象锁互不干扰,而每个类只有一个类锁
简述volatile字
两个特性
-
保证了不同线程对这个变量进行 读取 时的可见性,即一个线程修改 了某个变量的值 , 这新值对其他线程来说是立即可见的 。(volatile 解决了 线程间 共享变量
-
禁止进行指令重排序 ,阻止编译器对代码的优化
要想并发程序正确地执行,必须要保证原子性、可见性以及有序性,锁保证了原子性,而volatile保证可见性和有序性
happens-before 原则(先行发生原则):
-
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在 后面的操作
-
锁定规则:一个 unLock 操作先行发生于后面对同一个锁的 lock 操作
-
volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
-
传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以 得出操作 A 先行发生于操作 C
-
线程启动规则:Thread 对象的 start()方法先行发生于此线程的每个一个动作
-
线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测 到中断事件的发生
-
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 T hread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
-
对象终结规则:一个对象的初始化完成先行发生于他的 finalize()方法的开始
Lock 和synchronized 的区别
-
Lock 是一个 接口,而 synchronized 是 Java 中的 关键字, synchronized 是 内置的语言实现;
-
synchronized 在 发生异常时,会 自动释放线程占有的锁,因此 不会导 致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放 锁,则很 可能造成死锁现象,因此用 使用 Lock 时需要在 finally 块中释放锁;
-
Lock 可以让 等待锁的线程响应中断 (可中断锁),而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去, 不能够响应中 断 (不可中断锁);
-
通过 Lock 可以知道 有没有成功获取锁 (tryLock ( ) 方法 :如果获取 了锁 ,回 则返回 true ;回 否则返回 false e, , 也就说这个方法无论如何都会立即返回 。在拿不到锁时不会一直在那等待。),而 synchronized 却无法办到。
-
Lock 可以提高 多个线程进行读操作的效率( 读写锁)。
-
Lock 可以实现 公平锁,synchronized 不保证公平性。在性能上来说,如果线程竞争资源不激烈时,两者的性能是差不多的,而 当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优 于 synchronized。所以说,在具体使用时要根据适当情况选择。
ThreadLocal(线程变量副本)
Synchronized实现内存共享,ThreadLocal为每个线程维护一个本地变量。采用空间换时间,它用于线程间的数据隔离,为每一个使用该变量的线程提供一个副本,每个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。ThreadLocal类中维护一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值为对应线程的变量副本。ThreadLocal在Spring中发挥着巨大的作用,在管理Request作用域中的Bean、事务管理、任务调度、AOP等模块都出现了它的身影。Spring中绝大部分Bean都可以声明成Singleton作用域,采用ThreadLocal进行封装,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。
通过Callable和Future创建线程
Java 5在concurrency包中引入了java.util.concurrent.Callable 接口,它和Runnable接口很相似,但它可以返回一个对象或者抛出一个异常。
Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法去在线程池中执行Callable内的任务。由于Callable任务是并行的,我们必须等待它返回的结果。java.util.concurrent.Future对象为我们解决了这个问题。在线程池提交Callable任务后返回了一个Future对象,使用它我们可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法让我们可以等待Callable结束并获取它的执行结果。
-
创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
-
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
-
使用FutureTask对象作为Thread对象的target创建并启动新线程。
-
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
什么叫守护线程,用什么方法实现守护线程(Thread.setDeamon()的含义)
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆:只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。JVM内部的实现是如果运行的程序只剩下守护线程的话,程序将终止运行,直接结束。所以守护线程是作为辅助线程存在的,主要的作用是提供计数等等辅助的功能。
如何停止一个线程?
终止线程的三种方法:
-
使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。在定义退出标志exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值,
thread.exit = true; // 终止线程thread
-
使用stop方法强行终止线程(这个方法不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果)。使用stop方法可以强行终止正在运行或挂起的线程。我们可以使用如下的代码来终止线程:thread.stop(); 虽然使用上面的代码可以终止线程,但使用stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,因此,并不推荐使用stop方法来终止线程。
-
使用interrupt方法中断线程,使用interrupt方法来终端线程可分为两种情况:
-
线程处于阻塞状态,如使用了sleep方法。
-
使用while(!isInterrupted()){……}来判断线程是否被中断。在第一种情况下使用interrupt方法,sleep方法将抛出一个InterruptedException例外,而在第二种情况下线程将直接退出。
注意:在Thread类中有两个方法可以判断线程是否通过interrupt方法被终止。一个是静态的方法interrupted(),一个是非静态的方法isInterrupted(),这两个方法的区别是interrupted用来判断当前线是否被中断,而isInterrupted可以用来判断其他线程是否被中断。因此,while (!isInterrupted())也可以换成while (!Thread.interrupted())。
什么是线程安全?什么是线程不安全?
-
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
-
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据 在多线程的情况下,由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。
HashSet和TreeSet区别
HashSet
-
不能保证元素的排列顺序,顺序有可能发生变化
-
不是同步的
-
集合元素可以是null,但只能放入一个null 当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置。
TreeSet
-
TreeSet是SortedSet接口的唯一实现类
-
TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序 和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象
讲一下LinkedHashMap
LinkedHashMap的实现就是HashMap+LinkedList的实现方式,以HashMap维护数据结构,以LinkList的方式维护数据插入顺序
LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的。在遍历的时候会比HashMap慢TreeMap能够把它保存的记录根据键排序,默认是按升序排序,也可以指定排序的比较器
利用LinkedHashMap实现LRU算法缓存(
-
LinkedList首先它是一个Map,Map是基于K-V的,和缓存一致
-
LinkedList提供了一个boolean值可以让用户指定是否实现LRU)
Java8 中HashMap的优化(引入红黑树的数据结构和扩容的优化)
-
if (binCount >= TREEIFY_THRESHOLD - 1) 当符合这个条件的时候,把链表变成treemap红黑树,这样查找效率从o(n)变成了o(log n) ,在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:
-
我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置
这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算。
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:hashMap 1.8 哈希算法例图2
因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”
Map遍历的keySet()和entrySet()性能差异原因
Set<Entry<String, String>> entrySet = map.entrySet();
Set<String> set = map.keySet();`
-
keySet()循环中通过key获取对应的value的时候又会调用getEntry()进行循环。循环两次
-
entrySet()直接使用getEntry()方法获取结果,循环一次
-
所以 keySet()的性能会比entrySet()差点。所以遍历map的话还是用entrySet()来遍历
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
抽象类和接口的对比
参数 | 抽象类 | 接口 |
---|---|---|
默认的方法实现 | 它可以有默认的方法实现 | 接口完全是抽象的。它根本不存在方法的实现 |
实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现 |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
与正常Java类的区别 | 除了你不能实例化抽象类之外,它和普通Java类没有任何区别 | 接口是完全不同的类型 |
访问修饰符 | 抽象方法可以有public、protected和default这些修饰符 | 接口方法默认修饰符是public。你不可以使用其它修饰符。 |
main方法 | 抽象方法可以有main方法并且我们可以运行它 | 接口没有main方法,因此我们不能运行它。 |
多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口只可以继承一个或多个其它接口 |
速度 | 它比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。 |
添加新方法 | 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 | 如果你往接口中添加方法,那么你必须改变实现该接口的类。 |
创建一个类的几种方法?
-
使用new关键字 → 调用了构造函数
-
使用Class类的newInstance方法 → 调用了构造函数
Employee emp2 = (Employee)Class.forName("org.programming.mitra.exercises.Employee").newInstance();
-
使用Constructor类的newInstance方法 → 调用了构造函数
Constructor<Employee> constructor = Employee.class.getConstructor();
Employee emp3 = constructor.newInstance();
-
使用clone方法 → 没有调用构造函数
-
使用反序列化 }→ 没有调用构造函数
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj"));
Employee emp5 = (Employee) in.readObject();
Redirect和forward
-
上图所示的间接转发请求的过程如下:浏览器向Servlet1发出访问请求;Servlet1调用sendRedirect()方法,将浏览器重定向到Servlet2;浏览器向servlet2发出请求;最终由Servlet2做出响应。
-
上图所示的直接转发请求的过程如下:浏览器向Servlet1发出访问请求;Servlet1调用forward()方法,在服务器端将请求转发给Servlet2;最终由Servlet2做出响应。
什么是泛型,为什么要使用以及类型擦除
-
泛型的本质就是“参数化类型”,也就是说所操作的数据类型被指定为一个参数。创建集合时就指定集合元素的数据类型,该集合只能保存其指定类型的元素, 避免使用强制类型转换。
-
Java 编译器生成的字节码是不包含泛型信息的,泛型类型信息将在 编译处理 时 被擦除,这个过程即 类型擦除。类型擦除可以简单的理解为将泛型 java 代码转 换为普通 java 代码,只不过编译器更直接点,将泛型 java 代码直接转换成普通 java 字节码。
类型擦除的主要过程如下:
-
将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
-
移除所有的类型参数。
Object跟这些标记符代表的java类型有啥区别呢?
Object是所有类的根类,任何类的对象都可以设置给该Object引用变量,使用的时候可能需要类型强制转换,但是用使用了泛型T、E等这些标识符后,在实际用之前类型就已经确定了,不需要再进行类型强制转换。
Error类和Exception类区别
-
Error类和Exception类的父类都是throwable类,他们的区别是:Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。Exception类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行, 而不应该随意终止异常。
-
Exception类又分为运行时异常(Runtime Exception)和受检查的异常(Checked Exception ),运行时异常;ArithmaticException,IllegalArgumentException,编译能通过,但是一运行就终止了,程序不会处理运行时异常,出现这类异常,程序会终止。而受检查的异常,要么用try。。。catch捕获,要么用throws字句声明抛出,交给它的父类处理,否则编译不会通过。
throw和throws区别
throw:(针对对象的做法)抛出一个异常,可以是系统定义的,也可以是自己定义的
public void yichang(){
NumberFormatException e = new NumberFormatException();
throw e;
}
throws:(针对一个方法抛出的异常)抛出一个异常,可以是系统定义的,也可以是自己定义的。
public void yichang() throws NumberFormatException{
int a = Integer.parseInt("10L");
}
-
throws出现在方法函数头;而throw出现在函数体。
-
throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常。
-
两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
.class 文件是什么类型文件
class文件是一种8位字节的二进制流文件
java中序列化之子类继承父类序列化
父类实现了Serializable,子类不需要实现Serializable
相关注意事项 1. 序列化时,只对对象的状态进行保存,而不管对象的方法;2. 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;c)当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;3. 并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如:1.安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行rmi传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。2. 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现。
2,反过来父类未实现Serializable,子类实现了,序列化子类实例的时候,父类的属性是直接被跳过不保存,还是能保存但不能还原?(答案:值不保存)
解:父类实现接口后,所有派生类的属性都会被序列化。子类实现接口的话,父类的属性值丢失。
java中序列化之子类继承父类序列化
标识符
标识符可以包括这4种字符:字母、下划线、$、数字;开头不能是数字;不能是关键字
Integer i=new Integer(127);和Integer i=127;的区别
Integer i = 127的时候,使用Java常量池技术,是为了方便快捷地创建某些对象,当你需要一个对象时候,就去这个池子里面找,找不到就在池子里面创建一个。但是必须注意 如果对象是用new 创建的。那么不管是什么对像,它是不会放到池子里的,而是向堆申请新的空间存储。Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值在-128到127之间的数时才可使用对象池。超过了就要申请空间创建对象了
int i1=128;
Integer i2=128;
Integer i3=new Integer(128);//自动拆箱
System.out.println(i1==i2);//true
System.out.println(i1==i3);//true
Integer i5=127;
Integer i6=127;
System.out.println(i5==i6);//true
Integer i5=127;
Integer ii5=new Integer(127);
System.out.println(i5==ii5);//false
Integer i7=new Integer(127);
Integer i8=new Integer(127);
System.out.println(i7==i8);//false
手写单例模式
最好的单例模式是静态内部类,不要写双重检验
private static class LazySomethingHolder {
public static Something something = new Something();
}
public static Something getInstance() {
return LazySomethingHolder.something;
}
为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里?
Java的每个对象中都有一个锁(monitor,也可以成为监视器) 并且wait(),notify()等方法用于等待对象的锁或者通知其他线程对象的监视器可用。在Java的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是Object类的一部分,这样Java的每一个类都有用于线程间通信的基本方法
Java中wait 和sleep 方法比较
-
这两个方法来自不同的类分别是Thread和Object
-
最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
-
wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)
-
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
-
sleep方法属于Thread类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在sleep的过程中过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及以后的代码。
-
注意sleep()方法是一个静态方法,也就是说他只对当前对象有效,通过t.sleep()让t对象进入sleep,这样的做法是错误的,它只会是使当前线程被sleep 而不是t线程
-
wait属于Object的成员方法,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。wait()方法也同样会在wait的过程中有可能被其他对象调用interrupt()方法而产生
hashCode和equals方法的关系
在有些情况下,程序设计者在设计一个类的时候为需要重写equals方法,比如String类,但是千万要注意,在重写equals方法的同时,必须重写hashCode方法。也就是说对于两个对象,如果调用equals方法得到的结果为true,则两个对象的hashcode值必定相等;如果equals方法得到的结果为false,则两个对象的hashcode值不一定不同;如果两个对象的hashcode值不等,则equals方法得到的结果必定为false;如果两个对象的hashcode值相等,则equals方法得到的结果未知。
Object类中有哪些方法,列举3个以上(可以引导)
Object方法:equals()、toString()、finalize()、hashCode()、getClass()、clone()、wait()、notify()、notifyAll()
String s=new String("xyz")究竟创建String Object分为两种情况:
-
如果String常理池中,已经创建"xyz",则不会继续创建,此时只创建了一个对象new String("xyz");
-
如果String常理池中,没有创建"xyz",则会创建两个对象,一个对象的值是"xyz",一个对象new String("xyz")。
什么是值传递和引用传递
值传递
public class TempTest {
private void test1(int a) {
a = 5;
System.out.println("test1方法中的a=" + a);
}
public static void main(String[] args) {
TempTest t = new TempTest();
int a = 3;
t.test1(11);
System.out.println("main方法中a=" + a);
}
}
test1方法中的a=5 main方法中a=3 值传递:传递的是值的拷贝,传递后就互不相关了 引用传递:传递的是变量所对应的内存空间的地址
public class TempTest {
private void test1(A a) {
a.age = 20;
System.out.println("test1方法中a=" + a.age);
}
public static void main(String[] args) {
TempTest t = new TempTest();
A a = new A();
a.age = 10;
t.test1(a);
System.out.println("main方法中a=" + a.age);
}
}
class A {
public int age = 0;
}
test1方法中a=20 main方法中a=20 传递前和传递后都指向同一个引用(同一个内存空间) 如果不互相影响,方法是在test1方法里面新new一个实例就可以了
讲一下netty
netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件
Nio的原理(同步非阻塞)
服务端和客户端各自维护一个管理通道的对象,我们称之为 selector,该对 象能检测一个或多个通道(channel)上的事件。我们以服务端为例,如果服务 端的 selector 上注册了读事件,某时刻客户端给服务端送了一些数据,阻塞 I/O 这时会调用 read()方法阻塞地读取数据,而 NIO 的服务端会在 selector 中添加 一个读事件。服务端的处理线程会轮询地访问 selector,如果访问 selector 时发 现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处 理线程会一直阻塞直到感兴趣的事件到达为止。
缓冲区Buffer、通道Channel、选择器Selector
缓冲区Buffer
-
缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。
通道Channel
-
通道是一个对象,通过它可以读取和写入数据,当然了所有数据都通过Buffer对象来处理。我们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区。同样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。通道与流的不同之处在于 通道是双向 的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类,比如 InputStream 只能进行读取操作,OutputStream 只能进行写操作),而通道是双向的,可以用于读、写或者同时用于读写。
选择器(Selector )
-
NIO 有一个主要的类 Selector,这个类似一个观察者,只要我们把需要探知 的 socketchannel 告诉 Selector,我们接着做别的事情, 当有事件发生时,他会 通知我们,传回一组 SelectionKey, 我们读取这些 Key, 就会获得我们刚刚注册 过的 socketchannel, 然后,我们从这个 Channel 中读取数据,放心,包准能 够读到,接着我们可以处理这些数据。
-
Selector 内部原理实际是在做一个 对所注册的 channel 的轮询访问,不断 地轮询,一旦轮询到一个 channel 有所注册的事情发生,比如数据来了,他就 会站起来报告, 交出一把钥匙,让我们 通过这把钥匙来读取这个 channel 的内 容。
BIO和NIO的区别
-
BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
-
NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
NIO的selector作用
Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
为了实现Selector管理多个SocketChannel,必须将具体的SocketChannel对象注册到Selector,并声明需要监听的事件(这样Selector才知道需要记录什么数据),一共有4种事件:
-
connect:客户端连接服务端事件,对应值为SelectionKey.OP_CONNECT(8)
-
accept:服务端接收客户端连接事件,对应值为SelectionKey.OP_ACCEPT(16)
-
read:读事件,对应值为SelectionKey.OP_READ(1)
-
write:写事件,对应值为SelectionKey.OP_WRITE(4)
每次请求到达服务器,都是从connect开始,connect成功后,服务端开始准备accept,准备就绪,开始读数据,并处理,最后写回数据返回。
所以,当SocketChannel有对应的事件发生时,Selector都可以观察到,并进行相应的处理。