本文参考:
Java 多线程编程
这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
一个线程的生命周期
线程,有时被称为轻量级进程(Lightweight Process,LWP)
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
下图显示了一个线程完整的生命周期。
- 新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
- 就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
- 运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
- 阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
-
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
-
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
-
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
-
- 死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
-
线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。Thread.currentThread().getPriority()获取当前线程优先级。.setPriority(int priority)设置优先级
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
创建一个线程
Java 提供了三种创建线程的方法:
- 通过实现 Runnable 接口;
- 通过继承 Thread 类本身;
- 通过 实现Callable , Future 创建线程。
通过 Callable 和 Future 创建线程
-
1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
-
2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
-
3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
-
4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
创建一个类实现Callable接口
public class ThreadDemo implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int i=0;
for(;i<5;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
在main执行。接收 i 的值
public static void main(String[] args) throws InterruptedException, ExecutionException {
ThreadDemo thead = new ThreadDemo();
FutureTask<Integer> ft= new FutureTask<>(thead);
Thread th = new Thread(ft,"thread - 1");
th.start();
System.out.println("i : "+ft.get());
}
输出结果
thread - 1 0
thread - 1 1
thread - 1 2
thread - 1 3
thread - 1 4
i : 5
Thread 方法
下表列出了Thread类的一些重要方法:
序号 | 方法描述 |
---|---|
1 | public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
2 | public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
3 | public final void setName(String name) 改变线程名称,使之与参数 name 相同。 |
4 | public final void setPriority(int priority) 更改线程的优先级。 |
5 | public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。 |
6 | public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。 |
7 | public void interrupt() 中断线程。 |
8 | public final boolean isAlive() 测试线程是否处于活动状态。 |
测试线程是否处于活动状态。 上述方法是被Thread对象调用的。下面的方法是Thread类的静态方法。
序号 | 方法描述 |
---|---|
1 | public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。 |
2 | public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
3 | public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
4 | public static Thread currentThread() 返回对当前正在执行的线程对象的引用。 |
5 | public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。 |
线程安全
对象锁的同步
(1)同步:synchronized:可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”。一个对象有一把锁。多个对象,多个线程就会变成多个锁。可以在方法或者变量对象加上static,是该方法和对象始终具有相同的引用。
同步的概念就是共享,我们要知道“共享”这两个字,如果不是共享的资源,就没有必要进行同步,也就是没有必要进行加锁;
同步的目的就是为了线程的安全,其实对于线程的安全,需要满足两个最基本的特性:原子性和可见性。当多个线程访问某一个类(对象或方法)时,这个类始终能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。
Synchronized锁重入
父子可继承性:当存在父子类关系时,子类完全可以通过可冲入锁,调用父类的同步方法
出现异常时,锁自动释放
(1)关键字Synchronized拥有锁重入的功能,也就是在使用Synchronized的时候,当一个线程得到一个对象的锁后,在该锁里执行代码的时候可以再次请求该对象的锁时可以再次得到该对象的锁。
(2)也就是说,当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。
(3)一个简单的例子就是:在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的,示例代码A如下:
public class SyncDubbo {
public synchronized void method1() {
System.out.println("method1-----");
method2();
}
public synchronized void method2() {
System.out.println("method2-----");
method3();
}
public synchronized void method3() {
System.out.println("method3-----");
}
public static void main(String[] args) {
final SyncDubbo syncDubbo = new SyncDubbo();
new Thread(new Runnable() {
@Override
public void run() {
syncDubbo.method1();
}
}).start();
}
}
结果:
method1-----
method2-----
method3-----
单例模式-双重校验锁:
普通的加锁的单例模式:
public class Singleton {
private static Singleton instance = null; //懒汉模式
//private static Singleton instance = new Singleton(); //饿汉模式
private Singleton() {
}
public static synchronized Singleton newInstance() {
if (null == instance) { //判断实例是否已经被其他线程创建了
instance = new Singleton();
}
return instance;
}
}
使用上述的方式可以实现多线程的情况下获取到正确的实例对象,但是每次访问newInstance()方法都会进行加锁和解锁操作,也就是说该锁可能会成为系统的瓶颈,为了解决这个问题,有人提出了“双重校验锁”的方式,示例代码如下:
public class DubbleSingleton {
private static DubbleSingleton instance;
public static DubbleSingleton getInstance(){
if(instance == null){ //判断实例是否已经被其他线程创建了,如果没有则创建
try {
//模拟初始化对象的准备时间...
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//类上加锁,表示当前对象不可以在其他线程的时候创建
synchronized (DubbleSingleton.class) {
//如果不加这一层判断的话,这样的话每一个线程会得到一个实例
//而不是所有的线程的到的是一个实例
if(instance == null){ //从第一次判断是否为null到加锁之间的时间内判断实例是否已经被创建
instance = new DubbleSingleton();
}
}
}
return instance;
}
}
(双重校验锁的方式相对于线程安全的懒汉模式来说,从表面上是将锁的粒度缩小为方法内部的同步代码块,而不是线程安全的懒汉模式同步整个方法!是锁优化中:减小锁粒度的一种表现形式)
但是,需要注意的是,上述的代码是错误的写法,这是因为:指令重排优化,可能会导致初始化单例对象和将该对象地址赋值给instance字段的顺序与上面Java代码中书写的顺序不同。
例如:线程A在创建单例对象时,在构造方法被调用之前,就为 该对象分配了内存空间并将对象设置为默认值。此时线程A就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有完成初始化操作。线程B来调用newInstance()方法,得到的 就是未初始化完全的单例对象,这就会导致系统出现异常行为。
为了解决上述的问题,可以使用volatile关键字进行修饰instance字段。volatile关键字在这里的含义就是禁止指令的重排序优化(另一个作用是提供内存可见性),从而保证instance字段被初始化时,单例对象已经被完全初始化。
public class DubbleSingleton {
private static volatile DubbleSingleton instance;
public static DubbleSingleton getInstance(){
if(instance == null){
try {
//模拟初始化对象的准备时间...
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//类上加锁,表示当前对象不可以在其他线程的时候创建
synchronized (DubbleSingleton.class) {
//如果不加这一层判断的话,这样的话每一个线程会得到一个实例
//而不是所有的线程的到的是一个实例
if(instance == null){
instance = new DubbleSingleton();
}
}
}
return instance;
}
}
ThreadLocal简介
ThreadLocal主要解决的就是每个线程绑定自己的值,可以将ThreadLocal类比喻成全局存放数据的盒子,盒子中可以存储每个线程的私有变量。
public class ThreadLocalDemo {
public static ThreadLocal<List<String>> threadLocal = new ThreadLocal<>();
public void setThreadLocal(List<String> values) {
threadLocal.set(values);
}
public void getThreadLocal() {
System.out.println(Thread.currentThread().getName());
threadLocal.get().forEach(name -> System.out.println(name));
}
public static void main(String[] args) throws InterruptedException {
final ThreadLocalDemo threadLocal = new ThreadLocalDemo();
new Thread(() -> {
List<String> params = new ArrayList<>(3);
params.add("张三");
params.add("李四");
params.add("王五");
threadLocal.setThreadLocal(params);
threadLocal.getThreadLocal();
}).start();
new Thread(() -> {
try {
Thread.sleep(1000);
List<String> params = new ArrayList<>(2);
params.add("Chinese");
params.add("English");
threadLocal.setThreadLocal(params);
threadLocal.getThreadLocal();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
结果:
Thread-0
张三
李四
王五
Thread-1
Chinese
English
线程间通信简介
Java的一种实现线程间通信的机制是:wait/notify线程间通信
一、wait方法(立即释放锁,一直等待被唤醒或者直接终端)
(1)方法wait()的作用是使当前执行代码的线程进行等待,该方法会将该线程放入”预执行队列“中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。
(2)在调用wait()之前,线程必须获得该对象级别锁,这是一个很重要的地方,很多时候我们可能会忘记这一点,即只能在同步方法或同步块中调用wait()方法。
(3)还需要注意的是wait()是释放锁的,即在执行到wait()方法之后,当前线程会释放锁,当从wait()方法返回前,线程与其他线程竞争重新获得锁。
二、notify方法(等待同步代码块中代码执行完毕以后才释放锁,唤醒等待线程来获取该对象的锁资源)
(1)和wait()方法一样,notify()方法也要在同步块或同步方法中调用,即在调用前,线程也必须获得该对象的对象级别锁。
(2)该方法是用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程,每次只唤醒一个。对其发出通知notify,并使它等待获取该对象的对象锁。
(3)这里需要注意的是,执行notify方法之后,当前线程不会立即释放其拥有的该对象锁,而是执行完之后才会释放该对象锁,被通知的线程也不会立即获得对象锁,而是等待notify方法执行完之后,释放了该对象锁,才可以获得该对象锁。
(4)notifyAll()唤醒所有等待同一共享资源的全部线程从等待状态退出,进入可运行状态,重新竞争获得对象锁。优先级最高的那个线程会最先执行
注意事项:
(1)wait()和notify()方法要在同步块或同步方法中调用,即在调用前,线程也必须获得该对象的对象级别锁。
(2)wait方法是释放锁,notify方法是不释放锁的;
(3)notify每次唤醒wait等待状态的线程都是随机的,且每次只唤醒一个;
(4)notifAll每次唤醒wait等待状态的线程使之重新竞争获取对象锁,优先级最高的那个线程会最先执行;
(5)当线程处于wait()状态时,调用线程对象的interrupt()方法会出现InterruptedException异常
Lock对象简介
这里为什么说Lock对象哪?Lock其实是一个接口,在JDK1.5以后开始提供,其实现类常用的有ReentrantLock,这里所说的Lock对象即是只Lock接口的实现类,为了方便记忆或理解,都简称为Lock对象。
我们知道synchronized关键字可以实现线程间的同步互斥,从JDK1.5开始新增的ReentrantLock类能够达到同样的效果,并且在此基础上还扩展了很多实用的功能,比使用synchronized更佳的灵活。
ReentrantLock的另一个称呼就是“重入锁”,Reentrant的英文释义为:重入。
使用ReentrantLock实现线程同步
public class Run {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
//lambda写法
new Thread(() -> runMethod(lock), "thread1").start();
new Thread(() -> runMethod(lock), "thread2").start();
new Thread(() -> runMethod(lock), "thread3").start();
new Thread(() -> runMethod(lock), "thread4").start();
//常规写法
new Thread(new Runnable() {
@Override
public void run() {
runMethod(lock);
}
}, "thread5").start();
}
private static void runMethod(Lock lock) {
lock.lock();
for (int i = 1; i <= 5; i++) {
System.out.println("ThreadName:" + Thread.currentThread().getName() + (" i=" + i));
}
System.out.println();
lock.unlock();
}
}
结果:
ThreadName:thread1 i=1
ThreadName:thread1 i=2
ThreadName:thread1 i=3
ThreadName:thread1 i=4
ThreadName:thread1 i=5
ThreadName:thread2 i=1
ThreadName:thread2 i=2
ThreadName:thread2 i=3
ThreadName:thread2 i=4
ThreadName:thread2 i=5
ThreadName:thread3 i=1
ThreadName:thread3 i=2
ThreadName:thread3 i=3
ThreadName:thread3 i=4
ThreadName:thread3 i=5
ThreadName:thread4 i=1
ThreadName:thread4 i=2
ThreadName:thread4 i=3
ThreadName:thread4 i=4
ThreadName:thread4 i=5
ThreadName:thread5 i=1
ThreadName:thread5 i=2
ThreadName:thread5 i=3
ThreadName:thread5 i=4
ThreadName:thread5 i=5
使用Lock对象实现线程间通信
我们已经知道可以使用关键字synchronized与wait()方法和notify()方式结合实现线程间通信,也就是等待/通知模式。在ReentrantLock中,是借助Condition对象进行实现的。
Condition对象创建:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Condition按字面意思理解就是条件。这样的话我们可以创建多个Condition条件,我们就可以根据不同的条件来控制现成的等待和通知。而我们还知道,在使用关键字synchronized与wait()方法和notify()方式结合实现线程间通信的时候,notify/notifyAll的通知等待的线程时是随机的,显然使用Condition相对灵活很多,可以实现”选择性通知”。
这是因为,synchronized关键字相当于整个Lock对象只有一个单一的Condition对象,所有的线程都注册到这个对象上。线程开始notifAll的时候,需要通知所有等待的线程,让他们开始竞争获得锁对象,没有选择权,这种方式相对于Condition条件的方式在效率上肯定Condition较高一些。
public class LockConditionDemo {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
//使用同一个LockConditionDemo对象,使得lock、condition一样
LockConditionDemo demo = new LockConditionDemo();
new Thread(() -> demo.await(), "thread1").start();
Thread.sleep(3000);
new Thread(() -> demo.signal(), "thread2").start();
}
private void await() {
try {
lock.lock();
System.out.println("开始等待await! ThreadName:" + Thread.currentThread().getName());
condition.await();
System.out.println("等待await结束! ThreadName:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private void signal() {
lock.lock();
System.out.println("发送通知signal! ThreadName:" + Thread.currentThread().getName());
condition.signal();
lock.unlock();
}
}
结果:
开始等待await! ThreadName:thread1
发送通知signal! ThreadName:thread2
等待await结束! ThreadName:thread1
使用Lock对象和多个Condition实现等待/通知实例
public class LockConditionDemo {
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
LockConditionDemo demo = new LockConditionDemo();
new Thread(() -> demo.await(demo.conditionA), "thread1_conditionA").start();
new Thread(() -> demo.await(demo.conditionB), "thread2_conditionB").start();
new Thread(() -> demo.signal(demo.conditionA), "thread3_conditionA").start();
System.out.println("稍等5秒再通知其他的线程!");
Thread.sleep(5000);
new Thread(() -> demo.signal(demo.conditionB), "thread4_conditionB").start();
}
private void await(Condition condition) {
try {
lock.lock();
System.out.println("开始等待await! ThreadName:" + Thread.currentThread().getName());
condition.await();
System.out.println("等待await结束! ThreadName:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private void signal(Condition condition) {
lock.lock();
System.out.println("发送通知signal! ThreadName:" + Thread.currentThread().getName());
condition.signal();
lock.unlock();
}
}
结果:
开始等待await! ThreadName:thread1_conditionA
开始等待await! ThreadName:thread2_conditionB
发送通知signal! ThreadName:thread3_conditionA
等待await结束! ThreadName:thread1_conditionA
稍等5秒再通知其他的线程!
发送通知signal! ThreadName:thread4_conditionB
等待await结束! ThreadName:thread2_conditionB
可以看出实现了分别通知。因此,我们可以使用Condition进行分组,可以单独的通知某一个分组,另外还可以使用signalAll()方法实现通知某一个分组的所有等待的线程。
公平锁和非公平锁
概念很好理解,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配,即先进先出,那么他就是公平的;非公平是一种抢占机制,是随机获得锁,并不是先来的一定能先得到锁,结果就是不公平的。
ReentrantLock提供了一个构造方法,可以很简单的实现公平锁或非公平锁,源代码构造函数如下:
参数:fair为true表示是公平锁,反之为非公平锁,这里不再写代码测试。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock的其他方法
(1)getHoldCount()方法:查询当前线程保持此锁定的个数,也就是调用lock()的次数;
(2)getQueueLength()方法:返回正等待获取此锁定的线程估计数目;
(3)isFair()方法:判断是不是公平锁;
使用ReentrantReadWriteLock实现并发
上述的类ReentrantLock具有完全互斥排他的效果,即同一时间只能有一个线程在执行ReentrantLock.lock()之后的任务。
类似于我们集合中有同步类容器 和 并发类容器,HashTable(HashTable几乎可以等价于HashMap,并且是线程安全的)也是完全排他的,即使是读也只能同步执行,而ConcurrentHashMap就可以实现同一时刻多个线程之间并发。为了提高效率,ReentrantLock的升级版ReentrantReadWriteLock就可以实现效率的提升。
ReentrantReadWriteLock有两个锁:一个是与读相关的锁,称为“共享锁”;另一个是与写相关的锁,称为“排它锁”。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。
在没有线程进行写操作时,进行读操作的多个线程都可以获取到读锁,而写操作的线程只有获取写锁后才能进行写入操作。即:多个线程可以同时进行读操作,但是同一时刻只允许一个线程进行写操作。
特性:
(1)读读共享;
(2)写写互斥;
(3)读写互斥;
(4)写读互斥;
(1)读读共享:
public class ReentrantReadWriteLockDemo {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) {
ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
new Thread(() -> demo.read(), "ThreadA").start();
new Thread(() -> demo.read(), "ThreadB").start();
}
private void read() {
try {
try {
lock.readLock().lock();
System.out.println("获得读锁" + Thread.currentThread().getName()
+ " 时间:" + System.currentTimeMillis());
//模拟读操作时间为5秒
Thread.sleep(5000);
} finally {
lock.readLock().unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果:
获得读锁ThreadA 时间:1507720692022
获得读锁ThreadB 时间:1507720692022
可以看出两个线程之间,获取锁的时间几乎同时,说明lock.readLock().lock();
允许多个线程同时执行lock()方法后面的代码。
(2)写写互斥
public class ReentrantReadWriteLockDemo {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) {
ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
new Thread(() -> demo.write(), "ThreadA").start();
new Thread(() -> demo.write(), "ThreadB").start();
}
private void write() {
try {
try {
lock.writeLock().lock();
System.out.println("获得写锁" + Thread.currentThread().getName()
+ " 时间:" + System.currentTimeMillis());
//模拟写操作时间为5秒
Thread.sleep(5000);
} finally {
lock.writeLock().unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果:
获得写锁ThreadA 时间:1507720931662
获得写锁ThreadB 时间:1507720936662
(3)读写互斥或写读互斥
public class ReentrantReadWriteLockDemo {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) throws InterruptedException {
ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
new Thread(() -> demo.read(), "ThreadA").start();
Thread.sleep(1000);
new Thread(() -> demo.write(), "ThreadB").start();
}
private void read() {
try {
try {
lock.readLock().lock();
System.out.println("获得读锁" + Thread.currentThread().getName()
+ " 时间:" + System.currentTimeMillis());
Thread.sleep(3000);
} finally {
lock.readLock().unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void write() {
try {
try {
lock.writeLock().lock();
System.out.println("获得写锁" + Thread.currentThread().getName()
+ " 时间:" + System.currentTimeMillis());
Thread.sleep(3000);
} finally {
lock.writeLock().unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果:
获得读锁ThreadA 时间:1507721135908
获得写锁ThreadB 时间:1507721138908