2016.7.31更新...........................................................................

(35):Java中的异常分类

        Java中的异常分为三大类:Error/Runtime Exception(运行时异常)/普通异常

        这三类异常的类继承结构是

        java.lang.Throwable

              java.lang.Error

              java.lang.Exception

                     java.lang.RuntimeException

        说明异常全部是继承自Throwable类的;

        这三类异常中,Error是被设置成不能被捕获的,因为这种异常是由JVM自身产生的;而运行时异常(Runtime Exception)往往是与我们的环境有关系的,并且这种异常发生的情况太普遍了,甚至是你只要写短短的一句代码都可能发生这种异常,因此系统允许你不去捕获这个异常,系统自己会去处理;普通异常就是我们经常需要在程序中捕获处理的异常了;

(36):final、finally、finalize的区别

        final是java中的关键字,该关键字修饰类表示当前类不存在子类,也就是不会存在既由final又由abstract修饰的类;该关键字修饰方法表示子类不能重写该方法;该关键字修饰变量表示该变量的值将不会发生变化,也就是不能在程序中改变该变量的值;

        finally是java异常处理语句结构的一部分,finally结构里面的语句总会被执行而不管是否发生了异常;如果在try或者catch语句中存在return/break/continue的话,finally语句实际上是在这些退出方法之前调用的,此外,如果finally中存在return语句的话,他是会覆盖掉try或者catch语句中的返回结果的,因为finally执行先于try或者catch里面的返回语句,如果finally返回了,那么程序当然返回了;

        finalize是用在虚拟机中的,如果我们在可达性分析之后发现没有与GC Root相连接的引用链。那么他将会被第一次标记并且进行筛选,筛选的条件是看是否有必要执行finalize方法,如果该对象没有覆盖finalize方法或者已经被虚拟机执行过finalize方法,虚拟机就会认为该对象是需要回收的,也就是说finalize是对象逃脱死亡的最后一次机会,如果这时候还没有逃脱,那么基本上它就真的被回收了,当然我们也可以在这个方法里面做一些在对象被垃圾回收器回收前我们自己想要回收的一些操作,相当于C++中的析构函数,注意一点就是垃圾回收器并不保证一定会执行某个对象的finalize方法;

(37):Object类中有哪些公有方法?

        hashCode()、equals()、toString()、wait()、notify()、getClass()、finalize()、clone()

(38):CMS垃圾回收器

        CMS(Concurrent Mark Sweep)是一种以最短回收停顿时间为目标的收集器,整个手机过程分为4个阶段:初始标记、并发标记、重新标记、并发清除,这4个步骤中,初始标记和重新标记会产生"Stop The World"现象,但是并发标记和并发清除可以做到和用户线程一起工作,但是也会相应的占用CPU的资源导致应用程序速度变慢;初始标记仅仅是标记一下GC Roots直接关联的对象,速度很快,并发标记就是进行GC Root Tracing过程,而重新标记是为了修正并发标记期间因为应用程序的继续运作而导致标记产生变动的那一部分对象的标记记录,这一阶段的停顿时间要长于初始标记,但是远比并发标记时间要短,CMS在并发清理阶段因为和应用程序并发执行,那么在这个阶段可能会产生"浮动垃圾",这部分垃圾是没有进行过标记的,如果垃圾过多的话,可能会导致额外的GC操作;此外,CMS采用的是"标记-清除"算法,那么势必会导致内存碎片的出现,导致某一时候无法找到足够大的连续空间来存放较大对象;

2016.9.2更新...........................................................................

