打赏

Java 集合系列之四:Queue基本操作

1. Java Queue

1. Java Queue 重要观点

  • Java Queue接口是Java Collections Framework的成员。
  • Queue 实现通常不允许插入 null 元素
  • 队列通常(但并非一定)以 FIFO(先进先出)的方式排序各个元素。不过优先级队列和 LIFO 队列(或堆栈)例外,前者根据提供的比较器或元素的自然顺序对元素进行排序,后者按 LIFO(后进先出)的方式对元素进行排序。无论使用哪种排序方式,队列的 都是调用 remove() 或 poll() 所移除的元素。在 FIFO 队列中,所有的新元素都插入队列的末尾。其他种类的队列可能使用不同的元素放置规则。每个 Queue 实现必须指定其顺序属性。
  • 在处理元素前用于保存元素的 collection。除了基本的 Collection 操作外,队列还提供其他的插入、提取和检查操作。每个方法都存在两种形式:一种抛出异常(操作失败时),另一种返回一个特殊值(null 或 false,具体取决于操作)。
  • Queue 接口并未定义阻塞队列的方法,而这在并发编程中是很常见的。BlockingQueue 接口定义了那些等待元素出现或等待队列中有可用空间的方法,这些方法扩展了此接口。
  •   抛出异常   返回特殊值
    插入:add(e)     offer(e)  插入一个元素
    移除:remove()      poll()      移除和返回队列的头
    检查:element()     peek()    返回但不移除队列的头。

  • JDK中并发队列提供了两种实现,一种是高性能队列ConcurrentLinkedQueue,一种是阻塞队列BlockingQueue(7种阻塞队列),两种都继承自Queue。
  • JDK中队列有两大类,一类是双端队列,一类是单端队列。

2. Java Queue类图

此接口是 Java Collections Framework 的成员。

Java Queue接口扩展了Collection接口。Collection接口 externs Iterable接口。

子接口:BlockingQueue, Deque, BlobkingDequeue

一些最常用的Queue实现类是LinkedList,ArrayBlickingQueue, LinkedBlockingQueue,PriorityQueue, PriorityBlockingQueue。

3. Java Queue 方法

boolean  add(E e) //将指定的元素插入此队列(如果立即可行且不会违反容量限制),在成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException。
E        element() //获取,但是不移除此队列的头。
boolean  offer(E e) //将指定的元素插入此队列(如果立即可行且不会违反容量限制),当使用有容量限制的队列时,此方法通常要优于 add(E),后者可能无法插入元素,而只是抛出一个异常。
E        peek() //获取但不移除此队列的头;如果此队列为空,则返回 null。
E        poll() //获取并移除此队列的头,如果此队列为空,则返回 null。
E        remove() //获取并移除此队列的头。

2. ConcurrentLinkedQueue接口(JUC)

1. ConcurrentLinkedQueue 结构图

一个基于链接节点的无界线程安全队列。

2. ConcurrentLinkedQueue重要特点

  1. 一个基于链接节点的无界线程安全队列。此队列按照 FIFO(先进先出)原则对元素进行排序。队列的头部 是队列中时间最长的元素。队列的尾部是队列中时间最短的元素。新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。当多个线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择。

  2. 此队列不允许使用 null 元素。

  3. 此实现采用了有效的“无等待 (wait-free)”算法,该算法基于 Maged M. Michael 和 Michael L. Scott 合著的 Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms 中描述的算法。

  4. 需要小心的是,与大多数 collection 不同,size 方法不是 一个固定时间操作。由于这些队列的异步特性,确定当前元素的数量需要遍历这些元素。

  5. 内存一致性效果:当存在其他并发 collection 时,将对象放入 ConcurrentLinkedQueue 之前的线程中的操作 happen-before 随后通过另一线程从 ConcurrentLinkedQueue 访问或移除该元素的操作。
  6. ConcurrentLinkedQueue中有两个volatile类型的Node节点分别用来存在列表的首尾节点,其中head节点存放链表第一个item为null的节点,tail则并不是总指向最后一个节点。Node节点内部则维护一个变量item用来存放节点的值,next用来存放下一个节点,从而链接为一个单向无界列表。
  7. 开源框架中使用:Tomcat中NioEndPoint中的每个poller里面就维护一个ConcurrentLinkedQueue<Runnable>用来作为缓冲存放任务。
  8. ConcurrentLinkedQueue使用CAS非阻塞算法实现使用CAS解决了当前节点与next节点之间的安全链接和对当前节点值的赋值。【CAS的基本思想是认为当前环境中的并发并没有那么高,比较乐观的看待整个并发,只需要在更新某个值时先检查下该值有没有发生变化,如果没有发生变化则更新,否则放弃更新。CAS的操作其底层是通过调用sun.misc.Unsafe类中的CompareAndSwap的方法保证线程安全的。】由于使用CAS没有使用锁,所以获取size的时候有可能进行offer,poll或者remove操作,导致获取的元素个数不精确,所以在并发情况下size函数不是很有用。另外第一次peek或者first时候会把head指向第一个真正的队列元素。
  9. 线程安全的volatile + CAS 可知入队出队函数都是操作volatile变量:head,tail所以要保证队列线程安全只需要保证对这两个Node操作的可见性和原子性,由于volatile本身保证可见性,所以只需要看下多线程下如果保证对着两个变量操作的原子性。对于offer操作是在tail后面添加元素,也就是调用tail.casNext方法,而这个方法是使用的CAS操作,只有一个线程会成功,然后失败的线程会循环一下,重新获取tail,然后执行casNext方法。对于poll也是这样的。

