码迷,mamicode.com
首页 > 编程语言 > 详细

java多线程基本概述(九)——ThreadLocal

时间:2017-04-18 23:30:52      阅读:212      评论:0      收藏:0      [点我收藏+]

标签:block   通知   lock   eth   algorithm   fusion   最新   cti   维护   

public interface Lock
Lock implementations provide more extensive locking operations than can be obtained using synchronized methods and statements. 
//Lock对象提供比同步方法或者同步块更多的灵活性和拓展性,
They allow more flexible structuring, may have quite different properties, and may support multiple associated Condition objects.
//它们允许更多更灵活的结构,有很多不同的属性,并且支持关联不同的Condition对象 A lock is a tool
for controlling access to a shared resource by multiple threads. Commonly, a lock provides exclusive access to a shared resource:
//一个Lock对象是一种控制并发访问共享资源的工具。通常,一个Lock对象提供对资源的专有访问:
only one thread at a time can acquire the lock and all access to the shared resource requires that the lock be acquired first.
在同一个时间,只能最新持有锁的线程才能访问共享资源
However, some locks may allow concurrent access to a shared resource, such as the read lock of a ReadWriteLock. //然而,一些锁随想允许多个线程同时访问共享资源,比如:ReadWriteLock. The use of
synchronized methods or statements provides access to the implicit monitor lock associated with every object,
//同步方法或者同步块提供了对与每个对象相关的隐式锁的访问
but forces all lock acquisition and release to occur in a block-structured way: when multiple locks are acquired they must be released in the opposite order,
//但是强制要求锁的获取和释放必须发生在一个块结构中,当要获取多个锁时,它们必须以相反的顺序进行释放。
and all locks must be released in the same lexical scope in which they were acquired. //且必须在与所有锁被获取时相同的词法范围内释放所有锁 While the scoping mechanism
for synchronized methods and statements makes it much easier to program with monitor locks,
//虽然 synchronized 方法和语句的范围机制使得使用监视器锁编程方便了很多,
and helps avoid many common programming errors involving locks, there are occasions where you need to work with locks in a more flexible way.
// 而且还帮助避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。
For example, some algorithms for traversing concurrently accessed data structures require the use of "hand-over-hand" or "chain locking":
you acquire the lock of node A, then node B, then release A and acquire C, then release B and acquire D and so on.
Implementations of the Lock interface enable the use of such techniques by allowing a lock to be acquired and released in different scopes,
and allowing multiple locks to be acquired and released in any order. With
this increased flexibility comes additional responsibility. The absence of block-structured locking removes the automatic release of locks that occurs with synchronized methods and statements. In most cases, the following idiom should be used: Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); } When locking and unlocking occur in different scopes, care must be taken to ensure that all code that is executed while the lock is held is protected by try-finally or try-catch to ensure that the lock is released when necessary. Lock implementations provide additional functionality over the use of synchronized methods and statements by providing a non-blocking attempt to acquire a lock (tryLock()), an attempt to acquire the lock that can be interrupted (lockInterruptibly(), and an attempt to acquire the lock that can timeout (tryLock(long, TimeUnit)). A Lock class can also provide behavior and semantics that is quite different from that of the implicit monitor lock, such as guaranteed ordering, non-reentrant usage, or deadlock detection. If an implementation provides such specialized semantics then the implementation must document those semantics. Note that Lock instances are just normal objects and can themselves be used as the target in a synchronized statement. Acquiring the monitor lock of a Lock instance has no specified relationship with invoking any of the lock() methods of that instance. It is recommended that to avoid confusion you never use Lock instances in this way, except within their own implementation. Except where noted, passing a null value for any parameter will result in a NullPointerException being thrown. Memory Synchronization All Lock implementations must enforce the same memory synchronization semantics as provided by the built-in monitor lock, as described in section 17.4 of The Java? Language Specification: A successful lock operation has the same memory synchronization effects as a successful Lock action. A successful unlock operation has the same memory synchronization effects as a successful Unlock action. Unsuccessful locking and unlocking operations, and reentrant locking/unlocking operations, do not require any memory synchronization effects. Implementation Considerations The three forms of lock acquisition (interruptible, non-interruptible, and timed) may differ in their performance characteristics, ordering guarantees, or other implementation qualities. Further, the ability to interrupt the ongoing acquisition of a lock may not be available in a given Lock class. Consequently, an implementation is not required to define exactly the same guarantees or semantics for all three forms of lock acquisition, nor is it required to support interruption of an ongoing lock acquisition. An implementation is required to clearly document the semantics and guarantees provided by each of the locking methods. It must also obey the interruption semantics as defined in this interface, to the extent that interruption of lock acquisition is supported: which is either totally, or only on method entry. As interruption generally implies cancellation, and checks for interruption are often infrequent, an implementation can favor responding to an interrupt over normal method return. This is true even if it can be shown that the interrupt occurred after another action may have unblocked the thread. An implementation should document this behavior.

下面的翻译:

随着灵活性的增加,也带来了更多的责任。不使用块结构锁就失去了使用 synchronized 方法和语句时会出现的锁自动释放功能。在大多数情况下,应该使用以下语句:

     Lock l = ...; 
     l.lock();
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();
     }
 