(39):Java内存模型

        Java内存模型可以类比于处理器模型,我们都知道计算机系统为了加快读写速度引入高速缓存来缓解内存与处理器之间处理速度的量级差别的,将运算需要使用的数据复制到缓存中,让其快速运行,当运算结束之后再从缓存中将数据写回内存中,这样处理器是直接和高速缓存打交道的,不用等相对缓慢的内存读写数据了,这在单处理器情况下是不会有什么问题的,但是到了多处理器情况下就会出现缓存一致性问题了,不同的处理器各自有各自的高速缓存,但是他们之间却是共享主存的,那么势必会出现某一处理器已经修改了某一值,但是还没从自己的高速缓存中写到主存中,另一个处理器的高速缓存却从主存中取出了该值原先的值,造成了读取脏数据的情况出现,对于这种情况,需要缓存一致性协议来进行保障,Java中的内存模型其实和处理器的这种模型是有可比性的,我们可以把Java内存模型中的栈类比为处理器,在栈中存在一个本地工作内存区域,我们可以把这个本地工作内存区域看成是处理器模型中的高速缓存,把堆类比为处理器模型中的主存,每个线程在自己的栈空间中都存在一个本地工作内存区域就相当于每个处理器都有自己的高速缓存一样,栈与栈之间不能进行数据共享,他们要想共同操作数据,那么就需要堆空间的参与,这就会出现和处理器模型类似的一致性问题了,不同的线程栈空间是不可以直接互相访问的,线程间变量值的传递是需要主内存的参与完成的;

        在Java内存模型中,为了能使得变量从主内存拷贝到每个线程栈的工作内存或者从每个线程栈的工作内存拷贝到主内存中,为我们定义了八种操作:lock/unlock/read/load/use/assign/save/write

        具体这八种操作的关系见下:

                                            

java中的例外 java例外三大类_java面试题

                   

        这八个操作使用起来是有规则的:

        (1):read/load,store/write必须成对出现,不允许单独出现,否则会造成从主存读取的值工作内存不接受,或者工作内存写到主存中的值,主存不接受;

        (2):如果在线程中使用了assign操作改变了变量副本,那么必须通过write-store同步回主存中,如果线程中没有发生assign操作,那么也不允许通过write-store同步到主存中;

        (3):一个新的变量只能在主存中生成,即不允许在工作内存中直接使用一个未被初始化的变量,也就是说在use和store之前必须先执行assign和load操作;

        (4):主存中的变量在同一时刻值允许一个线程对他进行lock操作,有多少lock操作就应该有多少unlock操作;

        (5):在lock操作之后,会清空当前线程工作内存中原先的副本值,需要再次从主存中read-load新值;

        (6):在执行unlock之前,需要把改变的副本同步到主存中,也就是调用write-store方法;

        那么针对Java内存模型中出现的一致性问题,我们可以通过synchronized和volatile来解决;

(40):Java线程池实现原理

       

(41):Callable和Runnable的区别

        (1):Callable是可以有返回值的,具体来讲是他的接口中的call方法可以返回值,这个返回值我们可以通过实现Future的接口对象的get方法来获得;Runnable是没有返回值的,也就是说Runnable的run方法没有返回值;

        (2):Callable在使用的过程中是可以抛出异常的,而Runnable是不能抛出异常的,也就是在使用Runnable的时候,我们必须自己处理可能产生的异常;

(42):yield与sleep以及wait的区别

        yield与sleep的区别:

        (1):yield与sleep都属于Thread类的静态方法,调用两者的时候都不会释放掉锁,调用sleep当前线程状态切换成阻塞状态,调用yield,当前线程状态切换成就绪状态;

        (2):调用sleep的话,能够使得优先级低于当前线程的线程也获得CPU的竞争机会,当然高于当前线程优先级的线程也是可以获取到的;调用yield的话,只会使得和当前线程优先级相同的线程获得竞争CPU的竞争机会;

        sleep与wait的区别:

        (1):首先两者是位于不同类中的,sleep位于Thread类,wait位于Object类;

        (2):调用sleep方法的话,只是会将当前线程切换成阻塞状态,但是如果当前线程占有锁资源的话,他是不会释放掉锁资源的;但是对于wait方法来说的话,他是会释放掉锁资源的;

        (3):wait是必须在synchronized语句块里面执行的,在synchronized中调用了wait之后,其他线程就可以竞争synchronized锁起来的那部分代码了,而当前线程是会被放到等待池里面的,在别的线程调用了notify或者notifyAll方法之后,就会从等待池里面找一个或者全部线程唤醒了,我们可以把wait和notify理解成适用于线程间通信的一种方式吧;