3. Deque接口

1. Deque结构图

 

  一个线性 collection,支持在两端插入和移除元素。名称 deque 是“double ended queue(双端队列)”的缩写,通常读为“deck”。大多数 Deque 实现对于它们能够包含的元素数没有固定限制,但此接口既支持有容量限制的双端队列,也支持没有固定大小限制的双端队列。

  队列:此接口扩展了 Queue 接口。在将双端队列用作队列时,将得到 FIFO(先进先出)行为。将元素添加到双端队列的末尾,从双端队列的开头移除元素。

  堆栈:双端队列也可用作 LIFO(后进先出)堆栈。应优先使用此接口而不是遗留 Stack 类。在将双端队列用作堆栈时,元素被推入双端队列的开头并从双端队列开头弹出。堆栈方法完全等效于 Deque 方法,

4. LinkedList

1. LinkedList 结构图

LinkedList是基于链表实现的,从源码可以看出是一个双向链表。除了当做链表使用外,它也可以被当作堆栈、队列或双端队列进行操作

LinkedList不是线程安全的,继承AbstractSequentialList实现List、Deque、Cloneable、Serializable。

  • LinkedList继承AbstractSequentialList,AbstractSequentialList 实现了get(int index)、set(int index, E element)、add(int index, E element) 和 remove(int index)这些函数。这些接口都是随机访问List的。
  • LinkedList 实现 List 接口,能对它进行队列操作。
  • LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
  • LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
  • LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。

2. LinkedList 重要特点

  1. 本质实现:底层使用一个Node数据结构,有前后两个指针,双向链表实现。
  2. 线程安全:非同步的。
  3. 列表长度:LinkedList中元素个数用size记录。
  4. 列表容量:LinkedList是基于链表实现的,因此不存在容量不足的问题,所以这里没有扩容的方法。
  5. 内存:需要更多的内存,LinkedList 每个节点中需要多存储前后节点的信息,占用空间更多些(previous  element next)。
  6. 元素允许为null。
  7. Fail-Fast机制:同ArrayList相同。
  8. 遍历方法:所有指定位置的操作都是从头开始遍历进行的。LinkedList是基于链表实现的,因此插入删除效率高,查找效率低(虽然有一个加速动作)。源码中先将index与长度size的一半比较,如果index<size/2,就只从位置0往后遍历到位置index处,而如果index>size/2,就只从位置size往前遍历到位置index处。这样可以减少一部分不必要的遍历,从而提高一定的效率(实际上效率还是很低)。Arrays.copyOf() 方法:
  9. 它适合删除,插入较多的场景。

5. ConcurrentLinkedDeque接口(JUC)

1. ConcurrentLinkedDeque 结构图

并发队列ConcurrentLinkedDeque,这是一个非阻塞,无锁,无界 ,线程安全双端操作的队列。简单说就是ConcurrentLinkedQueue的升级版,在JDK7之后才提供。该队列也不允许空元素,而且size方法并不是常量时间,其需要遍历链表,此时并发修改链表会造成统计size不正确。同样,bulk操作和equal以及toArray方法不保证原子性。

2. ConcurrentLinkedDeque重要特点

  1. ConcurrentLinkedDeque 是双向链表结构的无界并发队列。从JDK 7开始加入到J.U.C的行列中。
  2. 使用CAS实现并发安全,与 ConcurrentLinkedQueue 的区别是该阻塞队列同时支持FIFOFILO两种操作方式,即可以从队列的头和尾同时操作(插入/删除)。
  3. 适合“多生产,多消费”的场景
  4. 内存一致性遵循对 ConcurrentLinkedDeque 的插入操作先行发生于(happen-before)访问或移除操作

