-
说一下单例模式的各种写法,手写一种线程安全的
-
第一种(懒汉,线程不安全):
- public class Singleton {
- private static Singleton instance;
- private Singleton (){}
- public static Singleton getInstance() {
- if (instance == null) {
- instance = new Singleton();
- }
- return instance;
- }
- }
-
这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。
第二种(懒汉,线程安全):
- public class Singleton {
- private static Singleton instance;
- private Singleton (){}
- public static synchronized Singleton getInstance() {
- if (instance == null) {
-
这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。
第三种(饿汉):
- public class Singleton {
- private static Singleton instance = new Singleton();
- private Singleton (){}
- public static Singleton getInstance() {
- return instance;
- }
- }
-
这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
第四种(饿汉,变种):
- public class Singleton {
- private Singleton instance = null;
- static {
- instance = new Singleton();
- }
- private Singleton (){}
- public static Singleton getInstance() {
- return this.instance;
- }
- }
-
表面上看起来差别挺大,其实更第三种方式差不多,都是在类初始化即实例化instance。
- instance = new Singleton();
- }
- return instance;
- }
- }
-
ArrayList如何扩容?(常考)
-
ArrayList是一个可动态添加删除数据的集合,底层数据结构是数组。当添加数据的容量大于底层数组容量时则会产出扩容,即通过生成数组来实现。它的主要核心就是扩容机制(当插入时所需要的长度超过数组原本的长度时则需要扩容)。本文主要抓ArrayList的重点分析。
接下来抱着这几个问题来分析一下ArrayList的源码
1.ArrayList怎么判断是否需要扩容的?
2.ArrayList是怎么扩容的,怎么操作数组实现的?
3.ArrayList在扩容做了什么优化?
4.ArrayList为什么是线程不安全的?
5.ArrayList为什么增删慢(源码角度看)?
6.ArrayList的缩容机制是怎样的?
ArrayList类与成员变量:
//继承自AbstractList
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
//默认初始容量
private static final int DEFAULT_CAPACITY = 10;
//默认元素集合
private static final Object[] EMPTY_ELEMENTDATA = {};
//无参构造实例默认元素集合
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存放元素的数组
transient Object[] elementData;
//记录ArrayList中存储的元素的个数
private int size;
构造函数:
//可指定大小的构造函数
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//默认构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//传入一个为Collection的对象
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
三个构造函数目的都是为初始化elementData,一般我们使用中间默认的即可。如需使用其它的可按情况使用,比如当我们知道需要使用的容量那就用第一个指定容量大小,避免频繁扩容。插入add():
//从尾部插入数据
public boolean add(E e) {
//判断是否扩容进行扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//在数组后面添加元素e
elementData[size++] = e;
return true;
}
ensureCapacityInternal方法为了判断是否扩容,跟进去private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0) //如果所需容量大于现数组容量,则进行扩容
//扩容方法
grow(minCapacity);
}
这里我们就可以解决上面说的第1个问题了。根据所需容量是否大于现数组容量来进行扩容,调用grow方法,继续跟进去
grow方法是ArrayList扩容的核心方法:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//增加1.5倍的长度
int newCapacity = oldCapacity + (oldCapacity >> 1);
//当增加1.5倍后还是不够所需要长度,则直接用所需长度来扩容
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
通过oldCapacity + (oldCapacity >> 1)(右移左移运算的思路:先转二进制计算后再转回十进制)来实现扩容1.5倍的长度,再通过Arrays.copyOf实现扩容,这里就解决了第2个问题。第3个问题的解决也在这里,我们可以看到,它是先扩容1.5倍,当增加后还是不够用,则直接使用所需要的长度作为数组的长度。为什么呢?这里ArrayList是做了优化,它先扩1.5倍,如果这次够用下次再来扩容可能就可以不用扩容。如果每次都用所需要的长度来扩容,那么以后每次增加元素都会进行一次扩容操作,当增加后还是不够用的时候,ArrayList无法知道到底给你多少容量才合适,所以就直接使用你所需的长度。扩容的时候底层是需要操作数组的(Arrays.copyOf),经常扩容会消耗性能的。
我们看到ArrayList里面的方法都没有添加锁,即当多个线程同时调用的话会会引发不可预知的错误。这是第4个问题的。
删除remove方法:
public boolean remove(Object o) {
//如果传入元素为null,则循环判断元素为null的进行删除
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index); //删除
return true;
}
} else { //循环数组进行判断删除
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index); //删除
return true;
}
}
return false;
}
循环整个数组,判断是否相等进行删除。因为ArrayList 允许空值,所以源码这里进行了多一次的判断是否为null的情况。可以看到核心删除方法是fastRemove。private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
这个核心方法的思路很简单,定位到当前需要删除的元素位置,然后把后面元素的位置+1顶上来一个位置,,然后把最后一个位置设置为null,size同时减1,实现删除。就好比一根三节棍,把中间那截给删了,原来后面的就顶上来跟前面的接在了一起,成了两节棍。注:其实从这里就可以解决第5个问题,在它删除元素时候后面的元素需要移动上去,采用了System.arraycopy复制,当移动多了自然性能就低了,这就是删除慢的原因。为什么插入慢呢,我想大家都明白了,删除的时候后面得向前移动,那插入的时候同理得向后移动。(通过指定位置插入元素的情况,因为ArrayList可以从尾部,也可以指定位置插入)。
思考一个问题,当我们需要向ArrayList增加大量元素时它会扩容成一个大数组,当我们不需要那么多元素的时候把它删了,那数组虽然元素被删了它还是会在内存中占了空间,好比一个大盆子装一个小鸡蛋。ArrayList没有自动来处理这个问题,它提供了一个方法trimToSize()方法。我们可以手动调用此方法触发ArrayList 的缩容机制。这样就可以释放多余的空间,提高空间利用率。
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
size是记录元素数据,当size(元素数量)小于此数组的长度(elementData.length),说明数组有空闲空间位置没有被使用,那就把数组转换成长度当前元素数量的长度。这就是第6个问题ArrayList的缩容机制 -
场景题:生产者消费者模式可解
-
设计模式了解哪些,手写一下观察者模式
-
一个十进制的数在内存中是怎么存的?
-
为啥有时会出现4.0-3.6=0.40000001这种现象?(这个没回答上来,让我回去看看
-
Hibernate中有哪几种数据库语句写法?我只回答了Sql和Hql,另一种没回答上来,让我回去查查
-
3. JVM,垃圾回收算法,垃圾回收器
4. 会哪些排序算法,解释一下快排原理
6. 一个学生表,一个课程成绩表,怎么找出学生课程的最高分数
7. 一个数的因子只能是3,5,7,问第n个这样的数是多少?(剑指offer丑数那题变型,我回答的不好
-
怎么解决你这个系统高并发的问题?
我说可以用负载均衡来平衡流量,扩大服务器规模,面试官说你数据库服务器不要处理嘛,我赶紧补了一句可以用缓存
4. 负载均衡怎么配置?
只看过介绍,没配置过阿
5. 缓存找到了数据怎么配置,找不到又怎样处理?画一下
6. 规定1分钟之内只能处理1000个请求,你怎么实现,手撕代码
写好了之后,面试官一再强调一分钟是相对时间,感觉我的写法面试官并不满意,然后这个问题纠缠了很久
7. 怎么求一个二叉树的深度?手撕代码
8. 两个数组A和B,怎么求解两个数组中和为S的所有组合(组合中一个元素是A的,一个元素是B的)
我说先排序,然后头尾指针
-
问了数据库的隔离级别
-
forward与redirect区别,说一下你知道的状态码,redirect的状态码是?
-
算法题:二叉树层序遍历,进一步提问:要求每层打印出一个换行符
-
8.那ConcurrentHashMap内部是如何实现的?每个segment是个什么数据结构?ConcurrentHashMap如何扩容,内部结构?(HashTable)
由多个segment组成,每个segment是一个HashEntry数组,HashEntry是链表。
实现同步的方法是segment继承了ReentrantLock类;Hashtable实现线程安全的方法是使用synchronized6.序列化,以及json传输
- hashMap和ConcurrentHashMap的区别
- 总结ConcurrentHashMap就是一个分段的hashtable ,根据自定的hashcode算法生成的对象来获取对应hashcode的分段块进行加锁,不用整体加锁,提高了效率
-
HashMap底层,负载因子,为啥是2^n
ConcurrentHashMap锁加在了哪些地方
-
算法题:求一个数组中连续子向量的最大和
-
找出数组中和为S的一对组合,找出一组就行