Java集合类工作原理及实现

图片 3

集合类的实现原理

创建列表

使用默认的构造函数创建一个空列表,元素添加到列表后,列表容量会扩大到可接纳4个元素。
如果添加了第5个元素,列表大小会重新设置为8个元素。每次都会将列表的容量重新设置为原来的2倍.

var intList=new List<int>();

 

如果列表的容量变了,整个集合就要重新分配到新的内存块中,我们可以在初始化时设置它的容量:

List<int> intList=new List<int>(10);

如果列表的个数超过10个,可以设置容量Capacity:

intList.Capacity = 20;

如果列表的元素已经添加完了,列表会存在多余的容量空间。可以使用TrimExcess方法去除不要的容量:

intList.TrimExcess();

a.集合初始值设定项

使用初始化构造器初始化设定项

int[] arr = { 1, 2, 3 };
var intList = new List<int>(arr) ;

括号中初始化

var intList = new List<int>() { 4, 5 };

 

b.添加元素

intList.Add(5);

添加数组

intList.AddRange(new int[] { 3, 5 });

c.插入元素

intList.Insert(3, 4);

d.访问元素

使用索引获取:

var value = intList[3];

循环遍历:

foreach (var item in intList)
{
     var res = item;
}

forEach方法:

class List<T> : IList<T>
{
    private T[] items;
    public void forEach(Action<T> action)
    {
        if (action == null) throw new ArgumentNullException("action");
        foreach (var item in items)
        {
            action(item);
        }
    }
}

然后我们可以这样调用:

intList.ForEach(m => Console.WriteLine(m));

e.删除元素

按索引删除,比较快

intList.RemoveAt(3);

按元素值删除

intList.Remove(4);

f.搜索

在集合中搜索元素。可以查找索引和元素。

FindIndex通过匹配元素值,获得索引:

intList.FindIndex(m => m==4);

FindIndex方法参数Predicate<T>传入匹配的表达式,返回匹配的元素索引值,Predicate<T>委托表示定义一组条件并确定指定对象是否符合这些条件的方法

intList.Find(m => m == 4);
intList.FindAll(m => m > 2);

Find返回了匹配条件的元素值,FindAll返回了匹配条件的所有元素

g.排序

列表使用Sort方法进行元素排序

intList.Sort();

intList.Sort((m, n) => m);

Sort(Comparison<T>
comparison)方法参数中的委托Comparison含有2个参数,方法将这2个元素进行比较,然后返回绝对值,如果返回-1的,元素需要排前面,返回1的元素需要排后面.

Sort(IComparer<T>
comparer)方法参数中是一个比较接口,接口实现Comparer方法

图片 1图片 2

public enum CompareType
  {
    FirstName,
    LastName,
    Country,
    Wins
  }

  public class RacerComparer : IComparer<Racer>
  {
    private CompareType compareType;
    public RacerComparer(CompareType compareType)
    {
      this.compareType = compareType;
    }

    public int Compare(Racer x, Racer y)
    {
      if (x == null && y == null) return 0;
      if (x == null) return -1;
      if (y == null) return 1;

      int result;
      switch (compareType)
      {
        case CompareType.FirstName:
          return string.Compare(x.FirstName, y.FirstName);
        case CompareType.LastName:
          return string.Compare(x.LastName, y.LastName);
        case CompareType.Country:
          result = string.Compare(x.Country, y.Country);
          if (result == 0)
            return string.Compare(x.LastName, y.LastName);
          else
            return result;
        case CompareType.Wins:
          return x.Wins.CompareTo(y.Wins);
        default:
          throw new ArgumentException("Invalid Compare Type");
      }
    }
  }

View Code

 

h.类型转换

 Converter委托

public delegate TOutput Converter<in TInput, out TOutput>(TInput input);

ConvertAll可以将一种类型的集合转换为另一种类型的集合。

intList.ConvertAll(m => m.ToString());

3. ConcurrentBag实现新增元素

接下来我们看一看ConcurentBag<T>是如何新增元素的。