6. BlockingQueue接口(JUC)

1. BlockingQueue 结构图

JDK7提供了7个阻塞队列。分别是

  • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue :一个由链表结构组成的可选有界阻塞队列。如果未指定容量,那么容量将等于 Integer.MAX_VALUE。 2 31-1 
  • PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列,,只有在延迟期满时才能从中提取元素。
  • SynchronousQueue:一个不存储元素、没有内部容量的阻塞队列。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞TransferQueue队列。
  • LinkedBlockingDeque:一个由链表结构组成的可选范围双向阻塞队列。如果未指定容量,那么容量将等于 Integer.MAX_VALUE。 2 31-1 

2. BlockingQueue重要特点

  1. 支持两个附加操作的 Queue,这两个操作是:获取元素时等待队列变为非空,以及存储元素时等待空间变得可用(阻塞)。
  2. BlockingQueue 方法以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同:第一种是抛出一个异常,第二种是返回一个特殊值(null 或 false,具体取决于操作),第三种是在操作可以成功前,无限期地阻塞当前线程,第四种是在放弃前只在给定的最大时间限制内阻塞。下表中总结了这些方法:
  3. BlockingQueue 不接受 null 元素。试图 addput 或 offer 一个 null 元素时,某些实现会抛出 NullPointerExceptionnull 被用作指示 poll 操作失败的警戒值。

  4.  BlockingQueue 可以是限定容量的。它在任意给定时间都可以有一个 remainingCapacity超出此容量,便无法无阻塞地 put 附加元素。没有任何内部容量约束的 BlockingQueue 总是报告 Integer.MAX_VALUE 的剩余容量。

  5.  BlockingQueue 实现主要用于生产者-使用者队列BlockingQueue 可以安全地与多个生产者和多个使用者一起使用。但它另外还支持 Collection 接口。因此,举例来说,使用 remove(x) 从队列中移除任意一个元素是有可能的。然而,这种操作通常 会有效执行,只能有计划地偶尔使用,比如在取消排队信息时。

  6.  BlockingQueue 实现是线程安全的。所有排队方法都可以使用内部锁或其他形式的并发控制来自动达到它们的目的。然而,大量的 Collection 操作(addAllcontainsAllretainAll 和 removeAll没有 必要自动执行,除非在实现中特别说明。因此,举例来说,在只添加了 c 中的一些元素后,addAll(c) 有可能失败(抛出一个异常)。

  7. BlockingQueue 实质上 支持使用任何一种“close”或“shutdown”操作来指示不再添加任何项。这种功能的需求和使用有依赖于实现的倾向。例如,一种常用的策略是:对于生产者,插入特殊的 end-of-stream 或 poison 对象,并根据使用者获取这些对象的时间来对它们进行解释。

  8. 内存一致性效果:当存在其他并发 collection 时,将对象放入 BlockingQueue 之前的线程中的操作 happen-before 随后通过另一线程从 BlockingQueue中访问或移除该元素的操作。

7. ArrayBlockingQueue(JUC)

1. ArrayBlockingQueue 结构图

2. ArrayBlockingQueue 重要特点

  1. 一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。

  2. 这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。

  3. 此类支持对等待的生产者线程和使用者线程进行排序的可选公平策略。默认情况下,不保证是这种排序。然而,通过将公平性 (fairness) 设置为 true 而构造的队列允许按照 FIFO 顺序访问线程。公平性通常会降低吞吐量,但也减少了可变性和避免了“不平衡性”。

  4. ArrayBlockingQueue内部有个循环数组items用来存放队列元素,putIndex下标标示入队元素下标,takeIndex是出队下标,count统计队列元素个数,从定义可知道并没有使用volatile修饰,这是因为访问这些变量使用都是在锁块内,并不存在可见性问题。

  5. 有个独占锁lock用来对出入队操作加锁,这导致同时只有一个线程可以访问入队出队,

  6. notEmpty,notFull条件变量用来进行出入队的同步。另外构造函数必须传入队列大小参数,所以为有界队列,默认是Lock为非公平锁。

  7. offer操作:在队尾插入元素,如果队列满则返回false,否者入队返回true。由于在操作共享变量前加了锁【final ReentrantLock lock = this.lock; 获取独占锁 lock.lock();】,所以不存在内存不可见问题,加过锁后获取的共享变量都是从主内存获取的,而不是在CPU缓存或者寄存器里面的值,释放锁后修改的共享变量值会刷新会主内存中。另外这个队列是使用循环数组实现,所以计算下一个元素存放下标时候有些特殊。【i=putIndex;  return(++i == items.length) ? 0 : i;】另外insert后调用 notEmpty.signal();是为了激活调用notEmpty.await()阻塞后放入notEmpty条件队列中的线程。

  8. put操作:在队列尾部添加元素,如果队列满则等待队列有空位置插入后返回。【final ReentrantLock lock = this.lock;  获取可被中断锁  lock.lockInterruptibly();】如果队列满了那么当前线程会阻塞,直到出队操作调用了notFull.signal方法激活该线程。
  9. size操作:获取队列元素个数,非常精确,因为计算size时候加了独占锁,其他线程不能入队或者出队或者删除元素
  10. ArrayBlockingQueue通过使用全局独占锁实现同时只能有一个线程进行入队或者出队操作,这个锁的粒度比较大,有点类似在方法上添加synchronized的意味。其中offer,poll操作通过简单的加锁进行入队出队操作,而put,take则使用了条件变量实现如果队列满则等待,如果队列空则等待,然后分别在出队和入队操作中发送信号激活等待线程实现同步。另外相比LinkedBlockingQueue,ArrayBlockingQueue的size操作的结果是精确的,因为计算前加了全局锁。

8. LinkedBlockingQueue(JUC)

1. LinkedBlockingQueue 结构图

 

 

一个由链接节点支持的可选有界队列。

2. LinkedBlockingQueue 重要特点

  1. 一个基于已链接节点的、范围任意的 blocking queue。此队列按 FIFO(先进先出)排序元素。队列的头部 是在队列中时间最长的元素。队列的尾部 是在队列中时间最短的元素。新元素插入到队列的尾部,并且队列获取操作会获得位于队列头部的元素。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。

  2. 可选的容量范围构造方法参数作为防止队列过度扩展的一种方法。如果未指定容量,则它等于 Integer.MAX_VALUE除非插入节点会使队列超出容量,否则每次插入后会动态地创建链接节点。

  3. LinkedBlockingQueue中也有两个Node分别用来存放首尾节点,并且里面有个初始值为0的原子变量count用来记录队列元素个数,另外里面有两个ReentrantLock的独占锁,分别用来控制元素入队和出队加锁,其中takeLock用来控制同时只有一个线程可以从队列获取元素,其他线程必须等待,putLock控制同时只能有一个线程可以获取锁去添加元素,其他线程必须等待。另外notEmpty和notFull用来实现入队和出队的同步。 另外由于出入队是两个非公平独占锁,所以可以同时又一个线程入队和一个线程出队,其实这个是个生产者-消费者模型。

9. PriorityBlockingQueue(JUC)

1. PriorityBlockingQueue 结构图

 

一个基于优先级堆的无界优先级阻塞队列。

2. PriorityBlockingQueue 重要特点

  1. 一个基于优先级堆的无界阻塞队列。,它使用与类 PriorityQueue 相同的顺序规则,并且提供了阻塞获取操作。虽然此队列逻辑上是无界的,但是资源被耗尽时试图执行 add 操作也将失败(导致 OutOfMemoryError)。
  2. 此类不允许使用 null 元素。依赖自然顺序的优先级队列也不允许插入不可比较的对象(这样做会导致抛出 ClassCastException)。
  3. 此类及其迭代器可以实现 Collection 和 Iterator 接口的所有可选 方法。iterator() 方法中提供的迭代器并 保证以特定的顺序遍历 PriorityBlockingQueue 的元素。如果需要有序地进行遍历,则应考虑使用 Arrays.sort(pq.toArray())。此外,可以使用方法 drainTo 按优先级顺序移除 全部或部分元素,并将它们放在另一个 collection 中。
  4. 在此类上进行的操作不保证具有同等优先级的元素的顺序。如果需要实施某一排序,那么可以定义自定义类或者比较器,比较器可使用修改键断开主优先级值之间的联系。例如,以下是应用先进先出 (first-in-first-out) 规则断开可比较元素之间联系的一个类。要使用该类,则需要插入一个新的 FIFOEntry(anEntry) 来替换普通的条目对象。
  5. PriorityBlockingQueue内部有个数组queue用来存放队列元素,size用来存放队列元素个数,allocationSpinLockOffset是用来在扩容队列时候做cas的,目的是保证只有一个线程可以进行扩容。
  6. 由于这是一个优先级队列所以有个比较器comparator用来比较元素大小。lock独占锁对象用来控制同时只能有一个线程可以进行入队出队操作。notEmpty条件变量用来实现take方法阻塞模式。这里没有notFull 条件变量是因为这里的put操作是非阻塞的,为啥要设计为非阻塞的是因为这是无界队列。
  7. 最后PriorityQueue q用来搞序列化的。如下构造函数,默认队列容量为11,默认比较器为null;

10. DelayQueue(JUC)

1. DelayQueue 结构图

 

一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。

DelayQueue队列中每个元素都有个过期时间,并且队列是个优先级队列,当从队列获取元素时候,只有过期元素才会出队列。

2. DelayQueue 重要特点

  1. Delayed 元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部 是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且 poll 将返回 null

  2. 当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于等于 0 的值时,将发生到期。即使无法使用 take 或 poll 移除未到期的元素,也不会将这些元素作为正常元素对待。例如,size 方法同时返回到期和未到期元素的计数。

  3. 此队列不允许使用 null 元素。

  4. DelayQueue中内部使用的是PriorityQueue存放数据,使用ReentrantLock实现线程同步,可知是阻塞队列。另外队列里面的元素要实现Delayed接口,一个是获取当前剩余时间的接口,一个是元素比较的接口,因为这个是有优先级的队列。

11. SychronousQueue(JUC)

1. SychronousQueue 结构图

一个不存储元素、没有内部容量的阻塞同步队列。

SynchronousQueue的吞吐量高于LinkedBlockingQueue 和 ArrayBlockingQueue。

2. SychronousQueue 重要特点

  1. 一个不存储元素、没有内部容量的阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有。不能在同步队列上进行 peek,因为仅在试图要移除元素时,该元素才存在;除非另一个线程试图移除某个元素,否则也不能(使用任何方法)插入元素;也不能迭代队列,因为其中没有元素可用于迭代。队列的 是尝试添加到队列中的首个已排队插入线程的元素;如果没有这样的已排队线程,则没有可用于移除的元素并且 poll() 将会返回 null。对于其他 Collection 方法(例如 contains),SynchronousQueue 作为一个空 collection。

  2. 此队列不允许 null 元素

  3. 同步队列类似于 CSP 和 Ada 中使用的 简单聚集(rendezvous)机制信道。它非常适合于传递性设计,在这种设计中,在一个线程中运行的对象要将某些信息、事件或任务传递给在另一个线程中运行的对象,它就必须与该对象同步。 

  4. 对于正在等待的生产者和使用者线程而言,此类支持可选的公平排序策略。默认情况下不保证这种排序。但是,使用公平设置为 true 所构造的队列可保证线程以 FIFO 的顺序进行访问。

  5. SynchronousQueue的吞吐量高于LinkedBlockingQueue 和 ArrayBlockingQueue。

12. LinkedTransferQueue(JUC)

1. LinkedTransferQueue 结构图

LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。

2. LinkedTransferQueue 重要特点

  1. LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。
  2. 相对于其他阻塞队列LinkedTransferQueue多了tryTransfer和transfer方法。
  3. transfer方法。如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。
  4. tryTransfer方法则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。

  5.  对于带有时间限制的tryTransfer(E e, long timeout, TimeUnit unit)方法,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true。

13. LinkedBlockingDeque(JUC)

1. LinkedBlockingDeque 结构图

 

 一个基于已链接节点的、任选范围的阻塞双端队列。

2. LinkedBlockingDeque 重要特点

  1. 一个基于已链接节点的、任选范围的阻塞双端队列。
  2. 可选的容量范围构造方法参数是一种防止过度膨胀的方式。如果未指定容量,那么容量将等于 Integer.MAX_VALUE只要插入元素不会使双端队列超出容量,每次插入后都将动态地创建链接节点。
  3. 大多数操作都以固定时间运行(不计阻塞消耗的时间)。异常包括 removeremoveFirstOccurrenceremoveLastOccurrencecontainsiterator.remove() 以及批量操作,它们均以线性时间运行。

 

摘录网址

  1. 并发队列-无界非阻塞队列 ConcurrentLinkedQueue 原理探究
  2. 并发队列-有界阻塞队列ArrayBlockingQueue原理探究
  3. 并发队列-无界阻塞队列LinkedBlockingQueue原理探究
  4. 并发队列-无界阻塞优先级队列PriorityBlockingQueue原理探究
  5. JUC源码分析-集合篇(八):SynchronousQueue
posted @ 2019-05-04 19:47  海米傻傻  阅读(39089)  评论(0编辑  收藏  举报