java面试题<1>
(1):equals和==的区别
equals是用于比较两个实例所指向的内存空间里面的值是否相等;
==用于两个实例是否指向同一内存空间;
(2):String与StringBuffer、StringBuffer和StringBuilder的区别
String与StringBuffer的区别通俗的讲就是常量和变量的区别,String的内容一旦赋值之后是不能修改的,可能你觉得使用“+“运算符是在修改String的值,其实不是这样的“+“其实是要重新在内存里面开辟一块空间来存放最新的String内容的;但是StringBuffer却不是这样的,因为他的底层实现是char数组,所以对于追加、删除之类的操作来说他只是在原有的内存空间上面进行的,因此比较来看,如果你是频繁的进行字符串的拼接或者删除的话,建议还是使用StringBuffer效率更高点,当然StringBuilder也是可以的;
再来说说StringBuffer和StringBuilder的区别,两者最大的区别在于StringBuffer是线程安全的,StringBuilder是非线程安全的,两者的底层实现都是char数组,所以在进行追加、删除操作的时候同样都不会开辟新的内存空间的;
(3):overload和override的区别
overload指的是重载,具体存在于方法名字相同,但是方法参数类型、参数个数、参数顺序至少有一个不同的方法之间,但是除方法返回值之外全部其他都相同的两个方法不能称之为重载,重载可以存在于同类里面,也可以存在于父类和子类之间;
override指的是重写,两个方法称之为重写的条件的:两个方法的名称、参数个数、参数类型、参数顺序都是一样的,并且两个方法分别位于父类和子类,子类中的方法不能缩小父类方法定义的访问权限,也不能抛出比父类方法更多的异常,如果父类的方法前有final关键字修饰的话,那么子类就不能重写该方法;
其中overload重载称为编译时多态,override重写称为运行时多态,他们是多态的两种形式;
(4):抽象类和接口的区别
抽象类属于类,那么很明显继承了他的类就不能再继承别的类了,不然就违反了单继承要求,而接口却可以多继承、多实现;
抽象类里面可以有抽象方法和实现方法,但是接口里面的方法必须是抽象的;
抽象类里面可以有构造函数,但是接口里面不可以;
抽象类里面的抽象方法可以用public、protected修饰,但是接口里面的方法都是public类型的;
抽象类中可以定义public、private、protected类型的变量,但是接口里面的变量只要你定义了默认都是public static final类型的;
(5):hashCode与equals的区别
(6):HashMap实现原理
HashMap是数组+链表实现的,既然用到hash散列,那么肯定不可避免的会出现冲突问题,HashMap解决冲突的方法是拉链法,因为这里有用到数组,那么当容量不足的时候就需要进行扩容操作了,在HashMap中有个术语叫冲突,当冲突几率越来越高的时候就需要进行扩容操作了,那什么情况就叫冲突几率高呢?就是当我们的数组元素个数超过了数组原先大小*装填因子,默认情况下的装填因子是0.75,扩容有个坏处就是每次扩容之后都必须重新计算原先数组中的元素在新数组中的存储位置,这点比较消耗性能,所以一般情况下如果你已经能够确定最大需要多大散列范围的数组的话,建议还是能够指定大小;
接下来就是HashMap的put和set原理了:
put操作和set操作进行操作的对象是主要是key,如果你查看源码的话会发现value只是跟着key的步伐在走而已,并没有实质性的进行操作,对于put操作,首先会计算出当前key对应的hash值,接着找到计算出来的hash值在数组中的下标位置,查看该下标位置处对应的链表是否为null,为空的话直接将当前键值对插入到该链表首位,不会执行当前key对象的equals方法;如果下标位置处对应的链表不为null的话,会通过for循环来通过key的equals方法来查看这个链表中有没有与当前键值相等的键值对Entry存在,有的话,会用当前值替换原先这个键值对的value值,遍历结束如果不存在的话,会将当前键值对插入到链表的头部,这个就是put过程了;
get操作过程思想可以借助于put过程,首先会计算出当前key值的hash值,接着找到此hash值在数组中的位置,找到这个位置对应的链表,接着通过for循环遍历这个链表,遍历过程中调用equals方法查看有没有等于当前key的键值对存在,有的话直接返回这个键值对对应的value值即可;
HashMap数据结构原理图:
HashMap注意点:
HashMap是非线程安全的,也就是说你在使用迭代器的过程中有其他线程修改了map的话,你的程序可能会抛出ConcurrentModificationException异常,这就是我们常见的fail-fast机制了,原因在于我们在调用HashMap的迭代器里面的每个方法的时候,都会通过判断原先map被修改次数和当前被修改次数是否相等,不等的话直接就抛出了ConcurrentModificationException异常了,这点在ArrayList里面使用迭代器也会出现,具体解决方法就是使用ConcurrentHashMap代替HashMap了;
HashMap是允许你的键或者值为null的;
HashMap是不能保证随着时间的推移,你里面元素之间的顺序不变,原因就在于map中存放hash值的数组在扩容的时候会重新计算原先元素在新数组中位置的;
(7):HashTable和HashMap的区别
下面分别从线程安全性、键值是否允许为空、效率几个角度进行区分:
HashTable是线程安全的具体线程安全这点是怎么实现的呢?通过synchronized关键字,这也就是后来他被ConcurrentHashMap替代的原因了,效率不是很高呗,每次想要对HashTable进行更新操作都需要获得同步锁这点很烦人的,而且锁住的是整个hash表,但是ConcurrentHashMap的实现就比较灵活了,他每次锁住的是你将要操作的那个hashtable表,具体原理分析在问题(8)给出;而HashMap是非线程安全的;
HashTable不允许key或者value值为null,但是HashMap是允许的;
因为HashTable在使用的时候涉及到了加锁操作,势必会带来性能上的损失,效率是不如HashMap的,如果你已经明确知道是在单线程环境下使用map的话,建议直接使用HashMap;
HashTable使用的是Enumerator迭代器,不是fail-fast机制的,而HashMap使用的是Iterator迭代器,会出现fail-fast现象;
(8):ConcurrentHashMap实现原理
从JDK1.5开始就出现了ConcurrentHashMap类用来解决HashMap非线程安全这个问题,虽然之前已经有HashTable的存在,但是因为他是使用synchronized锁住整个Hash表的,这样会带来很多性能上的不足,而ConcurrentHashMap将锁粒度细化,因此效率上相对来说更优,他的实现原理是这样的:一个ConcurrentHashMap由多个Segment组成,每一个Segment是包含了一个HashEntry数组的小HashTable,每一个Segment包含了对自己Hashtable的一组操作,比如put,get之类的,他的数据结构原理图是这样子的:
你会发现ConcurrentHashMap其实是通过Segment将大的HashTable分割成一个个的小HashTable了,这样做的目的是为了将加锁操作作用在小的HashTable上面,不至于把整个HashTable全都锁起来;
我们来看看他的put方法是怎么执行的,首先计算出当前key值对应的hash值,找到该hash值对应的Segment位置,接下来调用的就是该位置Segment的put方法了,进入这个方法就会进行加锁操作,也就是从上面的绿色部分开始进行加锁操作,在Segment的put方法里面会传入刚刚key值的hash值,通过(tab.length - 1) & hash获得当前hash值在当前Segment对应的HashEntry数组中的位置,找到HashEntry数组的位置之后获得该位置处对应链表的第一个HashEntry值,接着就开始通过equals方法来看看该链表中是否有与当前key值equals的键值对存在了,有的话直接替换值,没有的话插入到当前链表的第一个位置,这点和HashMap是一致的,最后在put方法结束的finally中释放掉锁就可以了,这个是put过程,至于get过程和put过程原理类似,不再阐述;
(9):HashSet实现原理
在HashSet中你会发现存在一个属性类型为HashMap的变量,而且他的所有方法的真正操作都是这个map来执行的,也就是说他底层实现其实就是HashMap了,那么既然作为Set,它里面的元素是肯定不能相等的,这里的相等指的是两个对象的执行hashCode和equals方法之后的返回值都一样,那么HashSet是怎么保证这一点的呢?通过把要加入HashSet的值作为HashMap的key值来保证,我们来看看HashSet中的add源码:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
很简单,你平常所add进来的值其实是以key的形式存在在HashMap中的,而这个key对应的value是PRESENT,这个值的定义是:
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
从上面的注释就可以看出来PRESENT只是一个虚设的,只是为了保证key值对应的value值不为null而已,除此之外没什么作用;HashSet充分利用了HashMap的key值唯一这个特点进行重复元素的筛选,还有一点需要注意add方法的返回值表示的是HashMap中是否存在对应元素的key,不存在返回true,存在返回false;
(10):ArrayList和LinkedList对比
ArrayList的底层实现是动态数组,因而他的空间开销主要体现在需要为数组的扩充而预留的空间上;LinkedList的底层实现是双向链表,因而他的主要开销体现在需要存储指向前后元素的指针上面;
ArrayList的底层实现是数组,也就造成了他适合随机访问的有点和不适合插入删除的缺点;LinkedList的底层实现是链表,也就造成了他适合插入删除的有点和不适合随机访问的缺点了;
LinkedList实现了Deque接口,那么他也就可以用来作为队列以及双端队列使用了;
ArrayList和LinkedList都是非线程安全的,那么相应的在多线程环境下使用的过程中他们都可能会出现fail-fast现象抛出ConcurrentModificationException异常,如果在多线程环境下使用他们的话,可以用CopyOnWriteArrayList代替ArrayList,用ConcurrentLinkedQueue代替LinkedList;
因为ArrayList的底层是采用动态数组实现的,所以不可避免的在使用的过程中当数组容量不足的话就要进行扩容,而扩容的过程中将伴随着数组元素的拷贝,这点相对来说是是有损性能的,所以如果你已经明确知道最大要多大的容量最好是在初始化ArrayList的时候直接指定大小;而LinkedList是不存在这个问题的,毕竟是链表嘛,你内存有多大我就可以死劲添加元素,并不用关心我自己到底要放多少元素啦;
具体更详细的区别可以看我的这篇博客;
(11):Java内存模型
在计算机中鉴于处理器的处理速度要远远高于主存也就是我们常说的内存,因此引入了高速缓存的概念,他有效的缓和了处理速度与存储空间大小之间的矛盾,这点在单处理器环境下是没什么问题的,但是换做多处理器的场景也就是我们现在所说的多核情况下,问题就出现了,大家用各自的处理器高速缓存进行计算但是内存空间都是共享的,所以经常会出现数据不一致问题,因此计算机领域存在着缓存一致性的协议,在我们的java内存模型中,同样也能见到类似的身影,java内存模型中规定变量要存储在主内存中,但是不同的线程内部都有各自的工作内存,这点就类似于前面说到的处理器内部的高速缓存了,在本地内存中存储的是该线程自己用到的主内存变量的副本,线程操作的变量都是自己本地内存的,不能直接去操作主内存的变量,也不能访问另一个线程内部的本地内存里面存储的变量,两个线程之间变量值的传递是需要主内存做中介的,这有点类似于在河两边的人想要达到各自对岸的话都必须通过桥才能实现,这里的桥就是主内存了;也正是因为这个原因,所以会导致多线程之间如果操作同一个变量的话会导致变量的值不确定;
(12):Java中引用的类型
java中将引用分为强引用、软引用、弱引用、虚引用:
强引用:只要强引用存在,垃圾回收器就不会对它进行回收;
软引用:对于软引用对象,在系统发生内存溢出之前,将会把这类引用对象列为回收范围对象进行第二次垃圾回收,如果这次回首之后还没足够的内存,才会抛出内存溢出;
弱引用:被引用的关联对象只能生存到下一次垃圾回收之前,在下次垃圾回收的时候,不论当前内存释放足够,都会回收掉被弱引用关联的对象;
虚引用:一个对象有没有虚引用完全不会影响该对象的生存时间,为一个对象设置虚引用的唯一目的就是在该对象被回收之前能够收到一个系统通知而已;
(13):JVM垃圾回收原理
谈到垃圾回收,需要搞清楚两个问题就可以了,什么样的对象需要回收,怎样进行回收;首先来说说什么样的对象需要回收,Java的内存区域被分为各个部分,对于程序计数器、本地方法栈、虚拟机栈这三个区域来说,因为是属于线程所独享的,随线程的产生而产生,随线程的灭亡而灭亡,因此这部分的内存空间是不需要垃圾回收器关心的,但是对于堆区和方法区来说,情况就不一样了,因为一个接口的多个实现类占用的内存可能是不同的,一个方法的不同分支占用的内存也可能是不同的,他们占用内存的大小是需要在运行的时候才能确定的,这才是垃圾回收器所关心的地方,那么确定哪些对象需要回收之前采用的是引用计数法,但是这种方法有个很大缺点,就是很难解决对象循环引用的问题,HotSpot虚拟机也没采用这种方法,后来出现了可达性分析算法,这种方法通过将一些称为"GC Root"的对象作为起始点,当一个对象到"GC Root"没有任何引用链相连的时候称为对象不可达,那么这些对象就是需要垃圾回收器回收的对象了,重点就是哪些对象可以作为"GC Root"了,java语言规定可以作为"GC Root"的由虚拟机栈中所引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象;解决了要回收哪些对象之后,我们就该聊聊怎样进行回收了,因为方法区中存储的是类信息、静态变量、常量,HotSpot虚拟机将他称为了永久代,也就说明在该区域发生垃圾回收的操作不是很频繁;早期的垃圾回收算法是标记-清除算法,该算法的缺点在于标记清除过程效率不是很高,同时会导致碎片太多,无法存储较大对象而不得不进行频繁的垃圾回收操作,而大家都知道垃圾回收过程会出现"Stop the World"现象,导致当前任务停止等待垃圾回收完成,影响系统运行效率,随后出现的复制算法虽然解决了标记清理效率低以及碎片化严重的问题,但是却造成了内存空间没有完全利用起来的缺点,因此HotSpot虚拟机采用了两者的结合体来实现对堆内存的垃圾回收操作,具体来说他将堆内存分为新生代和老年代,采用分代收集算法来进行垃圾回收操作,新生代的GC操作称为Minor GC,老年代的GC称为Major GC,两者合起来称为Full GC,新生代他的回收操作比较频繁因而采用复制算法进行垃圾回收,老年代因为回收操作不是很频繁,因而采用标记-整理算法进行垃圾回收,新生代又按8:1:1的比例分成了Eden区、From Survivor区、To Survivor区,为了防止多个线程同时分配内存带来的问题,又会将Eden区分为一个或者多个本地线程缓存(TLAB),每个线程占用其中之一,注意一点就是新生代的Survivor区域至少有一个处于空闲状态,刚开始整个新生代都是处于空闲状态的,当我们new对象的时候会将该对象放到Eden区域,这时候两个Survivor区域都是空闲的,当继续new对象出现Eden区域出现空间不足的时候,会触发Minor GC操作,这时候会通过可达性分析算法查看Eden区哪些对象还是存活的,将这些存活的对象存放到From Survivor中,如果From Survivor中存放不下这些存活的对象的话,会直接将多出来的对象存放到分配担保区域,也就是老年代中,同时会将Eden区域清空,这时候From Survivor区域是由对象的,To Survivor区域是空闲的,接着我们继续new对象的时候,同样会将对象放在Eden区域,当Eden区域再次满的时候,同样会触发一个Minor GC操作,这次Minor GC操作将会把Eden区域和From Survivor区域通过可达性分析算法计算出存活的对象移到To Survivor中,如果这时候To Survivor不足以存放下这么多存活对象的话,同样也会直接将多余的对象直接存放到分配担保区域老年代中,接着将Eden区和From Survivor区域清空,这样From Survivor和To Survivor区域交替空闲,并且老年代做分配担保来完成新生代的垃圾回收操作;至于老年代的垃圾回收操作就相对来说简单多了,采用标记整理算法,将存活的对象移到一边,然后直接清理掉端边界意外的内存就好啦;
(14):字节流和字符流的区别
(1):字节流是以字节为单位来进行IO操作的,java中有字节流有关的最大的两个父类是InputStream和OutputStream两个抽象类;字符流是以字符为单位进行IO操作的,java中与字符流有关的最大的两个父类是Writer和Reader两个抽象类;
(2):对字节流的操作是不会用到内存缓冲区的,是直接对文件本身进行操作;而对字符流的操作需要用到内存缓冲区,原因在于对字符流的操作本质上是对字节流的操作,所以用到内存作为缓存将数据先暂存到内存中,然后直接从内存中获取,可以避免多次IO操作,提高效率;
(3):磁盘上的文件都是以字节的形式存在的,而字符值是在内存中才会形成的,也就是记住一点,字符的底层是字节就可以了;
(15):sleep()和wait()的区别
(1):两者位于不同的类中,sleep位于Thread类中,wait位于Object类中;
(2):调用sleep方法之后,线程是不会释放对象锁的,到达指定时间之后会自动苏醒;调用wait方法之后线程会释放对象锁;
(3):sleep之后不让出系统资源,但是wait之后是会让出系统资源的,其他线程可以占用释放的资源,如果wait之后想要重新获得系统资源,只能通过notify唤醒线程,让他有机会重新获得CPU时间片;
(16):throw和throws的区别
(1):throw代表动作,表示抛出异常的动作;throws表示一种状态,表示方法可能会抛出异常;
(2):throw用在方法实现中,throws用在方法声明中;
(3):throw只能抛出一种异常,而throws可以抛出多个异常;
(17):java多态实现原理
实现原理靠的是父类或者接口中定义的引用变量可以指向子类或者具体的实现类的实例对象,而程序调用的方法是在运行的时候动态绑定的,即引用变量所指向的具体事例对象的方法也就是当前内存中正在运行的那个对象的方法,而不是引用变量的类型中定义的方法,通俗点讲JVM是通过对象的自动向上转型来实现多态的;
(18):java面向对象的三大特性
封装、继承、多态
(19):Collection与Collections的区别
Collection<E>是Java集合框架中的基本接口;Collections是集合框架提供的一个工具类,其中包括大量的静态操作方法;
(20):Java中进行线程同步的方法有哪些呢?
(1):使用volatile关键字,保证对同一volatile变量的修改操作happens before对他的读操作;
(2):使用synchronized,可以对一个代码块或者方法上锁,被锁住的地方称为临界区,需要获得对象的monitor才能进入临界区,以后再进入临界区的线程会因为无法获得monitor而被阻塞,直到释放monitor之后其他线程才能进去,注意一点的就是由于等待另一个线程释放monitor而被阻塞的线程是无法被中断的;
(3):使用ReentrantLock可重入锁,尝试获取锁的线程可以被中断并且可以设置超时参数;
(21):TreeMap, LinkedHashMap, HashMap的区别
HashMap与LinkedHashMap的区别主要体现在LinkedHashMap保存了记录的插入顺序,在使用迭代器遍历LinkedHashMap的时候,先得到的记录肯定是先插入的,但是在遍历的时候会比HashMap慢点,除了一种特殊情况,就是HashMap容量很大,但是实际数据较少,遍历的时候速度可能会快于HashMap,因为LinkedHashMap的遍历速度只与实际数据的个数有关系,与Map的容量是没有关系的,而HashMap的遍历速度是与他的容量有关系的;
HashMap和TreeMap的区别主要体现在:TreeMap能够把他保存的记录根据键值排序,默认是按照键值的升序排序,也可以指定排序的比较器,当用迭代器遍历TreeMap的时候得到的记录是排过序的;
(22):Java中堆和栈的区别
(1):Java堆是线程共享的,他的空间回收需要垃圾回收器的参与,Java栈是线程独享的,他会随线程的产生而产生,随线程的灭亡而灭亡,这部分内存空间的回收不需要垃圾回收器的参与;
(2):Java堆中存放的是对象实例,Java栈中存放的是局部变量表、操作数栈、动态链接、方法出口信息;
(23):ArrayList与Vector的区别
ArrayList与Vector都是采用动态数组的方式实现的,两者的最大区别在于,ArrayList是非线程安全的,要想在多线程下使用它,需要使用他的升级版本CopyOnWriteArrayList,而Vector是线程安全的,它内部的众多方法前面都有synchronized修饰,用于保证同一时刻只有一个线程可以访问他;
(24):简单介绍下Java反射
我们平常要想获得对象的话,往往是通过new的方式实现的,这个对象的获得实际上是JVM虚拟机帮我们解析Class字节码文件在运行的时候构建出来的,而反射是指我们在运行时根据一个类的Class对象来获得他的定义信息,比如类的方法、属性、父类等信息的机制,我们知道javac会将.java文件编译成.class文件,这个.class中就包含类的一些定义信息了,比如父类、接口、构造器等等,.class文件在运行的时候会被ClassLoader类加载器加载到java虚拟机中,当一个.class文件被加载后,JVM会为之生成一个Class对象,我们在程序中的new操作实际上是根据相应的Class对象构造出来的,确切的讲,这个Class对象实际上是java.lang.Class<T>泛型类的实例,因为Class类没有提供公有类型的构造器,所以我们一般是通过Class.forName的方式获得他的,有了Class对象之后,我们便可以利用该Class对象获得构造器,具体可以通过getConstructors获得所有public类型的构造器,通过getDeclaredConstructor获得所有类型的构造器,包括public/private类型的,然后调用这些构造器的newInstance方法便可以创建一个该类型的实例出来了,通过同样的方法,我们也可以通过Class对象获得该类的方法、属性、父类、接口等信息,此外我们可以通过将方法调用setAccessible(true)使得某个private方法能够通过反射访问到,正是因为setAccessible的存在,导致了反射操作会破坏单例模式;
(25):Java深拷贝与浅拷贝
浅拷贝:会创建一个新的对象,这个对象有着和原始对象属性值的一份精确拷贝,如果属性值是基本类型的话,拷贝的就是基本类型的值;如果属性是引用类型的话,拷贝的是内存地址,因此如果一个对象改变了这个地址里面的值的话也会影响到这个拷贝的对象;
深拷贝:不仅仅会对对象属性中的基本数据类型进行复制操作,也会对引用类型进行复制操作,这里的复制将不再仅仅是拷贝内存地址了,而将创建的是完完全全的新对象,只不过这个对象里面的值和之前对象里面的值一样而已了;
我们平常见到的克隆实际上是浅拷贝的,如果想要实现真正意义上的拷贝,我们需要将对象中的所有引用类型全部实现Cloneable接口里面的clone方法,这点对于类结构比较复杂的操作来说未免太麻烦了,一个有效的解决方案就是使用序列化的方式实现,序列化创建出来的对象将是和原先对象属性值完全相同的对象,注意一点的是,序列化也会破坏单例模式;
(26):Java动态代理实现原理
代理模式主要用于一些权限方面的限制功能,比如服务端并不想让某些用户直接访问自己,那么就可以抽象出来一个代理用户,其他用户想要访问自己的时候首先需要通过这个代理用户的检查,看他有没有资格访问服务端,有的话才会放行;代理分为静态代理和动态代理,静态代理是我们自己实现的而动态代理是在程序的运行期间动态生成的;在代理模式中最关键的就是需要代理和服务真正提供者有相同的功能,代理中有真正服务的对象,实际上代理中的操作还是真正的服务对象在操作,实现这点要求有两种方式:一种是代理和真正服务提供者实现相同的接口,这也就是JDK为我们实现动态代理所采用的方式,一种是代理类继承自真正服务提供者类,并且可以重写里面的方法,这也就是CGLIB实现动态代理所采用的方式;我们只看JDK实现动态代理的原理,在我们平时的使用中,只需要通过Proxy.newProxyInstance方法创建一个代理类实例,接着使用这个代理类实例就可以进行真正的操作了,其实在创建代理类实例的时候我们会传入一个实现了InvocationHander接口的对象,该对象实现了接口中的invoke方法,并且该对象中是有一个真正服务提供者的引用的,我们在调用代理类的某些方法的时候实际上调用的是实现了InvocationHandler接口的类实例的invoke方法,该invoke方法会通过接口中的真正服务提供者的引用来调用真正服务者的方法,简单点的话,我们可以这样理解,动态代理实际上就是在代理和真正服务者之间又增添了一个代理一样,这个代理就是实现了InvocationHandler接口的对象,我们可以将其称为中介类,动态代理实际上就是两组静态代理的组合,我们调用代理类方法的时候,实际上该代理类会去调用中介类的invoke方法,这个方法会通过反射的方式调用中介类中的真正服务类的方法,这就是Java动态代理的实质;
(27):简单介绍下单例模式及其注意点
单例模式从类加载的时候是否会创建实例的角度可以分为恶汉式和懒汉式,恶汉式是不会加载的,只有在用的时候通过显式的方法调用才会创建实例,懒汉式是会加载的,在类被加载的时候就会被创建出来的;
我们常见的单例模式实现方式是双重校验锁方式,具体实现代码见下:
class SingletonMode
{
private static SingletonMode instance;
private SingletonMode(){}
public SingletonMode getInstance()
{
if(instance == null)
{
//这里可能对于不同的线程存在不同时间长度的耗时操作
synchronized (SingletonMode.class) {
if(instance == null)
instance = new SingletonMode();
}
}
return instance;
}
}
这里我们有几点需要注意一下:
(1):单例模式中的构造函数是private类型的,也正是因为是private类型,所以才能保证外界无法通过new关键字创建实例;
(2):在上面的单例模式中,我们采用的是恶汉式,也就是说只有你显式的调用getInstance方法才有可能创建SingletonMode实例;
(3):上述方法中,我们对instance是否为null进行了两次判断,很多人都觉得进行一次判断就可以了,其实不是这样的,假如现在有两个线程,线程1和线程2,线程1执行getInstance方法的时候,会发现instance的值是null,那么他会进入if语句块中,接着开始执行一些耗时操作了,但是此时呢,线程2也同样调用了getInstance方法,因为线程1一直在执行耗时操作,所以并没有执行synchronized语句块,那么此时instance的值是null的,同样也会进入if语句块中,但是线程2不需要执行耗时操作,那么他就会获得SingletonMode的对象锁,如果不存在第12行通过if判断instance是否为null的话,则直接创建了instance对象出来,执行结束之后释放掉了SingletonMode对象锁,接着线程1执行完耗时代码之后,同样也会获得SingletonMode对象锁,那么不存在第12行代码的话,他同样也会创建一个instance值出来,这显然是违背了单例模式的,所以我们通常采用双重校验锁的方式,加入了第12行代码;
(4):在一些情况下可能会破坏单例模式,比如序列化反序列化的过程中,具体解决方法是在单例类中实现readResolve方法,该方法是private类型的,在该方法中返回instance实例就可以了;反射也会破坏单例模式,因为虽然我们把单例类的构造方法设置成了private类型,但是我们可以通过setAccessible方法将该构造函数设置成外界可以访问,这样外界可以随便访问了,自然也就不是单例了,解决措施是在第二次创建的时候抛出已经存在实例的异常,java中就是这么做的;
(5):在<Effective java>中提到了使用枚举来实现单例模式,这种方法是可以防止序列化反序列化以及反射破坏单例的,也是这本书的作者多提倡我们使用的单例模式方法;
(28):关于String的intern关键字
JDK1.6和JDK1.7中intern关键字是有区别的,原因在于JDK1.7中将常量池移出了堆内存中;
在JDK1.6中,调用String的intern方法的时候,会去常量池中查看是否有与当前String值相同的String存在,有的话则直接返回常量池中的这个引用就可以了,如果不存在的话,则会在常量池中创建一个和这个String相同的String出来;
在JDK1.7中,在调用String的intern方法的时候,首先会去常量池中查看有没有和当前String值相同的String存在,存在的话直接返回常量池中这个String值的引用就可以了,如果不存在的话,则不会像JDK1.6那样在常量池也创建一个和当前String相同的String出来,而是会将原始堆中当前String的引用存储在常量池中而已;
(29):synchronized与volatile的区别:
并发编程有三大特性,原子性、可见性和有序性,当然在java中的并发编程也应该是满足这三个特性的,针对原子性来说,java语言本身对于基本数据类型的变量读取和赋值操作是原子性的,但有一点需要注意,在32位平台下,对64位数据的读取和赋值是通过两个操作来完成的,不能保证其原子性比如double类型,对于更大范围的原子性,java是通过synchronized或者Lock来实现的,原因在于synchronized与Lock能够保证同一时刻只有一个线程可以访问代码块,但是volatile是不能够确保原子性的;对于可见性,java中是使用volatile关键字来达到目的的,他会保证变量被修改的值立即被更新到内存中,当有其他线程需要读取的时候,直接从内存中读取就好了,取到的值将是新值,但是对于普通变量是不能够保证可见性的因为普通变量被修改后什么时候写入内存的时间是不固定的,其他线程去读取该变量值的时候可能读到的是旧值,具体的原因在于java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的本地内存(这里的本地内存可以类比为CPU与主存之间的高速缓存),线程的本地内存中存放着该线程所使用到的主内存变量的拷贝,线程对变量的所有操作都必须在本地内存中进行,而不能直接读取主内存中的变量,不同线程之间无法直接访问对方本地内存中的变量,线程间变量值的传递均需要主内存的参与,因此在多线程情况下如果两个线程共享某一变量,一个线程可能还未在另一个线程写入新值就读出里面的内容,导致了内容不一致,而使用volatile修饰的变量会强制将修改的值直接写入主内存中而不是线程自己的本地内存;对于有序性的话,synchronized和Lock是可以保证的,因为每一时刻都只能有一个线程访问同步代码,自然能够保证有序性,而volatile能够保证一定的"有序性";
首先应该知道的一点,正是因为编译器为了加快程序的运行速度,对变量的写操作首先是在自己的本地内存中进行的,而这部分内容自己自己知道,其他线程根本没法感知,因而要写入主内存,正是因为这一过程被分成两个步骤导致了线程*享变量的不同步,而volatile就是要求线程对该修饰符修饰的变量的操作直接写入主内存中,将两步操作直接变为一步完成;
(1):volatile本质是告诉JVM当前变量在寄存器中存储的值是不确定的,需要直接从主存中读取;synchronized则是锁住当前变量,只有当期线程可以访问该变量,其他访问该变量的线程只能阻塞;
(2):volatile只适用于变量级别,而synchronized适用于方法、变量中;
(3):volatile仅能保证对变量修改保证可见性,但是不能保证原子性;synchronized对变量的修改则可以保证可见性和原子性;
(4):volatile不会造成线程阻塞,而synchronized可能会造成线程阻塞;
(5):volatile标记的变量不会被编译器优化,而synchronized标记的变量可能被编译器优化;
(30):synchronized与Lock以及Atomic的区别
synchronized是在JVM层面上实现的,在代码执行时出现异常后,JVM会自动释放锁定,但是Lock不可以,Lock是通过代码实现的,要保证锁一定会被释放,就必须将unLock()放到finally{ }语句块中;
在资源竞争不是很激烈的情况下,synchronized的性能要优于ReentrentLock,但是当资源竞争激烈的情况下,synchronized的性能会下降,而ReentrantLock能够维持常态;
在资源竞争不是很激烈的情况下,synchronized的性能要优于Atomic,但在资源竞争激烈的时候,Atomic能够保证常态,并且性能也好于ReentrantLock,有一个缺点就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多余一个同步无效,因为他不能在多个Atomic之间同步;
(31):synchronized和ReentrantLock的区别:
Reentrant除了synchronized有的功能之外,还多了几个高级功能:
synchronized会在进入同步块的前后分别形成monitorenter和monitorexit字节码指令,在执行monitorenter指令时会尝试获取对象的锁,monitorenter指令执行时会让对象的锁计数加1,monitorexit指令执行时会让对象的锁计数减1;
(1):中断锁:在持有锁的线程长时间不释放锁的时候,等待锁的线程可以选择放弃等待,tryLock(long timeout,TimeUnit unit),当然对于中断锁可以选择忽略或者响应;
(2):公平锁:按照申请锁的顺序来依次获得锁,而synchronized是非公平锁,公平锁可以通过ReentrantLock的构造函数实现,new ReentrantLock(boolean fair);
(3):绑定多个Condition:通过多次new Condition可以获得多个Condition,可以简单的实现比较复杂的线程同步功能;
(32):Java中的final关键字
final表示"不可修改"的意思,可以用来修饰非抽象类、非抽象方法和变量;
final修饰非抽象类:表示该类不可以被继承,没有子类,final类中的方法默认是final类型的;
final修饰非抽象方法:表示该方法不可以被子类重写,但可以被继承;
final修饰成员变量:表示常量,只能被赋值一次,赋值后不再改变;
final不能修饰构造方法,父类的private成员方法是不能被子类方法覆盖的,因此private类型的方法默认是final类型的;
(33):Java中的static关键字
static表示"全局"或者"静态"的意思,可以用来修饰成员变量和成员方法,也可以形成static代码块,但是java中没有全局变量的概念,被static修饰的成员变量或者方法是独立于该类的任何对象的,也就是说他不依赖于类特定的实例,被所有的实例共享,只要这个类被加载,虚拟机就可以根据类名在运行时数据区中的方法区中找到他们了,被static修饰的成员变量或者成员方法可以通过类名访问,注意一点的就是在static修饰的成员方法只能访问所属类的静态成员变量与成员方法,不能访问所属类的实例变量和实例方法;static默认情况下是不能修饰类的,但他可以修饰内部类,默认情况下不用static修饰的内部类会隐式包含有外部类的引用,但是静态内部类中不包含外部类的引用;
(34):start()方法和run()方法的区别
用过java线程的都应该知道这两个方法了,一般我们是通过start方法来开启线程的,但是最终执行的还是线程中的run方法了,因为run方法一般都是public,所以外界也可以直接调用run方法了,两者有什么区别呢?
确切的讲,调用start方法是会出现多线程的调用start方法之后线程处于可运行的状态,只要CPU给他时间片了,他便可以执行run方法了,但是调用run方法仍然还是只有一个主线程,和普通的方法调用是没什么区别的,只不过名字是run而已了;
(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采用的是"标记-清除"算法,那么势必会导致内存碎片的出现,导致某一时候无法找到足够大的连续空间来存放较大对象;
(39):Java内存模型
Java内存模型可以类比于处理器模型,我们都知道计算机系统为了加快读写速度引入高速缓存来缓解内存与处理器之间处理速度的量级差别的,将运算需要使用的数据复制到缓存中,让其快速运行,当运算结束之后再从缓存中将数据写回内存中,这样处理器是直接和高速缓存打交道的,不用等相对缓慢的内存读写数据了,这在单处理器情况下是不会有什么问题的,但是到了多处理器情况下就会出现缓存一致性问题了,不同的处理器各自有各自的高速缓存,但是他们之间却是共享主存的,那么势必会出现某一处理器已经修改了某一值,但是还没从自己的高速缓存中写到主存中,另一个处理器的高速缓存却从主存中取出了该值原先的值,造成了读取脏数据的情况出现,对于这种情况,需要缓存一致性协议来进行保障,Java中的内存模型其实和处理器的这种模型是有可比性的,我们可以把Java内存模型中的栈类比为处理器,在栈中存在一个本地工作内存区域,我们可以把这个本地工作内存区域看成是处理器模型中的高速缓存,把堆类比为处理器模型中的主存,每个线程在自己的栈空间中都存在一个本地工作内存区域就相当于每个处理器都有自己的高速缓存一样,栈与栈之间不能进行数据共享,他们要想共同操作数据,那么就需要堆空间的参与,这就会出现和处理器模型类似的一致性问题了,不同的线程栈空间是不可以直接互相访问的,线程间变量值的传递是需要主内存的参与完成的;
在Java内存模型中,为了能使得变量从主内存拷贝到每个线程栈的工作内存或者从每个线程栈的工作内存拷贝到主内存中,为我们定义了八种操作:lock/unlock/read/load/use/assign/save/write
具体这八种操作的关系见下:
图片来自于http://blog.csdn.NET/cauchyweierstrass/article/details/45751973
这八个操作使用起来是有规则的:
(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线程池实现原理
见博客:我眼中的Java线程池实现原理
(41):Callable和Runnable的区别
(1):Callable是可以有返回值的,具体来讲是他的接口中的call方法可以返回值,这个返回值我们可以通过实现Future的接口对象的get方法来获得;Runnable是没有返回值的,也就是说Runnable的run方法没有返回值;
(2):Callable在使用的过程中是可以抛出异常的,而Runnable是不能抛出异常的,也就是在使用Runnable的时候,我们必须自己处理可能产生的异常;
如果想更多的了解两者在源码方面的实现以及如何使用,可以查看博客:Callable与Runnable的区别及其在JDK源码中的应用
(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了,说白了就一个嘛;
(5):HashMap是非线程安全的,会出现fail-fast现象,要想使用多线程版本的HashMap,我们可以使用Collections.SynchronizedMap或者JUC下面的ConcurrentHashMap或者HashTable来实现,三者从效率上讲的话ConcurrentHashMap是最高的,而SynchronizedMap与HashTable都是通过Synchronized关键字实现的,相对来说效率不是很高;
(6):TreeMap不同于HashMap的一点在于,TreeMap是可以按照key来进行排序的,至于排序规则的话,我们在创建TreeMap的时候可以传入一个实现了Comparator的比较器,比较规则我们可以自己制定,如果不指定比较器的话,将以ket值默认的顺序进行排序,对于key值是Integer类型的话,我测试了下将按照升序的方式进行排序,也正是因为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是一致的;
(8):ConcurrentHashMap的实现原理是采用锁分离技术,他实际上是对HashTable的一种改进措施了,如果你查看HashTable源码的话会发现在每次put和get操作的时候,都会采用synchronized关键字锁住整个HashTable表的,这在性能上不是很高的,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,这样迭代的是原先老的数据,因此叫做弱一致嘛,这种方式的缺点在于会需要较大的空间开销,因为要暂时存储新元素和旧元素的集合嘛!
(9):最后就是LinkedHashMap了,LinkedHashMap与HashMap最大的区别在于他默认情况下会保证我们添加到map中值的顺序,其实具体实现还是在HashMap中的put方法里面的,查看put源码会发现执行了recordAccess方法,而这个方法在HashMap里面是没有实现的,而LinkedHashMap继承自HashMap,对该方法进行了覆写,因此执行的就是LinkedHashMap里面的recordAccess方法了,在该方法里面默认情况下是会对插入的数据进行类似于链表排序处理的,当然你可以指定不进行排序,那么此时LinkedHashMap将和HashMap没什么区别了;
最后来个大总结:
HashMap和HashTable的区别:
(1):HashMap是非线程安全的,HashTable是线程安全的,因此HashMap的效率相对来说比HashTable要高;
(2):HashMap的迭代器是会出现fail-fast现象的,我们可以用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的区别:
(1):TreeMap是可以按照key进行排序的,排序规则是我们通过实现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语言的关键字;
(2):两者实现的层面是不同的,Lock是在代码层面实现的,也就是我们需要手动执行lock()方法获得锁,同时需要我们在使用结束之后自己手动释放掉锁,调用的是unLock()方法,因为可能在使用锁的过程中出现异常,所以释放锁的操作一般是放在try{ } catch语句的finally语句部分的,如果不释放锁的话会出现死锁现象;而synchronized是在JVM层面实现的,JVM会在synchronized语句块中的前后多出两条指令,monitorenter和monitorexit,monitorenter指令执行的时候会让对象的锁计数加1,而monitorexit指令执行的时候会让对象的锁计数减1,而对于synchronized修饰方法的话,他的同步是隐式的,他实现在方法调用和返回操作之中,虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否是同步方法,当调用方法时,调用指令将会检查方法的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是可中断锁;
(3):公平锁,尽量以请求锁的顺序来获取锁,比如有很多个线程在等待一个锁,当锁被释放的时候,等待时间最长的线程将优先获得锁,这就是公平锁,非公平锁是无法保证锁的获取是按照请求锁的顺序的,这可能会导致某个或者一些线程永远获取不到锁的情况发生;在Java中,synchronized是非公平锁,无法保证等待线程获取锁的顺序,而对于ReentrantLock或者ReentrantReadWriteLock的话,默认情况下是非公平锁,但是可以设置为公平锁;
(4):读写锁,接口ReadWriteLock定义了两个方法readLock与writeLock用来获取读锁与写锁的,ReentrantReadWriteLock实现了这个接口,如果有一个线程占用着读锁,其他线程想要申请写锁的话,则申请写锁的线程会一直阻塞在那里,等待释放掉读锁;如果一个线程占用了写锁,其他线程想要申请读锁或者写锁的话,其他线程都会阻塞在那里,直到写锁被释放为止;