/// <summary>
/// 尝试获取无主列表,无主列表是指线程已经被暂停或者终止,但是集合中的部分数据还存储在那里
/// 这是避免内存泄漏的方法
/// </summary>
/// <returns></returns>
private ThreadLocalList GetUnownedList()
{
    //此时必须持有全局锁
    Contract.Assert(Monitor.IsEntered(GlobalListsLock));

    // 从头线程列表开始枚举 找到那些已经被关闭的线程
    // 将它所在的列表对象 返回
    ThreadLocalList currentList = m_headList;
    while (currentList != null)
    {
        if (currentList.m_ownerThread.ThreadState == System.Threading.ThreadState.Stopped)
        {
            currentList.m_ownerThread = Thread.CurrentThread; // the caller should acquire a lock to make this line thread safe
            return currentList;
        }
        currentList = currentList.m_nextList;
    }
    return null;
}
/// <summary>
/// 本地帮助方法,通过线程对象检索线程线程本地列表
/// </summary>
/// <param name="forceCreate">如果列表不存在,那么创建新列表</param>
/// <returns>The local list object</returns>
private ThreadLocalList GetThreadList(bool forceCreate)
{
    ThreadLocalList list = m_locals.Value;

    if (list != null)
    {
        return list;
    }
    else if (forceCreate)
    {
        // 获取用于更新操作的 m_tailList 锁
        lock (GlobalListsLock)
        {
            // 如果头列表等于空,那么说明集合中还没有元素
            // 直接创建一个新的
            if (m_headList == null)
            {
                list = new ThreadLocalList(Thread.CurrentThread);
                m_headList = list;
                m_tailList = list;
            }
            else
            {
               // ConcurrentBag内的数据是以双向链表的形式分散存储在各个线程的本地区域中
                // 通过下面这个方法 可以找到那些存储有数据 但是已经被停止的线程
                // 然后将已停止线程的数据 移交到当前线程管理
                list = GetUnownedList();
                // 如果没有 那么就新建一个列表 然后更新尾指针的位置
                if (list == null)
                {
                    list = new ThreadLocalList(Thread.CurrentThread);
                    m_tailList.m_nextList = list;
                    m_tailList = list;
                }
            }
            m_locals.Value = list;
        }
    }
    else
    {
        return null;
    }
    Debug.Assert(list != null);
    return list;
}
/// <summary>
/// Adds an object to the <see cref="ConcurrentBag{T}"/>.
/// </summary>
/// <param name="item">The object to be added to the
/// <see cref="ConcurrentBag{T}"/>. The value can be a null reference
/// (Nothing in Visual Basic) for reference types.</param>
public void Add(T item)
{
    // 获取该线程的本地列表, 如果此线程不存在, 则创建一个新列表 (第一次调用 add)
    ThreadLocalList list = GetThreadList(true);
    // 实际的数据添加操作 在AddInternal中执行
    AddInternal(list, item);
}

/// <summary>
/// </summary>
/// <param name="list"></param>
/// <param name="item"></param>
private void AddInternal(ThreadLocalList list, T item)
{
    bool lockTaken = false;
    try
    {
        #pragma warning disable 0420
        Interlocked.Exchange(ref list.m_currentOp, (int)ListOperation.Add);
        #pragma warning restore 0420
        // 同步案例:
        // 如果列表计数小于两个, 因为是双向链表的关系 为了避免与任何窃取线程发生冲突 必须获取锁
        // 如果设置了 m_needSync, 这意味着有一个线程需要冻结包 也必须获取锁
        if (list.Count < 2 || m_needSync)
        {
            // 将其重置为None 以避免与窃取线程的死锁
            list.m_currentOp = (int)ListOperation.None;
            // 锁定当前对象
            Monitor.Enter(list, ref lockTaken);
        }
        // 调用 ThreadLocalList.Add方法 将数据添加到双向链表中
        // 如果已经锁定 那么说明线程安全  可以更新Count 计数
        list.Add(item, lockTaken);
    }
    finally
    {
        list.m_currentOp = (int)ListOperation.None;
        if (lockTaken)
        {
            Monitor.Exit(list);
        }
    }
}

从上面代码中,我们可以很清楚的知道Add()方法是如何运行的,其中的关键就是GetThreadList()方法,通过该方法可以获取当前线程的数据存储列表对象,假如不存在数据存储列表,它会自动创建或者通过GetUnownedList()方法来寻找那些被停止但是还存储有数据列表的线程,然后将数据列表返回给当前线程中,防止了内存泄漏。