锁定和取消锁定出现在不同作用范围中时,必须谨慎地确保保持锁定时所执行的所有代码用 try-finallytry-catch 加以保护,以确保在必要时释放锁。
Lock 实现提供了使用 synchronized 方法和语句所没有的其他功能,包括提供了一个非块结构的获取锁尝试 (tryLock())、一个获取可中断锁的尝试 (lockInterruptibly()) 和一个获取超时失效锁的尝试 (tryLock(long, TimeUnit))。

Lock 类还可以提供与隐式监视器锁完全不同的行为和语义,如保证排序、非重入用法或死锁检测。如果某个实现提供了这样特殊的语义,则该实现必须对这些语义加以记录。

注意,Lock 实例只是普通的对象,其本身可以在 synchronized 语句中作为目标使用。获取 Lock 实例的监视器锁与调用该实例的任何 lock() 方法没有特别的关系。为了避免混淆,建议除了在其自身的实现中之外,决不要以这种方式使用 Lock 实例。

除非另有说明,否则为任何参数传递 null 值都将导致抛出 NullPointerException。

内存同步

所有 Lock 实现都必须 实施与内置监视器锁提供的相同内存同步语义,如 The Java Language Specification, Third Edition (17.4 Memory Model) 中所描述的:

成功的 lock 操作与成功的 Lock 操作具有同样的内存同步效应。
成功的 unlock 操作与成功的 Unlock 操作具有同样的内存同步效应。
不成功的锁定与取消锁定操作以及重入锁定/取消锁定操作都不需要任何内存同步效果。
实现注意事项

三种形式的锁获取(可中断、不可中断和定时)在其性能特征、排序保证或其他实现质量上可能会有所不同。而且,对于给定的 Lock 类,可能没有中断正在进行的 锁获取的能力。因此,并不要求实现为所有三种形式的锁获取定义相同的保证或语义,也不要求其支持中断正在进行的锁获取。实现必需清楚地对每个锁定方法所提供的语义和保证进行记录。还必须遵守此接口中定义的中断语义,以便为锁获取中断提供支持:完全支持中断,或仅在进入方法时支持中断。

由于中断通常意味着取消,而通常又很少进行中断检查,因此,相对于普通方法返回而言,实现可能更喜欢响应某个中断。即使出现在另一个操作后的中断可能会释放线程锁时也是如此。实现应记录此行为。

例子:

package soarhu;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class TestNum {
  private Lock lock = new ReentrantLock(true);
    private int a = 1;
  public void test(){
        lock.lock();
      try {
          for (int i = 0; i < 5; i++) {
              System.out.println(Thread.currentThread().getName()+" "+a++);
          }
      } finally {
          lock.unlock();
      }
  }
    public synchronized void test2(){
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()+" "+a++);
            }
        } finally {
        }
    }
}
public class Test{
    public static void main(String[] args)  {
        TestNum t = new TestNum();
        for (int i = 0; i < 3; i++) {
            new Thread(){
                @Override
                public void run() {
                       t.test();
                      // t.test2();
                }
            }.start();
        }
    }
}

输出结果:test()与test2()实现了相同的效果:

Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-1 6
Thread-1 7
Thread-1 8
Thread-1 9
Thread-1 10
Thread-2 11
Thread-2 12
Thread-2 13
Thread-2 14
Thread-2 15

