就像时常所说的那样,感觉有bug的时候,它一定会出现。当不能够非常准确的确定一个概念的时候,它一定会在某一个时刻让你受到教训,戒之。
记下时候使用的概念,描述自己的理解。
java的hashMap的源码:
负载因子,到底指的是什么?
负载因子表示散表的装满程度,定义为散列表中节点的数目除以基本区域能容纳的节点数所得的商。
比如说散列表长度为m,其中有n个位置已放了值,那么负载因子 a=n/m。所以说增加负载因子的情况下,可以减少Hash表所占用的内存空间,但会增加查询数据的时间开销,反过来说:减小负载因子则是查询时间的开销一般会减小,但是哈希表所占的内存可能会扩大。所以说负载因子是时间和空间的折中值。
hashCode 是做什么用的?
每一个作为hashMap的key值,都有一个hashcode,这个hashCode 的作用在代码里面是这么写的:
int hash = hash(key); int i = indexFor(hash, table.length); static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); }
用来散列key所对应的哈希表里面的位置。
java中JDK的集合框架:
treeMap的实现的基础是红黑树,这个数据结构有一个最起码的了解。
WeakHashTable 弱引用的hashMap 。hashSet和 TreeSet ,HashSet是通过HashMap实现的,TreeSet是通过TreeMap实现的,只不过Set用的只是Map的key。
常用的设计模式:首先是工厂,你能够非常迅速的画出简单的工厂模式的UML图吗?
单例模式的代码,能够迅速的写出来,但是你能够比较清晰的描述出每一行代码的作用吗?单例模式中双重判定的由来:
public static synchronized Singleton getInstance() { if (instance == null) //1 instance = new Singleton(); //2 return instance; //3 }
从代码中,我们知道这样的话效率比较的慢,因为每一次都要去加锁,然后在去判断是否为空,如果不为空的的话,直接返回,压根就不用判断,所以我们知道真正需要同步的是line 2,然后有代码:
public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { instance = new Singleton(); } } return instance; }
这种写法,解决了上面所说的问题,但是从线程安全的角度来进行考虑的话,是不安全的,例如:线程1和线程2的操作顺序如下:
1,线程1 判断instance 为null
2,加锁
3,在创建实例前 (这个时候instance 还是null 的被阻塞了)线程2 判断instance 为null
4,线程2加锁,被阻塞
5,线程1创建Singleton,释放锁
6,线程2得到锁,但是这个时候没有判断instance 为null,再次的创建了一个实例,这个就是线程不安全的情况。
所以才有了双重判断的模式,代码如下:
public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { //1 if (instance == null) //2 instance = new Singleton(); //3 } } return instance; }
这个模式,在有的书上面扣的话,仍然是有问题的,例如:假设线程1执行到instance = new Singleton()这句,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情:
1.给Singleton的实例分配内存。
2.初始化Singleton的构造器
3.将Singleton对象指向分配的内存空间(注意到这步instance就非null了)。
但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候instance因为已经在线程一内执行过了第三点,instance已经是非空了,所以线程二直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来!
所以有了最终的版本:
public class Singleton { private Singleton(){} private static class SingletonHelper{ private static final Singleton instance= new Singleton(); } public static Singleton getInstance(){ return SingletonHelper.instance; } }
这个是一个最终的版本,利用的是JVM本身机制保证了线程安全问题;由于SingletonHelpher是私有的,除了getInstance()之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖JDK版本。
到这里为止,单例模式本身就先告一段落了,最后在介绍从其他途径屏蔽构造单例对象的方法:
1.直接new单例对象
2.通过反射构造单例对象
3.通过序列化构造单例对象。
对于第一种情况,一般我们会加入一个private或者protected的构造函数,这样系统就不会自动添加那个public的构造函数了,因此只能调用里面的static方法,无法通过new创建对象,这个基本上都能够理解明白。
对于第二种情况,反射时可以使用setAccessible方法来突破private的限制,我们需要做到第一点工作的同时,还需要在在ReflectPermission("suppressAccessChecks") 权限下使用安全管理器(SecurityManager)的checkPermission方法来限制这种突破。一般来说,不会真的去做这些事情,都是通过应用服务器进行后台配置实现。
对于第三种情况,如果单例对象有必要实现Serializable接口(很少出现),则应当同时实现readResolve()方法来保证反序列化的时候得到原来的对象,即是在原来的方法中添加这个方法,具体的代码如下:
public class Singleton { private Singleton() { } private static class SingletonHelper { private static final Singleton instance = new Singleton(); } public static Singleton getInstance() { return SingletonHelper.instance; } /** * readResolve方法应对单例对象被序列化时候 */ private Object readResolve() { return getInstance(); } }