在数据添加的过程中,实现了细颗粒度的lock同步锁,所以性能会很高。删除和其它操作与新增类似,本文不再赘述。

Hashtable

HashMap和HashTab的异同

1.HashTable 基于 Dictionary 类,而 HashMap 是基于
AbstractMap。Dictionary 是任何可将键映射到相应值的类的抽象父类,而
AbstractMap 是基于 Map
接口的实现,它以最大限度地减少实现此接口所需的工作。

2.HashMap 的 key 和 value 都允许为 null,而 Hashtable 的 key 和 value
都不允许为 null。HashMap 遇到 key 为 null 的时候,调用 putForNullKey
方法进行处理,而对 value 没有处理;Hashtable遇到 null,直接返回
NullPointerException。

3.Hashtable 方法是同步,而HashMap则不是。我们可以看一下源码,Hashtable
中的几乎所有的 public 的方法都是 synchronized
的,而有些方法也是在内部通过 synchronized
代码块来实现。所以有人一般都建议如果是涉及到多线程同步时采用
HashTable,没有涉及就采用 HashMap,但是在 Collections
类中存在一个静态方法:synchronizedMap(),该方法创建了一个线程安全的
Map 对象,并把它作为一个封装的对象来返回。

非泛型类集合

泛型集合类是在.NET2.0的时候出来的,也就是说在1.0的时候是没有这么方便的东西的。现在基本上我们已经不使用这些集合类了,除非在做一些和老代码保持兼容的工作的时候。来看看1.0时代的.NET程序员们都有哪些集合类可以用。

ArraryList后来被List<T>替代。

HashTable 后来被Dictionary<TKey,TValue>替代。 
Queue 后来被Queue<T>替代。 
SortedList 后来被SortedList<T>替代。 
Stack 后来被Stack<T>替代。

一、前言

笔者最近在做一个项目,项目中为了提升吞吐量,使用了消息队列,中间实现了生产消费模式,在生产消费者模式中需要有一个集合,来存储生产者所生产的物品,笔者使用了最常见的List<T>集合类型。

由于生产者线程有很多个,消费者线程也有很多个,所以不可避免的就产生了线程同步的问题。开始笔者是使用lock关键字,进行线程同步,但是性能并不是特别理想,然后有网友说可以使用SynchronizedList<T>来代替使用List<T>达到线程安全的目的。于是笔者就替换成了SynchronizedList<T>,但是发现性能依旧糟糕,于是查看了SynchronizedList<T>的源代码,发现它就是简单的在List<T>提供的API的基础上加了lock,所以性能基本与笔者实现方式相差无几。

最后笔者找到了解决的方案,使用ConcurrentBag<T>类来实现,性能有很大的改观,于是笔者查看了ConcurrentBag<T>的源代码,实现非常精妙,特此在这记录一下。

ConcurrentHashMap

ConcurrentHashMap 的成员变量中,包含了一个 Segment 的数组(final
Segment<K,V>[] segments;),而 Segment 是 ConcurrentHashMap
的内部类,然后在 Segment 这个类中,包含了一个 HashEntry
的数组(transient volatile HashEntry<K,V>[] table;)。而
HashEntry 也是 ConcurrentHashMap 的内部类。HashEntry 中,包含了 key 和
value 以及 next 指针(类似于 HashMap 中 Entry),所以 HashEntry
可以构成一个链表。

所以通俗的讲,ConcurrentHashMap 数据结构为一个 Segment 数组,Segment
的数据结构为 HashEntry 的数组,而 HashEntry
存的是我们的键值对,可以构成链表。

ConcurrentHashMap 的高并发性主要来自于三个方面:

  • 用分离锁实现多个线程间的更深层次的共享访问。
  • 用 HashEntery
    对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
  • 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 /
    写操作的内存可见性。

使用分离锁,减小了请求 同一个锁的频率。

通过 HashEntery 对象的不变性及对同一个 Volatile 变量的读 /
写来协调内存可见性,使得
读操作大多数时候不需要加锁就能成功获取到需要的值。由于散列映射表在实际应用中大多数操作都是成功的
读操作,所以 2 和 3
既可以减少请求同一个锁的频率,也可以有效减少持有锁的时间。通过减小请求同一个锁的频率和尽量减少持有锁的时间
,使得 ConcurrentHashMap 的并发性相对于 HashTable 和用同步包装器包装的
HashMap有了质的提高。