Process finished with exit code 0

         关键字synchronized与wait(),notify()/notifyAll()相结合可以实现等待/通知模式。类ReentrantLock也可以实现相同的功能,但须要借助Condition对象。Condition类时JDK5中出现的,它有更好的灵活性,比如可以多路通知功能,也就是一个Lock对象里面可以创建多个Condition(对象监视器)实例,线程对象可以注册在指定的Condition中,从而有选择的通知线程,在线程调度上更加灵活。

   在使用notify()/notifyAll()时,被通知的线程是有JVM随机选择的,但ReentrantLock对象结合Conditon类可以实现前面介绍过的“选择性通知”,这个功能是很重要的。

   而synchronized相当于整个Lock对象中只有一个单一的Condition对象,所有的线程都注册在它的一个对象身上。线程开始notifyAll()时,需要通知所有的WAITING线程,没有选择权,例如:多个wait()的线程和其他notify()的线程都在等待时,那么这些线程可能都会唤醒,而通常我们只是想唤醒处于wait()的等待中的线程。那么Condition就提供了很好的处理。

  

public interface Condition
Condition factors out the Object monitor methods (wait, notify and notifyAll) into distinct objects to give the effect of having multiple wait-sets per object, by combining them with the use of arbitrary Lock implementations. Where a Lock replaces the use of synchronized methods and statements, a Condition replaces the use of the Object monitor methods.
Conditions (also known as condition queues or condition variables) provide a means for one thread to suspend execution (to "wait") until notified by another thread that some state condition may now be true. Because access to this shared state information occurs in different threads, it must be protected, so a lock of some form is associated with the condition. The key property that waiting for a condition provides is that it atomically releases the associated lock and suspends the current thread, just like Object.wait.

A Condition instance is intrinsically bound to a lock. To obtain a Condition instance for a particular Lock instance use its newCondition() method.

As an example, suppose we have a bounded buffer which supports put and take methods. If a take is attempted on an empty buffer, then the thread will block until an item becomes available; if a put is attempted on a full buffer, then the thread will block until a space becomes available. We would like to keep waiting put threads and take threads in separate wait-sets so that we can use the optimization of only notifying a single thread at a time when items or spaces become available in the buffer. This can be achieved using two Condition instances.

 class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length)
         notFull.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }

   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0)
         notEmpty.await();
       Object x = items[takeptr];
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }
   }
 }
 
(The ArrayBlockingQueue class provides this functionality, so there is no reason to implement this sample usage class.)
A Condition implementation can provide behavior and semantics that is different from that of the Object monitor methods, such as guaranteed ordering for notifications, or not requiring a lock to be held when performing notifications. If an implementation provides such specialized semantics then the implementation must document those semantics.

Note that Condition instances are just normal objects and can themselves be used as the target in a synchronized statement, and can have their own monitor wait and notification methods invoked. Acquiring the monitor lock of a Condition instance, or using its monitor methods, has no specified relationship with acquiring the Lock associated with that Condition or the use of its waiting and signalling methods. It is recommended that to avoid confusion you never use Condition instances in this way, except perhaps within their own implementation.

Except where noted, passing a null value for any parameter will result in a NullPointerException being thrown.

Implementation Considerations

When waiting upon a Condition, a "spurious wakeup" is permitted to occur, in general, as a concession to the underlying platform semantics. This has little practical impact on most application programs as a Condition should always be waited upon in a loop, testing the state predicate that is being waited for. An implementation is free to remove the possibility of spurious wakeups but it is recommended that applications programmers always assume that they can occur and so always wait in a loop.

The three forms of condition waiting (interruptible, non-interruptible, and timed) may differ in their ease of implementation on some platforms and in their performance characteristics. In particular, it may be difficult to provide these features and maintain specific semantics such as ordering guarantees. Further, the ability to interrupt the actual suspension of the thread may not always be feasible to implement on all platforms.

Consequently, an implementation is not required to define exactly the same guarantees or semantics for all three forms of waiting, nor is it required to support interruption of the actual suspension of the thread.

An implementation is required to clearly document the semantics and guarantees provided by each of the waiting methods, and when an implementation does support interruption of thread suspension then it must obey the interruption semantics as defined in this interface.

As interruption generally implies cancellation, and checks for interruption are often infrequent, an implementation can favor responding to an interrupt over normal method return. This is true even if it can be shown that the interrupt occurred after another action that may have unblocked the thread. An implementation should document this behavior.

翻译内容如下:

Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。