(43):Vector与ArrayList的区别

        (1):首先来讲的话,Vector是线程安全的,他的线程安全是通过在每个方法前面添加synchronized来实现的,相对来说效率还是比较低的,而ArrayList是非线程安全的,要想实现线程安全版本的ArrayList,我们可以使用Collections.synchronizedList方法,也可以使用concurrent包下面的CopyOnWriteArrayList来实现;CopyOnWriteArrayList解决fast-fail的原理是每次我们在向List中添加元素的时候,实际上是会首先复制一个快照来进行修改,改完之后再将新数组赋值给原先数组引用;因为对快照的修改对读操作是不可见的,所以只有写锁没有读锁,而且每次添加元素的时候都要进行数组元素的赋值,因此CopyOnWriteArrayList适用于读多写少的情况,而如果是我们迭代的时候,就会发现迭代器里面其实也是对传入的数组对象赋值给了一个final类型的快照,这样的话就保证了我们在迭代的过程中,数组的元素发生变化之后对我们也没什么影响了;

        (2):因为Vector和ArrayList底层都是通过数组实现的嘛,所以的话肯定就会出现数组大小不足的情况,势必需要进行扩容,两者的扩容也不太一样,对于Vector的话如果我们没有设置增长因子的话(其实就是容量不足的时候应该增多大而已啦)扩充成原先数组大小的两倍,如果扩容之后数组大小还是不够的话,那么会将数组大小设置成我们真正的数组大小,如果设置了增长因子的话,在将数组大小扩充我们增长因子大小,如果扩充之后还是不够的话,同样也是设置为真正数组的大小;而对于ArrayList而言,如果数组大小不够的话,只会扩充到原先数组的1.5倍,剩下的和Vector就一致了;也即,ArrayList的扩充方式只有一种,没有像Vector那样还有增长因子;

(44):HashMap、HashSet、TreeMap、LinkedHashMap、ConcurrentHashMap再总结

        (1):HashMap底层实现是数组+链表,既然用到数组,那么肯定会出现数组大小不满足的情况,这时候就要进行扩容操作,扩容是将容量扩展为原先的2倍,并且扩容之后需要重新计算原先已经在HashMap中存储的key的hash值,因此在我们明确知道要多大HashMap数组大小的情况下最好是能够直接在初始化HashMap的时候就指定他的大小;那么什么情况下算是容量不足呢?就是我们的数组元素个数大于数组大小*装填因子,装填因子默认大小是0.75,比如数组大小是16的话,那么当数组内部元素个数容量超过16*0.75=12的时候就认为数组容量不足,需要扩容,其实装填因子的选择是有讲究的,如果装填因子选的比较小的话,那么会造成我们内存空间的浪费,但是添加和删除元素的效率是会增加的,因为添加删除元素的话每次都是需要查询操作,而查询操作是需要遍历每个数组元素位置对应的Entry链的,如果装填因子比较小的话,那么会减少hash冲突的概率,这样的话我们的Entry链就会比较短,在查询的时候就不会很耗时了;而如果装填因子比较大的话,虽然会减少内存空间的浪费,但是因为添加删除操作的话会频繁的进行查询数组元素对应的Entry链的操作,因此相对来说效率也不会很高;HashMap用到了hash算法,那么势必会出现hash冲突的情况了,一般处理hash冲突的方法有开放地址法、再散列法和拉链法,HashMap采用的是拉链法;

        (2):HashMap进行put和get操作的话,是先对key计算出对应的hash值,然后找到这个hash值在数组中的位置,接着查看该位置处的Entry链是否存在,如果存在的话,则查看是否有键值对的key与当前key相等,有的话则用新值覆盖掉原先键值对中的value值,没有的话,则将当前键值对插入到Entry链的链头就可以了;get操作的话,也是先计算出key值对应的hash值,然后找到该hash值在数组中的位置,接着再去该位置处的Entry链中查看是否存在与当前key值相等的key存在,存在的话返回value就可以了,不存在的话,返回null;

        (3):HashSet的底层实现是HashMap,其实就是通过HashSet中的key值唯一性来达到HashSet中元素唯一这点要求的,如果我们将要put进去的元素的key值等于已经存在在HashSet中的元素的话,那么是不会将当前元素添加到HashSet里面操作的;

        (4):HashMap的key值和value值是可以为null的,具体来讲的话,我们来说说key等于null的时候,HashMap的put方法是怎么处理的,get方法的话,处理过程类似了,在put方法体中首先会去判断当前的key值是不是等于null,如果等于的话,那么会调用putForNullKey方法,将当前的键值对放到我们数组元素的0号位置对应的Entry链中,当然对于get方法的话,如果key值等于null的话,直接执行的是getForNullKey方法,这个方法获取到的就是数组位置为0处的Entry链的第一个的值了,因为像key等于null的话,0号数组元素对应的Entry链中只可能存在一个元素啦,因为key等于null,它的hash值呀,equals值呀都是null了,说白了就一个嘛;