线程安全的集合类

ConcurrentQueue 线程安全版本的Queue 
ConcurrentStack线程安全版本的Stack 
ConcurrentBag线程安全的对象集合 
ConcurrentDictionary线程安全的Dictionary 
BlockingCollection

 

2. 用于数据存储的TrehadLocalList类

接下来我们来看一下ThreadLocalList类的构造,该类就是实际存储了数据的位置。实际上它是使用双向链表这种结构进行数据存储。

[Serializable]
// 构造了双向链表的节点
internal class Node
{
    public Node(T value)
    {
        m_value = value;
    }
    public readonly T m_value;
    public Node m_next;
    public Node m_prev;
}

/// <summary>
/// 集合操作类型
/// </summary>
internal enum ListOperation
{
    None,
    Add,
    Take
};

/// <summary>
/// 线程锁定的类
/// </summary>
internal class ThreadLocalList
{
    // 双向链表的头结点 如果为null那么表示链表为空
    internal volatile Node m_head;

    // 双向链表的尾节点
    private volatile Node m_tail;

    // 定义当前对List进行操作的种类 
    // 与前面的 ListOperation 相对应
    internal volatile int m_currentOp;

    // 这个列表元素的计数
    private int m_count;

    // The stealing count
    // 这个不是特别理解 好像是在本地列表中 删除某个Node 以后的计数
    internal int m_stealCount;

    // 下一个列表 可能会在其它线程中
    internal volatile ThreadLocalList m_nextList;

    // 设定锁定是否已进行
    internal bool m_lockTaken;

    // The owner thread for this list
    internal Thread m_ownerThread;

    // 列表的版本,只有当列表从空变为非空统计是底层
    internal volatile int m_version;

    /// <summary>
    /// ThreadLocalList 构造器
    /// </summary>
    /// <param name="ownerThread">拥有这个集合的线程</param>
    internal ThreadLocalList(Thread ownerThread)
    {
        m_ownerThread = ownerThread;
    }
    /// <summary>
    /// 添加一个新的item到链表首部
    /// </summary>
    /// <param name="item">The item to add.</param>
    /// <param name="updateCount">是否更新计数.</param>
    internal void Add(T item, bool updateCount)
    {
        checked
        {
            m_count++;
        }
        Node node = new Node(item);
        if (m_head == null)
        {
            Debug.Assert(m_tail == null);
            m_head = node;
            m_tail = node;
            m_version++; // 因为进行初始化了,所以将空状态改为非空状态
        }
        else
        {
            // 使用头插法 将新的元素插入链表
            node.m_next = m_head;
            m_head.m_prev = node;
            m_head = node;
        }
        if (updateCount) // 更新计数以避免此添加同步时溢出
        {
            m_count = m_count - m_stealCount;
            m_stealCount = 0;
        }
    }

    /// <summary>
    /// 从列表的头部删除一个item
    /// </summary>
    /// <param name="result">The removed item</param>
    internal void Remove(out T result)
    {
        // 双向链表删除头结点数据的流程
        Debug.Assert(m_head != null);
        Node head = m_head;
        m_head = m_head.m_next;
        if (m_head != null)
        {
            m_head.m_prev = null;
        }
        else
        {
            m_tail = null;
        }
        m_count--;
        result = head.m_value;

    }

    /// <summary>
    /// 返回列表头部的元素
    /// </summary>
    /// <param name="result">the peeked item</param>
    /// <returns>True if succeeded, false otherwise</returns>
    internal bool Peek(out T result)
    {
        Node head = m_head;
        if (head != null)
        {
            result = head.m_value;
            return true;
        }
        result = default(T);
        return false;
    }

    /// <summary>
    /// 从列表的尾部获取一个item
    /// </summary>
    /// <param name="result">the removed item</param>
    /// <param name="remove">remove or peek flag</param>
    internal void Steal(out T result, bool remove)
    {
        Node tail = m_tail;
        Debug.Assert(tail != null);
        if (remove) // Take operation
        {
            m_tail = m_tail.m_prev;
            if (m_tail != null)
            {
                m_tail.m_next = null;
            }
            else
            {
                m_head = null;
            }
            // Increment the steal count
            m_stealCount++;
        }
        result = tail.m_value;
    }