条件(也称为条件队列 或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式 释放相关的锁,并挂起当前线程,就像 Object.wait 做的那样。

Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法。

作为一个示例,假定有一个绑定的缓冲区,它支持 put 和 take 方法。如果试图在空的缓冲区上执行 take 操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行 put 操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set 中保存 put 线程和 take 线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个 Condition 实例来做到这一点。

 class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length) 
         notFull.await();
       items[putptr] = x; 
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }

   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0) 
         notEmpty.await();
       Object x = items[takeptr]; 
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }
   } 
 }
 
(ArrayBlockingQueue 类提供了这项功能,因此没有理由去实现这个示例类。)
Condition 实现可以提供不同于 Object 监视器方法的行为和语义,比如受保证的通知排序,或者在执行通知时不需要保持一个锁。如果某个实现提供了这样特殊的语义,则该实现必须记录这些语义。

注意,Condition 实例只是一些普通的对象,它们自身可以用作 synchronized 语句中的目标,并且可以调用自己的 wait 和 notification 监视器方法。获取 Condition 实例的监视器锁或者使用其监视器方法,与获取和该 Condition 相关的 Lock 或使用其 waiting 和 signalling 方法没有什么特定的关系。为了避免混淆,建议除了在其自身的实现中之外,切勿以这种方式使用 Condition 实例。

除非另行说明,否则为任何参数传递 null 值将导致抛出 NullPointerException。

实现注意事项

在等待 Condition 时,允许发生“虚假唤醒”,这通常作为对基础平台语义的让步。对于大多数应用程序,这带来的实际影响很小,因为 Condition 应该总是在一个循环中被等待,并测试正被等待的状态声明。某个实现可以随意移除可能的虚假唤醒,但建议应用程序程序员总是假定这些虚假唤醒可能发生,因此总是在一个循环中等待。

三种形式的条件等待(可中断、不可中断和超时)在一些平台上的实现以及它们的性能特征可能会有所不同。尤其是它可能很难提供这些特性和维护特定语义,比如排序保证。更进一步地说,中断线程实际挂起的能力在所有平台上并不是总是可行的。

因此,并不要求某个实现为所有三种形式的等待定义完全相同的保证或语义,也不要求其支持中断线程的实际挂起。

要求实现清楚地记录每个等待方法提供的语义和保证,在某个实现不支持中断线程的挂起时,它必须遵从此接口中定义的中断语义。

由于中断通常意味着取消,而又通常很少进行中断检查,因此实现可以先于普通方法的返回来对中断进行响应。即使出现在另一个操作后的中断可能会释放线程锁时也是如此。实现应记录此行为。

例子:

package extthread;

import service.MyService;

public class ThreadA extends Thread {

    private MyService service;

    public ThreadA(MyService service) {
        super();
        this.service = service;
    }