ConcurrentHashMap是最高的,而SynchronizedMap与HashTable都是通过Synchronized关键字实现的,相对来说效率不是很高;

TreeMap在插入或者删除的时候需要排序,因此在效率上讲的话是不如HashMap的;

        (7):在JDK1.8之后,HashMap的实现不再是简单的数组+链表的方式了,而是数组+链表+红黑树,为什么要引入红黑树呢?关键原因在于,我们在使用HashMap的时候会频繁的进行put和get操作,而这两个操作又都会用到查询方法(具体来讲的话就是在我们计算出key的hash值之后,会去查找指定hash值处的冲突链表,而链表的查找操作时间复杂度是O(n)的,如果链表长度过大的话,时间效率上不会很高的,因此引入了红黑树的方式来减少查询操作所带来的时间开销,引入红黑树之后的查找时间复杂度是O(nlgn)),具体实现过程我们以put方法为例来简单介绍下,在我们将元素插入到指定hash对应的冲突列表(JDK1.7之前都是链表)的时候,首先先查看当前列表是否为null,为null的话,则直接插入当前键值对即可;不为null的话则查看当前列表中的第一个元素的key值是都和当前key值相等,相等的话将其value替换成当前要插入的值即可,不等的话则会判断当前列表中的元素属性是不是TreeNode,如果是的话,则表示之前已经将列表转换成红黑树了,那么此刻我们需要将当前键值对按照红黑树的规则进行插入操作,如果列表结点属性值不是TreeNode类型的话,则判断当前列表长度是否大于8,如果大于的话,则首先将当前列表转换成红黑树,然后将当前键值对插入到红黑树里面,如果不大于8的话,则直接以链表的方式插入到里面即可,这时候的操作和JDK1.7是一致的;

ConcurrentHashMap对其进行了改进,具体改进措施是采用了锁分离技术,实现ConcurrentHashMap用到了Segment(桶)和HashEntry(结点),一个ConcurrentHashMap里面包含一个Segment数组,每一个Segment的结构和HashMap类似,一个Segment里面包含一个HashEntry数组,每一个HashEntry是一个链表结构的元素,每一个Segment守护着一个HashEntry数组里面的元素,当需要对HashEntry数组的元素进行修改的时候,需要首先获取到该HashEntry对应的Segment锁;接下来来看看ConcurrentHashMap的put和get操作是怎么实现的,和HashTable不同的是ConcurrentHashMap的get操作是不需要加锁的,除非读到的值是空才会加锁进行重读,为什么ConcurrentHashMap可以做到不加锁读呢?原因在于ConcurrentHashMap中的HashEntry中的value属性被设置成了volatile类型的,这样的话能够保证get方法读取到的值是最新的,不会读到过期的值,因为ConcurrentHashMap和HashTable一样是不允许key或者value值为null的,那么get操作怎么就可能读到null值呢?读到之后还要加锁进行重读又是怎么回事呢?原因还是出在了HashEntry中属性value的声明上面,你查看value属性声明的话发现他只声明成了volatile类型,并没有声明成final类型,那么在非同步读取的情况下就可能出现读取到是空值的情况了,还有一种情况就是有可能我们正在访问的HashEntry正在重构,这时候也可能造成value值为null的情况下,但是这两种情况下都有可能是别的线程正在修改,而之前的get操作均未锁定,因此需要锁定来重读,个人感觉锁定操作的情况之后出现在刚开始初始化以及扩容过程中造成的元素移动的场景;对于put操作而言的话,首先是根据当前key的hash值获取到其所在的Segment桶,接着便会锁住当前桶,调用Segment的put方法,根据当前key的hash值找到该HashEntry中的位置,接下来的put操作就和我们平常的HashMap的put操作一致了;