    /// <summary>
    /// 获取总计列表计数, 它不是线程安全的, 如果同时调用它, 则可能提供不正确的计数
    /// </summary>
    internal int Count
    {
        get
        {
            return m_count - m_stealCount;
        }
    }
}

从上面的代码中我们可以更加验证之前的观点,就是ConcurentBag<T>在一个线程中存储数据时,使用的是双向链表ThreadLocalList实现了一组对链表增删改查的方法。

HashSet

对于 HashSet 中保存的对象,请注意正确重写其 equals 和 hashCode
方法,以保证放入的对象的唯一性。这两个方法是比较重要的,希望大家在以后的开发过程中需要注意一下。

图片 3

image.png

BitArray类的方法和属性

下表列出了一些BitArray类的常用属性:

属性 描述
Count 获取包含在BitArray元素的数量
IsReadOnly 获取一个值,指示BitArray是否是只读
Item 获取或设置在所述BitArray的特定位置的比特的值
Length 获取或设置在BitArray元素的数量

下表列出了一些BitArray类的常用方法:

 

S.N 方法名称及用途
1 public BitArray And( BitArray value ); 
执行对指定BitArray的相应元素在当前BitArray元素的按位与运算
2 public bool Get( int index ); 
获取在所述BitArray的特定位置的比特的值
3 public BitArray Not();
反转当前BitArray所有的位值,使设置为true的元素被更改为false,并设置为false元素更改为true
4 public BitArray Or( BitArray value ); 
在执行对指定BitArray的相应元素在当前BitArray的元素的按位或操作
5 public void Set( int index, bool value ); 
设置在所述BitArray为指定值的特定位置的比特值
6 public void SetAll( bool value ); 
设置在BitArray所有位设置为指定值
7 public BitArray Xor( BitArray value ); 
执行关于对在指定BitArray的相应元素中的当前BitArray的元素按位异或运算

当需要存储位,但不知道事先比特数就使用它。您可以通过使用一个整数索引,它从零开始访问BitArray集合中的项。

图片 4图片 5

using System;
using System.Collections;

namespace CollectionsApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            //creating two  bit arrays of size 8
            BitArray ba1 = new BitArray(8);
            BitArray ba2 = new BitArray(8);
            byte[] a = { 60 };
            byte[] b = { 13 };

            //storing the values 60, and 13 into the bit arrays
            ba1 = new BitArray(a);
            ba2 = new BitArray(b);

            //content of ba1
            Console.WriteLine("Bit array ba1: 60");
            for (int i = 0; i < ba1.Count; i++)
            {
                Console.Write("{0, -6} ", ba1[i]);
            }
            Console.WriteLine();

            //content of ba2
            Console.WriteLine("Bit array ba2: 13");
            for (int i = 0; i < ba2.Count; i++)
            {
                Console.Write("{0, -6} ", ba2[i]);
            }
            Console.WriteLine();


            BitArray ba3 = new BitArray(8);
            ba3 = ba1.And(ba2);

            //content of ba3
            Console.WriteLine("Bit array ba3 after AND operation: 12");
            for (int i = 0; i < ba3.Count; i++)
            {
                Console.Write("{0, -6} ", ba3[i]);
            }
            Console.WriteLine();

            ba3 = ba1.Or(ba2);
            //content of ba3
            Console.WriteLine("Bit array ba3 after OR operation: 61");
            for (int i = 0; i < ba3.Count; i++)
            {
                Console.Write("{0, -6} ", ba3[i]);
            }
            Console.WriteLine();

            Console.ReadKey();
        }
    }
}

View Code

让我们编译和运行上面的程序,这将产生以下结果:

Bit array ba1: 60 
False False True True True True False False 
Bit array ba2: 13
True False True True False False False False 
Bit array ba3 after AND operation: 12
False False True True False False False False 
Bit array ba3 after OR operation: 61
True False True True False False False False 
  • 一、前言
  • 二、ConcurrentBag类
  • 三、
    ConcurrentBag线程安全实现原理

    • 1.
      ConcurrentBag的私有字段
    • 2.
      用于数据存储的TrehadLocalList类
    • 3.
      ConcurrentBag实现新增元素
    • 4. ConcurrentBag
      如何实现迭代器模式
  • 四、总结
  • 笔者水平有限,如果错误欢迎各位批评指正!

