Set表示一种没有重复元素的集合类,在JDK里面有HashSet的实现,底层是基于HashMap来实现的。这里实现一个简化版本的Set,有以下约束:
1. 基于链表实现,链表节点按照对象的hashCode()顺序由小到大从Head到Tail排列。
2. 假设对象的hashCode()是唯一的,这个假设实际上是不成立的,这里为了简化实现做这个假设。实际情况是HashCode是基于对象地址进行的一次Hash操作,目的是把对象根据Hash散开,所以可能有多个对象地址对应到一个HashCode,也就是哈希冲突,要做冲突的处理。常见的处理就是使用外部拉链法,在对应的Hash的节点再创建1个链表,相同HashCode的节点在这个链表上排列,再根据equals()方法来识别是否是同一个节点。这个是HashMap的基本原理。
有了以上假设,这篇从最简单的加锁的方式到最终的无锁展示一下如何一步一步演进的过程,以及最终无锁实现要考虑的要点。
1. 粗粒度锁
2. 细粒度锁
3. 乐观锁
4. 懒惰锁
5. 无锁
先看一下Set接口,1个添加方法,1个删除方法,1个检查方法
package com.lock.test;
public interface Set<T> {
public boolean add(T item);
public boolean remove(T item);
public boolean contains(T item);
}
第一个实现的是使用粗粒度的锁,就是对整个链表加锁,这样肯定是线程安全的,但是效率肯定是最差的
链表节点类的定义,三个元素
1. 加入节点的元素
2. next指针指向下一个节点,明显这是个单链表结构
3. key表示item的HashCode
class Node<T>{
T item;
Node<T> next;
int key;
public Node(T item){
this.item = item;
this.key = item.hashCode();
}
public Node(){}
}
粗粒度链表的实现
1. 链表维护了一个头节点,头节点始终指向最小的HashCode,头节点初始的next指针指向最大的HashCode,表示尾节点,整个链表是按照HashCode从小往大排列
2. 链表维护了一个整体的锁,add, remove, contains都加锁,保证线程安全,简单粗暴,但是效率低下
class CoarseSet<T> implements Set<T>{
private final Node<T> head;
private java.util.concurrent.locks.Lock lock = new ReentrantLock();
public CoarseSet(){
head = new Node<T>();
head.key = Integer.MIN_VALUE;
Node<T> MAX = new Node<T>();
MAX.key = Integer.MAX_VALUE;
head.next = MAX;
}
public boolean add(T item){
Node<T> pred, curr;
int key = item.hashCode();
lock.lock();
try{
pred = head;
curr = head.next;
while(curr.key < key){
pred = curr;
curr = curr.next;
}
if(curr.key == key){
return false;
}
Node<T> node = new Node<T>(item);
node.next = curr;
pred.next = node;
return true;
}finally{
lock.unlock();
}
}
public boolean remove(T item){
Node<T> pred, curr;
int key = item.hashCode();
lock.lock();
try{
pred = head;
curr = head.next;
while(curr.key < key){
pred = curr;
curr = curr.next;
}
if(curr.key == key){
pred.next = curr.next;
curr.next = null;
return true;
}
return false;
}finally{
lock.unlock();
}
}
public boolean contains(T item){
Node<T> pred, curr;
int key = item.hashCode();
lock.lock();
try{
pred = head;
curr = head.next;
while(curr.key < key){
pred = curr;
curr = curr.next;
}
return curr.key == key;
}finally{
lock.unlock();
}
}
}
对粗粒度链表的优化可以细化锁的粒度,粗粒度锁的问题在于使用了一把锁锁住了整个链表,那么可以使用多把锁,每个节点维护一把锁,这样单个节点上锁理论上不会影响其他节点。
单链表最简单add操作需要做两步
1. 把新加入节点的next指向当前节点
2. 把前驱节点的next指向新加入的节点
node.next = curr;
pred.next = node
单链表的删除操作只需要做一步
pred.next = curr.next
如果使用细粒度锁的话,添加和删除操作要同时锁住两个节点才能保证添加和删除的正确性,不然有可能添加进来的节点指向了一个已经被删除的节点。所以需要同时控制两把锁,这也叫锁耦合,需要先获取前驱节点的锁,再获取当前节点的锁。
由于这种锁的获取是按照从前往后的顺序获取的,是一个方向的,所以不会引起死锁的问题。
死锁有两种:
1. 由获取锁的顺序引起的,顺序形成了环
2. 由资源问题引起的
来看看细粒度锁的实现,首先是带锁的Node