    @Override
    public void run() {
        service.await();
    }
}
-------------------------------------------------
package service;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyService {

    private Lock lock = new ReentrantLock();
    public Condition condition = lock.newCondition();

    public void await() {
        try {
            lock.lock();
            System.out.println(" await时间为" + System.currentTimeMillis());
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void signal() {
        try {
            lock.lock();
            System.out.println("signal时间为" + System.currentTimeMillis());
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
}
---------------------------------------------------------------------------------
package test;

import service.MyService;
import extthread.ThreadA;

public class Run {

    public static void main(String[] args) throws InterruptedException {

        MyService service = new MyService();

        ThreadA a = new ThreadA(service);
        a.start();

        Thread.sleep(3000);

        service.signal();

    }

}

输出结果:

 await时间为1492527853327
signal时间为1492527856339

wait()等价于awiat,notify()等价于notifyAll()方法

用如果只有一个Condition那么效果和notifyAll()一样,例子:

package test;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

 class ThreadA extends Thread {
    private MyService service;
    public ThreadA(MyService service) {
        super();
        this.service = service;
    }
    @Override
    public void run() {
        service.awaitA();
    }
}
 class ThreadB extends Thread {
        private MyService service;
        public ThreadB(MyService service) {
            super();
            this.service = service;
        }
        @Override
        public void run() {
            service.awaitB();
        }
    }

class MyService {

     private Lock lock = new ReentrantLock();
     public Condition condition = lock.newCondition();

     public void awaitA() {
         try {
             lock.lock();
             System.out.println("begin awaitA时间为" + System.currentTimeMillis()
                     + " ThreadName=" + Thread.currentThread().getName());
             condition.await();
             System.out.println("  end awaitA时间为" + System.currentTimeMillis()
                     + " ThreadName=" + Thread.currentThread().getName());
         } catch (InterruptedException e) {
             e.printStackTrace();
         } finally {
             lock.unlock();
         }
     }

     public void awaitB() {
         try {
             lock.lock();
             System.out.println("begin awaitB时间为" + System.currentTimeMillis()
                     + " ThreadName=" + Thread.currentThread().getName());
             condition.await();
             System.out.println("  end awaitB时间为" + System.currentTimeMillis()
                     + " ThreadName=" + Thread.currentThread().getName());
         } catch (InterruptedException e) {
             e.printStackTrace();
         } finally {
             lock.unlock();
         }
     }

     public void signalAll() {
         try {
             lock.lock();
             System.out.println("  signalAll时间为" + System.currentTimeMillis()
                     + " ThreadName=" + Thread.currentThread().getName());
             condition.signalAll();
         } finally {
             lock.unlock();
         }
     }
 }

public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyService service = new MyService();
        ThreadA a = new ThreadA(service);
        a.setName("A");
        a.start();
        ThreadB b = new ThreadB(service);
        b.setName("B");
        b.start();
        Thread.sleep(3000);
        service.signalAll();

    }

}

输出结果:

begin awaitA时间为1492528283299 ThreadName=A
begin awaitB时间为1492528283300 ThreadName=B
  signalAll时间为1492528286305 ThreadName=main
  end awaitA时间为1492528286305 ThreadName=A
  end awaitB时间为1492528286305 ThreadName=B

可以看到a,b线程同时被唤醒。

package test;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

 class ThreadA extends Thread {
    private MyService service;
    public ThreadA(MyService service) {
        super();
        this.service = service;
    }
    @Override
    public void run() {
        service.awaitA();
    }
}
 class ThreadB extends Thread {
        private MyService service;
        public ThreadB(MyService service) {
            super();
            this.service = service;
        }
        @Override
        public void run() {
            service.awaitB();
        }
    }

class MyService {

     private Lock lock = new ReentrantLock();
     public Condition conditionA = lock.newCondition();
     public Condition conditionB = lock.newCondition();
     
     public void awaitA() {
         try {
             lock.lock();
             System.out.println("begin awaitA时间为" + System.currentTimeMillis()
                     + " ThreadName=" + Thread.currentThread().getName());
             conditionA.await();
             System.out.println("  end awaitA时间为" + System.currentTimeMillis()
                     + " ThreadName=" + Thread.currentThread().getName());
         } catch (InterruptedException e) {
             e.printStackTrace();
         } finally {
             lock.unlock();
         }
     }

     public void awaitB() {
         try {
             lock.lock();
             System.out.println("begin awaitB时间为" + System.currentTimeMillis()
                     + " ThreadName=" + Thread.currentThread().getName());
             conditionB.await();
             System.out.println("  end awaitB时间为" + System.currentTimeMillis()
                     + " ThreadName=" + Thread.currentThread().getName());
         } catch (InterruptedException e) {
             e.printStackTrace();
         } finally {
             lock.unlock();
         }
     }

     public void signalAll_A() {
         try {
             lock.lock();
             System.out.println("  signalAll时间为" + System.currentTimeMillis()
                     + " ThreadName=" + Thread.currentThread().getName());
             conditionA.signalAll();
         } finally {
             lock.unlock();
         }
     }
     
     public void signalAll_B() {
         try {
             lock.lock();
             System.out.println("  signalAll时间为" + System.currentTimeMillis()
                     + " ThreadName=" + Thread.currentThread().getName());
             conditionB.signalAll();
         } finally {
             lock.unlock();
         }
     }
 }

public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyService service = new MyService();
        ThreadA a = new ThreadA(service);
        a.setName("A");
        a.start();
        ThreadB b = new ThreadB(service);
        b.setName("B");
        b.start();
        Thread.sleep(3000);
        service.signalAll_A();

    }

}

输出:等待BCondition释放。一个锁可以有几个监视器。

begin awaitA时间为1492528603637 ThreadName=A
begin awaitB时间为1492528603637 ThreadName=B
  signalAll时间为1492528606640 ThreadName=main
  end awaitA时间为1492528606641 ThreadName=A

 

java多线程基本概述(九)——ThreadLocal

标签:block   通知   lock   eth   algorithm   fusion   最新   cti   维护   

原文地址:http://www.cnblogs.com/soar-hu/p/6731138.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!