Java Collection秋招復習
抽象類和介面的區別
我們先來看一下抽象類
* @auther draymonder */ public abstract class AbstractClassTest { private int Test1; public int Test2; public void test1() { return ; } protected void test2() { return ; } private void test3() { return ; } void test4() { return ; } public abstract void test5(); protected abstract void test6(); public static void test7() { return ; } }
我們再來看一下介面
/**
* @auther draymonder
*/
public interface IntefaceTest {
public int Test1 = 0;
void test1();
default void test2() {
return ;
}
public static void test3() {
return ;
}
}
由此我們可以知道
- 介面中沒有構造方式
- 介面中的方法必須是抽象的(在
JDK8
下interface
可以使用default
實現方法) - 介面中除了static、final變數,不能有其他變數
- 介面支援多繼承
Java集合
ArrayList
陣列的預設大小為 10。
private static final int DEFAULT_CAPACITY = 10;
新增元素時使用 ensureCapacityInternal()
方法來保證容量足夠,如果不夠時,需要使用 grow()
方法進行擴容,新容量的大小為 oldCapacity + (oldCapacity >> 1)
,也就是舊容量的 1.5 倍。
Vector
陣列的預設大小為 10。
Vector 每次擴容請求其大小的 2 倍空間,而 ArrayList 是 1.5 倍。
Vector 是同步的,因此開銷就比 ArrayList 要大,訪問速度更慢。最好使用 ArrayList 而不是 Vector,因為同步操作完全可以由程式設計師自己來控制;
List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);
CopyOnWriteArrayList
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}
適用場景
CopyOnWriteArrayList 在寫操作的同時允許讀操作,大大提高了讀操作的效能,因此很適合讀多寫少的應用場景。
但是 CopyOnWriteArrayList 有其缺陷:
- 記憶體佔用:在寫操作時需要複製一個新的陣列,使得記憶體佔用為原來的兩倍左右;
- 資料不一致:讀操作不能讀取實時性的資料,因為部分寫操作的資料還未同步到讀陣列中。
所以 CopyOnWriteArrayList 不適合記憶體敏感以及對實時性要求很高的場景。
hashMap
hash為2的冪的作用
key & (hash - 1)
等同於key % hash
,但前者效率比後者高擴容的時候,
table cap
變為2 * table cap
,rehash僅僅需要判斷key & hash
如果為0,還是原來的table[old]
,否則是table[old+table cap]
mask碼的作用
先考慮如何求一個數的掩碼,對於 10010000,它的掩碼為 11111111,可以使用以下方法得到:
mask |= mask >> 1 11011000
mask |= mask >> 2 11111110
mask |= mask >> 4 11111111
mask+1 是大於原始數字的最小的 2 的 n 次方。
num 10010000
mask+1 100000000
以下是 HashMap 中計算陣列容量的程式碼:
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
JDK7版本下的連結串列迴圈
在擴容時候,由於是頭插法,所以,原來是A->B,但是多執行緒情況下會出現。
執行緒1剛剛拿出A, 並準備rehash到B的後面,但是存在B->A還沒有解除的情況,因此正好出現了A->B->A的情況
hashmap為什麼load factor為0.75
如果load factor太小,那麼空間利用率太低;如果load factor太大,那麼hash衝撞就會比較多
JDK8下hashmap為什麼為長度為8連結串列轉為紅黑樹
我們來看一下hashmap的註釋
Because TreeNodes are about twice the size of regular nodes, we
use them only when bins contain enough nodes to warrant use
(see TREEIFY_THRESHOLD). And when they become too small (due to
removal or resizing) they are converted back to plain bins. In
usages with well-distributed user hashCodes, tree bins are
rarely used. Ideally, under random hashCodes, the frequency of
nodes in bins follows a Poisson distribution
(http://en.wikipedia.org/wiki/Poisson_distribution) with a
parameter of about 0.5 on average for the default resizing
threshold of 0.75, although with a large variance because of
resizing granularity. Ignoring variance, the expected
occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
factorial(k)). The first values are:
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million
我們去有道翻譯translate一下
因為樹節點的大小大約是普通節點的兩倍,所以我們
只有當容器中包含足夠的節點以保證使用時才使用它們
(見TREEIFY_THRESHOLD)。當它們變得太小的時候
移除或調整大小)它們被轉換回普通的箱子。在
使用分佈良好的使用者雜湊碼,樹箱是
很少使用。理想情況下,在隨機雜湊碼下
箱中的節點遵循泊松分佈
(http://en.wikipedia.org/wiki/Poisson_distribution)
預設大小調整的引數平均約為0.5
閾值為0.75,雖然由於方差較大
調整粒度。忽略方差,得到期望
列表大小k的出現次數為(exp(-0.5) * pow(0.5, k) /
階乘(k))。第一個值是:
0:0.60653066
1:0.30326533
2:0.07581633
3:0.01263606
4:0.00157952
5:0.00015795
6:0.00001316
7:0.00000094
8:0.00000006
多於:少於千分之一
所以,節點插入遵循泊松分佈,因此出現一個桶內8個節點是極小概率事件,所以遇到這種情況我們可以用紅黑樹加速get
操作
ConcurrentHashMap
不支援 key為null 也不知支援 value為null
JDK7版本下的
//預設的陣列大小16(HashMap裡的那個陣列)
static final int DEFAULT_INITIAL_CAPACITY = 16;
//擴容因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//ConcurrentHashMap中的陣列
final Segment<K,V>[] segments
//預設併發標準16
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//Segment是ReentrantLock子類,因此擁有鎖的操作
static final class Segment<K,V> extends ReentrantLock implements Serializable {
//HashMap的那一套,分別是陣列、鍵值對數量、閾值、負載因子
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int threshold;
final float loadFactor;
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
}
//換了馬甲還是認識你!!!HashEntry物件,存key、value、hash值以及下一個節點
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
//segment中HashEntry[]陣列最小長度
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
//用於定位在segments陣列中的位置,下面介紹
final int segmentMask;
final int segmentShift;
put函式
public V put(K key, V value) {
Segment<K,V> s;
//步驟①注意valus不能為空!!!
if (value == null)
throw new NullPointerException();
//根據key計算hash值,key也不能為null,否則hash(key)報空指標
int hash = hash(key);
//步驟②派上用場了,根據hash值計算在segments陣列中的位置
int j = (hash >>> segmentShift) & segmentMask;
//步驟③檢視當前陣列中指定位置Segment是否為空
//若為空,先建立初始化Segment再put值,不為空,直接put值。
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
ensureSegement
可以看到JDK7版本下,ConcurrentHashMap
的segment
也是使用寫時複製
的,並且使用CAS
演算法來將副本替換
private Segment<K,V> ensureSegment(int k) {
//獲取segments
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
//拷貝一份和segment 0一樣的segment
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
//大小和segment 0一致,為2
int cap = proto.table.length;
//負載因子和segment 0一致,為0.75
float lf = proto.loadFactor;
//閾值和segment 0一致,為1
int threshold = (int)(cap * lf);
//根據大小建立HashEntry陣列tab
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
//再次檢查
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
根據已有屬性建立指定位置的Segment
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
put value
首先lock獲取 tab[hash(key)]
然後進行操作
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//步驟① start
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
//步驟① end
V oldValue;
try {
//步驟② start
//獲取Segment中的HashEntry[]
HashEntry<K,V>[] tab = table;
//算出在HashEntry[]中的位置
int index = (tab.length - 1) & hash;
//找到HashEntry[]中的指定位置的第一個節點
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
//如果不為空,遍歷這條鏈
if (e != null) {
K k;
//情況① 之前已存過,則替換原值
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
//情況② 另一個執行緒的準備工作
if (node != null)
//連結串列頭插入方式
node.setNext(first);
else //情況③ 該位置為空,則新建一個節點(注意這裡採用連結串列頭插入方式)
node = new HashEntry<K,V>(hash, key, value, first);
//鍵值對數量+1
int c = count + 1;
//如果鍵值對數量超過閾值
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
//擴容
rehash(node);
else //未超過閾值,直接放在指定位置
setEntryAt(tab, index, node);
++modCount;
count = c;
//插入成功返回null
oldValue = null;
break;
}
}
//步驟② end
} finally {
//步驟③
//解鎖
unlock();
}
//修改成功,返回原值
return oldValue;
}
scanAndLockForPut
先retries
64次,不行的話,才用ReentrantLock
重入鎖
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
//通過Segment和hash值尋找匹配的HashEntry
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
//重試次數
int retries = -1; // negative while locating node
//迴圈嘗試獲取鎖
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
//步驟①
if (retries < 0) {
//情況① 沒找到,之前表中不存在
if (e == null) {
if (node == null) // speculatively create node
//新建 HashEntry 備用,retries改成0
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
//情況② 找到,剛好第一個節點就是,retries改成0
else if (key.equals(e.key))
retries = 0;
//情況③ 第一個節點不是,移到下一個,retries還是-1,繼續找
else
e = e.next;
}
//步驟②
//嘗試了MAX_SCAN_RETRIES次還沒拿到鎖,簡直B了dog!
else if (++retries > MAX_SCAN_RETRIES) {
//泉水掛機
lock();
break;
}
//步驟③
//在MAX_SCAN_RETRIES次過程中,key對應的entry發生了變化,則從頭開始
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
最後的put流程
rehash
的話 同jdk8版本下的rehash
size
retries
2次 如果還是不同,那麼就reentranLock
依次等待unlock
計算每個tab的size
JDK8下的ConcurrentHashMap
put
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key/value不能為空!!!
if (key == null || value == null) throw new NullPointerException();
//計算hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//註釋① 表為null則初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//CAS方法判斷指定位置是否為null,為空則通過建立新節點,通過CAS方法設定在指定位置
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//當前節點正在擴容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//指定位置不為空
else {
V oldVal = null;
//註釋② 加鎖
synchronized (f) {
if (tabAt(tab, i) == f) {
//節點是連結串列的情況
if (fh >= 0) {
binCount = 1;
//遍歷整體鏈
for (Node<K,V> e = f;; ++binCount) {
K ek;
//如果已存在,替換原值
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
//如果是新加節點,則以尾部插入實現新增
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//節點是紅黑樹的情況
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
//遍歷紅黑樹
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
//連結串列中節點個數超過8轉成紅黑樹
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//註釋③ 新增節點
addCount(1L, binCount);
return null;
}
為什麼tab[hash(key)]用cas,但put裡面的元素都需要用synchronized呢
其實hash衝撞的機率蠻低的,所以synchronized呼叫的次數並不多,更多的是在cas那裡...
然後就是cas比synchronized的優點...
size
每次put
完畢,都會呼叫addCount
方法
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}