另外ConcurrentHashMap还有两点做的非常到位:(1)计算Segment中所有HashEntry的个数,按常理来说的话需要将Segment进行加锁,但是ConcurrentHashMap利用了在累加过程中HashEntry变化几率比较小的特点,先不加锁的计算了两次,如果两次不相同的话才会加锁计算,相同的话直接返回,好巧妙啊!(2)我们在使用HashMap数组中元素的个数超过了数组大小*装填因子的时候会进行扩容,具体来讲是将容量扩充成原先的两倍,ConcurrentHashMap将扩容操作也进行了细化,他只会扩充某个Segment旗下的HashEntry数组的大小;

ConcurrentHashMap解决fast-fail问题采用的思路有点类似于CopyOnWriteArrayList方式,采用弱一致迭代器的方式,具体来讲的话就是在我们要改变ConcurrentHashMap中的元素时,比如put或者remove时,首先是new出来新的数据而不影响原先的数据,在迭代结束之后将新数据的引用赋给ConcurrentHashMap,这样迭代的是原先老的数据,因此叫做弱一致嘛,这种方式的缺点在于会需要较大的空间开销,因为要暂时存储新元素和旧元素的集合嘛!

LinkedHashMap与HashMap最大的区别在于他默认情况下会保证我们添加到map中值的顺序,其实具体实现还是在HashMap中的put方法里面的,查看put源码会发现执行了recordAccess方法,而这个方法在HashMap里面是没有实现的,而LinkedHashMap继承自HashMap,对该方法进行了覆写,因此执行的就是LinkedHashMap里面的recordAccess方法了,在该方法里面默认情况下是会对插入的数据进行类似于链表排序处理的,当然你可以指定不进行排序,那么此时LinkedHashMap将和HashMap没什么区别了;

        最后来个大总结:

        HashMap和HashTable的区别:

        (1):HashMap是非线程安全的,HashTable是线程安全的,因此HashMap的效率相对来说比HashTable要高;

ConcurrentHashMap来替换;HashTable的迭代器不会出现fast-fail现象;

        (3):HashMap是允许key或者value为null的,而HashTable是不允许的;

        HashMap和LinkedHashMap的区别:

        (1):两者都是非线程安全的,在迭代的过程中都会出现fast-fail现象;

        (2):遍历LinkedHashMap的时候,其输出元素的顺序和我们插入顺序是一致的,但是HashMap在这方面是得不到保证的,他是随机的;

        (3):当HashMap容量很大,但是里面实际元素的个数比较少的时候,使用HashMap遍历起来要比LinkedHashMap慢,因为LinkedHashMap的遍历只与存储的是实际元素的个数有关系,但是和map的存储容量没关系,而HashMap是和存储容量有关系的;但是一般情况下的话,HashMap的遍历效率要高于LinkedHashMap;

        HashMap和TreeMap的区别:

Comparator接口自己实现的,如果没有指定排序器的话,默认是按照key的升序存储的,但是HashMap是随机的,不会按照key的升序进行存储;

        (2):两者都是非线程安全的;

(45):volatile与synchronized的区别

        (1):volatile本质上是告诉JVM当前变量在寄存器中的值是不确定的,需要直接去主存中读取,而synchronized则是锁住当前变量,只能由当前线程来访问该变量,其他等待该变量的线程被阻塞住;

        (2):volatile仅能用在变量级别,但是synchronized可以用在变量、方法级别;

        (3):volatile仅仅能实现变量的可见性,但是不能保证变量的原子性,但是synchronized是可以保证对变量修改的可见性和原子性的;

        (4):volatile不会造成线程的阻塞,但是synchronized会造成线程的阻塞;

        (5):volatile标记的变量不会被编译器优化,即禁止指令重排序,但是synchronized标记的变量可以被编译器优化的;