LinkedList

LinkedList 是基于链表结构实现,所以在类中包含了 first 和 last
两个指针(Node)。Node
中包含了上一个节点和下一个节点的引用,这样就构成了双向的链表。每个 Node
只能知道自己的前一个节点和后一个节点,但对于链表来说,这已经足够了。

集合

 


 

1.集合接口和类型

接口

说明

IEnumerable<T>

如果foreach语句用于集合,就需要IEnumerable接口.这个借口定义了方法GetEnumerator(),他返回一个实现了IEnumerator接口的枚举

ICollection<T>

ICollection<T>接口有泛型集合类实现.使用这个借口可以获得集合中的元素个数(Count属性),把集合复制到数组中(CopyTo()方法),还可以从集合中添加和删除元素(Add(),Remove(),Clear())

List<T>

IList<T>接口用于可通过位置访问其中的元素列表,这个接口定义了一个 索引器,可以在集合的指定位置插入或删除 mount些项(Insert()和Remove()方法).IList<T>接口派生自ICollection<T>接口

ISet<T>

ISet<T>接口是.NET4中新增的.实现这个接口的集允许合并不同的集.获得两个集的交集,检查两个集合是否重叠.ISet<T>接口派生自ICollection<T>接口

IDictionary<TKey,TValue>

IDictionary<TKey,TValue>接口由包含键和值的泛型集合类 实现.使用这个接口可以访问所有的键和值,使用键类型的索引器可以访问某些项,还可以添加或删除某些项

ILookup<TKey,TValue>

ILookup<TKey,TValue>接口类似于IDictionary<TKey,TValue>接口,实现该接口的集合有键和值,且可以通过一个键包含多个值

IComparer<T>

接口ICommparer<T>由比较器实现,通过Comparer()方法给集合中的元素排序

IEqualityComparer<T>

接口IEqualityComparer<T>由一个比较器实现,该比较器可用于字典中的键.使用这个接口,可以对对象进行相等性比较.在.NET中,这个接口也由数组和元组实现

IProducerConsumerColllection<T>

IProducerConsumerCollection<T>接口是.NET4中新增的,它支持新的线程安全的集合类

IReadOnlyList<T>、

IReadOnlyDictionary<T>、

IReadOnlyCollection<T>

初始化后不能修改的集合,只能检索对象,不能添加和删除.

 

2.列表

先看看一个实例:

图片 6图片 7

[Serializable]
  public class Racer : IComparable<Racer>, IFormattable
  {
    public int Id { get; private set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Country { get; set; }
    public int Wins { get; set; }

    public Racer(int id, string firstName, string lastName, string country)
      : this(id, firstName, lastName, country, wins: 0)
    {
    }
    public Racer(int id, string firstName, string lastName, string country, int wins)
    {
      this.Id = id;
      this.FirstName = firstName;
      this.LastName = lastName;
      this.Country = country;
      this.Wins = wins;
    }

    public override string ToString()
    {
      return String.Format("{0} {1}", FirstName, LastName);
    }

    public string ToString(string format, IFormatProvider formatProvider)
    {
      if (format == null) format = "N";
      switch (format.ToUpper())
      {
        case null:
        case "N": // name
          return ToString();
        case "F": // first name
          return FirstName;
        case "L": // last name
          return LastName;
        case "W": // Wins
          return String.Format("{0}, Wins: {1}", ToString(), Wins);
        case "C": // Country
          return String.Format("{0}, Country: {1}", ToString(), Country);
        case "A": // All
          return String.Format("{0}, {1} Wins: {2}", ToString(), Country, Wins);
        default:
          throw new FormatException(String.Format(formatProvider,
                "Format {0} is not supported", format));
      }
    }

    public string ToString(string format)
    {
      return ToString(format, null);
    }

    public int CompareTo(Racer other)
    {
      if (other == null) return -1;
      int compare = string.Compare(this.LastName, other.LastName);
      if (compare == 0)
        return string.Compare(this.FirstName, other.FirstName);
      return compare;
    }
  }

View Code

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图