Stack(栈)表示对象的后进先出 (LIFO) 集合。实现了ICollection接口。这个数据结构是.net集中基础数据集合中实现起来最简单的。Stack内部是是维护的数组来存储实际的存入的数据,然后对外开放一些方法来操作栈。
概念
定义:限定仅在表尾进行插入或删除操作的线性表,表尾对应栈顶,表头对应栈底,不含元素的栈称为空栈。
入栈:往栈顶插入一个元素。
出栈:在栈顶删除一个元素
元素的操作只能在栈顶进行,最后入栈的元素最先出栈,结构图如下:
属性和变量
/// <summary> /// Storage for MyStack elements /// </summary> private Object[] _array; /// <summary> /// Number of items in the MyStack. /// </summary> private int _size; public virtual int Count { get { return _size; } } // Used to keep enumerator in [....] w/ collection. private int _version; [NonSerialized] private Object _syncRoot; /// <summary> /// 默认容量 /// </summary> private const int _defaultCapacity = 10;
构造函数
Stack有三个构造函数,分别为:
(1)全部使用Stack设定的默认值,不推荐使用(原因是动态扩容需要额外的计算与开辟新的内存空间,动态扩容应该发生在超出预期容量值范围的情况下,抑制溢出);
public Stack() { _array = _emptyArray; _size = 0; _version = 0; }
(2)使用一个非负的整数设定Stack的一个初始容量,推荐使用(给定一个预期的容量值,若预期值小了,会自动扩容,也不担心溢出)
public Stack(int capacity) { if (capacity < 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNumRequired); _array = new T[capacity]; _size = 0; _version = 0; }
(3)使用一个现有的非空引用集合进行填充初始化Stack,Stack具有与该集合相同的长度,并且按照集合元素的存储顺序(推荐使用实现了Collection泛型接口的集合,可以获得一些性能上的提升)。
public Stack(IEnumerable<T> collection) { if (collection==null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); ICollection<T> c = collection as ICollection<T>; if( c != null) { //实现了ICollection泛型接口的集合,简单粗暴,分配空间,数组复制 int count = c.Count; _array = new T[count]; c.CopyTo(_array, 0); _size = count; } else { _size = 0; //没有实现ICollection泛型接口的,默认初始容量为4,空间不够还需动态扩容 _array = new T[_defaultCapacity]; //遍历元素 执行入栈操作 using(IEnumerator<T> en = collection.GetEnumerator()) { while(en.MoveNext()) { Push(en.Current); } } } }
入栈出栈
// Pushes an item to the top of the MyStack. public virtual void Push(Object obj) { if (_size == _array.Length) { //以二倍新增 Object[] newArray = new Object[2 * _array.Length]; Array.Copy(_array, 0, newArray, 0, _size); _array = newArray; } _array[_size++] = obj; _version++; } // Pops an item from the top of the MyStack. If the MyStack is empty, Pop // throws an InvalidOperationException. public virtual Object Pop() { if (_size == 0) throw new InvalidOperationException(); _version++; Object obj = _array[--_size]; _array[_size] = null; // Free memory quicker. return obj; }
ConcurrentStack
原子操作
所谓无锁其实就是在普通栈的实现方式上使用了原子操作,原子操作的原理就是CPU在系统总线上设置一个信号,当其他线程对同一块内存进行访问时CPU监测到该信号存在会,然后当前线程会等待信号释放后才能对内存进行访问。原子操作都是由操作系统API实现底层由硬件支持,常用的操作有:原子递增,原子递减,比较交换,ConcurrentStack中的实现就是使用了原子操作中的比较交换操作。
使用原子操作的好处:
第一、由于没有使用锁,可以避免死锁。
第二、原子操作不会阻塞线程,例如执行某个指令时当前线程挂起了(或执行了一次上下文切换),其他线程还能继续操作,如果使用lock锁,当前线程挂起后由于没有释放锁,其他线程进行操作时会被阻塞。
第三、由于原子操作直接由硬件指令的支持,所以原子操作性能比普通锁的高。
使用原子操作的坏处:
第一,使用原子操作一般失败时会使用回退技术对当前操作进行重试,所以容易产生活锁和线程饥饿问题,但可以通过随机退让等技术进行缓解,但不能消除。
第二,程序员开发使用难度较大,测试难度较大。
下面开始进入正题:
由于.net 中的 ConcurrentStack的代码较多所以本文就不贴出所有代码,本人也只分析笔者认为重要的几个部分,全部源码可以再去以下微软官方网址查看
http://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentStack.cs
传统的栈结构都一般都使用单链表实现(.net中的Stack使用的是数组), 入栈操作就是把头节点替换为新节点,出栈操作就是把头结点指向下一个节点。所以当大量线程并发访问时线程的竞争条件都在头结点。也就是说如果我们能保证对于头结点操作时是安全的那么整个栈就是安全的。
这里ConcurrentStack使用私有类Node创建了一个链表。
查询
public bool TryPeek(out T result) { Node head = m_head; // If the stack is empty, return false; else return the element and true. if (head == null) { result = default(T); return false; } else { result = head.m_value; return true; } } public int Count { // 复杂度是O(n)。结果可能在返回前被更新 get { int count = 0; for (Node curr = m_head; curr != null; curr = curr.m_next) { //we don't handle overflow, to be consistent with existing generic collection types in CLR count++; } return count; } } public bool IsEmpty { get { return m_head == null; } }