(46):并发编程的特性

        并发编程的特性:原子性、有序性以及可见性;

        三者只要有一个不满足都会产生线程不安全的错误;

        (1):对于原子性来说的话,Java本身对于基本数据类型的读取和赋值操作是原子操作,如果想要实现更大范围内的原子性的话,需要借助于synchronized以及Lock的参与;

        (2):对于可见性而言,synchronized/Lock以及volatile是都可以保证的,但是保证的措施不一样,对于synchronized以及Lock而言,每次都保证只有一个线程来操作变量,在退出临界区域的时候,会将修改完成之后的变量刷新到主存中,这样的话保证了其他想要访问该变量的线程能够得到最新的值;但是对于volatile来说的话,实现原理不是这样的,一个变量被声明成volatile之后相当于告诉JVM该变量所处线程的本地内存中的值是不稳定的,需要到主存中直接去读取,并且在修改完成该变量之后也需要马上刷新到主线程中;

        (3):对于有序性而言,因为Java内存模型是允许编译器和处理器进行指令重排序的,这对于单线程环境的话是没什么的,但是在多线程环境下的话会出现乱序的结果了,可以采用synchronized以及Lock来保证有序性,当然了volatile是可以保证一定程度的有序性的;

(47):synchronized与Lock的区别

        (46)中介绍了使用和synchronized与Lock都可以实现并发编程中的原子性、可见性、有序性操作,但是两者有什么区别呢?

        (1):Lock是一个接口,里面包含一些常见的操作锁的一些方法,比如lock,unLock.....,而synchronized是属于Java语言的关键字;

ACC_SYNCHRONIZED访问标志是否设置,如果设置了的话,执行线程首先需要获得同步锁,然后执行方法,在方法执行结束之后需要释放掉锁;并且synchronized在出现异常的情况下是会自动释放锁的,并不需要我们手动参与;

        (3):通过synchronized的话,只要有一个线程进入了临界区,其他线程是没机会再进去了,也就是如果进入临界区的线程由于等待IO或者其他原因被阻塞了,他是会一直在那等的,而其他线程又不能进来,导致了资源的浪费,但是Lock就不一样了,因为Lock的种类很多,如果其他线程因为等待某个线程占用的资源而阻塞的时候,他是可以不用一直等下去的,他可以等待一段时间,或者能够响应中断;

        从性能上讲的话,如果资源竞争不是很激烈的话,两者的性能是差不多的,但是如果资源竞争激烈的话,Lock的性能是要高于synchronized的;

(48):Lock锁的分类

        Lock是一个接口,里面定义了一系列的方法,比如:lock/lockInterruptinly/tryLock/unlock等等,正是因为这些方法的存在导致了锁区别于synchronized的一些特点;

        (1):可重入锁,指的是锁具有可重入性,其实就是锁的分配机制,锁是基于线程分配的,而不是基于方法调用,当一个线程执行某个synchronized方法时,比如method1,而在method1里面会调用另外一个synchronized方法method2,此时线程是不需要重新申请锁的,可以直接执行method2,当然method1和method2是处于同一个类里面的;

        (2):可中断锁,如果线程A正在执行临界区代码,而线程B也想进入临界区,可能由于等待时间过长,线程B不想等待了,想处理其他事情了,我们可以让线程N自己中断自己或者由别的线程来中断他,lockInterruptibly方法的作用其实上就是干这个的,当调用这个方法而不是lock方法获取锁的时候,如果等待时间过长的话,我们是可以中断这个线程的;因此我们可以看出来synchronized不是中断锁,但是Lock是可中断锁;

ReentrantReadWriteLock的话,默认情况下是非公平锁,但是可以设置为公平锁;

        (4):读写锁,接口ReadWriteLock定义了两个方法readLock与writeLock用来获取读锁与写锁的,ReentrantReadWriteLock实现了这个接口,如果有一个线程占用着读锁,其他线程想要申请写锁的话,则申请写锁的线程会一直阻塞在那里,等待释放掉读锁;如果一个线程占用了写锁,其他线程想要申请读锁或者写锁的话,其他线程都会阻塞在那里,直到写锁被释放为止;