多线程中同步控制和锁

474 阅读17分钟

同步控制是并发程序必不可少的重要手段。synchronized关键字就是一种最简单的控制方法。同时,wait()和notify()方法起到了线程等待和通知的作用。这些工具对于实现复杂的多线程协作起到了重要的作用。接下来将介绍synchronized,wait,notify方法的代替品(或者说是增强版)——重入锁,这个专题需要大家对多线程基本的内部锁synchronized,wait, notify方法先有基本的认识。

1 synchronized的功能扩展: 重入锁

重入锁完全可以代替synchronized关键字。在早期JDK版本,重入锁的性能远远优于synchronized关键字,在JDK后期版本,对synchronized关键字做了大量的优化,使得两者的性能差不多。

下面展示一段简单的重入锁ReentrantLock使用案例:

public class ReenterLock implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();
    public static int i=0;

    public void run() {
        for(int j=0;j<10000000;j++){
            lock.lock();
//            lock.lock();
            try {
                i++;
            }finally {
                lock.unlock();
//                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReenterLock r1 = new ReenterLock();
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r1);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

​ 上述代码创建了一个全局的ReentrantLock对象,这个对象就是重入锁对象,该对象的lock()和unlock()方法之间7~12行的代码区域就是重入锁的保护临界区,确保了多线程对i变量的操作安全性。

​ 从这段代码可以看到,与synchronized相比,重入锁有着显示操作的过程。开发人员必须手动指定何时加锁 ,何时释放锁。也正是因为这样,重入锁逻辑控制远远要好于synchronized。但值得注意的是,在退出临界区时,必须记得要释放锁,否者永远没有机会再访问临界区了,会造成其线程的饥饿甚至是死锁。

​ 重入锁之所以被称作重入锁是因为重入锁是可以反复进入的。当然,这里的反复进入仅仅局限于一个线程。上诉代码还可以这样写:

    public void run() {
        for(int j=0;j<10000000;j++){
            lock.lock();
            lock.lock();
            try {
                i++;
            }finally {
                lock.unlock();
                lock.unlock();
            }
        }
    }

​ 在这种情况下,一个线程连续两次获得同一把锁。这是允许的!但要注意的是,如果一个线程多次获得锁,那么在释放锁的时候,也必须释放相同次数。如果释放的次数多了,那么会得到一个java.lang.IllegalMonitorStateException异常,反之,如果释放所得次数少了,MAME相当于县城还持有这个锁,因此,其他线程也无法进入临界区

​ 处使用上的灵活性以外,重入所还提供了一些高级功能。比如重入锁提供的中断处理的能力

1.1 中断响应

​ 重入锁除了提供上述的基本功能外,还提供了一些高级功能。比如,重入锁可以提供中断处理的能力。这是一个非常重要的功能,synchronized是没有中断功能的。在等待锁的过程中,程序可以根据需要取消对锁的请求。这是synchronized办不到的。也就是说,重入锁具有解除死锁的功能。

​ 比如你和朋友越好一起去打球,如果你等了半个小时朋友没有到,你突然接到一个电话,说由于突发情况,朋友不能如期钱来了,那么你一定扫兴的达到回府了。中断正是提供了一套类似的机制。如果一个县城正在等待锁,那么他依然可以收到一个通知,被告知无需等待,可以停止工作了,这种情况对于处理死锁是有一定帮助的。

​ 下面的代码产生了一个死锁,得益于锁的中断,我们可以轻易的解决这个死锁:

public class IntLock implements Runnable {
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;

    public IntLock(int lock) {
        this.lock = lock;
    }

    public void run() {
        try {
            if (this.lock == 1) {
                lock1.lockInterruptibly();
                Thread.sleep(500);
                lock2.lockInterruptibly();
            } else {
                lock2.lockInterruptibly();
                Thread.sleep(500);
                lock1.lockInterruptibly();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock1.isHeldByCurrentThread())
                lock1.unlock();
            if (lock2.isHeldByCurrentThread())
                lock2.unlock();
            System.out.println(this.lock + "线程退出");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        IntLock r1 = new IntLock(1);
        IntLock r2 = new IntLock(2);
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        t2.interrupt();
    }
}

​ 线程t1和线程t2启动后,t1先占用lock1,再占用lock2;t2先占用lock2,再请求lock1。这样很容易形成t1和t2之间的互相等待,造成死锁。在这里,对锁的请求,统一使用lockInterruptibly()方法。这是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中可以响应中断。

​ 在t1和t2线程start后,主线程39行main进入休眠,此时t1和t2线程处于死锁状态,然后主线程第40行main中断t2线程,故t2会放弃对lock1的请求,同时释放lock2。这个操作使得t1可以获得lock2从而继续执行下去。

​ 执行上诉代码,将输出:

2线程退出
1线程退出
java.lang.InterruptedException
	at com.lxs.demo.IntLock.run(IntLock.java:24)
	at java.lang.Thread.run(Thread.java:745)

​ 可以看到,中断后,两个线程双双退出。但真正完成工作的只有t1。而t2放弃任务直接退出,释放资源。

1.2 锁申请等待限时

​ 除了等待外部通知之外,还有一种避免死锁的方法,就是限时等待。通常,我们不会预料到系统在什么时候会产生死锁,就无法主动的解除死锁,最好的系统设计方式是,这个系统根本就不会产生死锁。我们可以用tryLock()方法进行限时等待。

​ 下面这段代码展示了限时等待锁的使用:

public class TimeLock implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();

    public void run() {
        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                Thread.sleep(6000);
            } else {
                System.out.println("Get lock failed");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if(lock.isHeldByCurrentThread())
                lock.unlock();
        }
    }

    public static void main(String [] args){
        TimeLock lock1 = new TimeLock();
        Thread t1 = new Thread(lock1);
        Thread t2 = new Thread(lock1);
        t1.start();
        t2.start();
    }
}

输出结果:

Get lock failed

​ 在这里,tryLock()接收两个参数,一个表示等待时长,另一个表示计时单位。这里设置为秒,时长为5,表示线程在这个锁的请求中,最多等待5秒。如果超过5秒还没有得到锁就返回false。如果成功就返回true。 ​ 在本例中,由于占用锁的线程会持有锁长达6秒,故另外一个线程无法在5秒内获得锁,因此,对锁的请求会失败。

​ tryLock()方法也可以不带参数直接运行,在这种情况下,当前进程会尝试获得锁,如果锁并未被其他进程占用,则申请就会成功,立即返回true。如果锁被其他线程占用,会立即返回false。这种模式不会引起线程的等待,因此不会造成死锁。下面演示了这种使用方式:

public class TryLock implements Runnable {
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;

    public TryLock(int lock) {
        this.lock = lock;
    }

    public void run() {
        if (lock == 1) {
            while (true) {
                if(lock1.tryLock()){
                    try {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        if (lock2.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getId() + " my job done");
                                return;
                            } finally {
                                lock2.unlock();
                            }
                        }
                    }finally {
                        lock1.unlock();
                    }
                }
            }
        } else {
            while (true) {
                if(lock2.tryLock()){
                    try {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        if (lock1.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getId() + " my job done");
                                return;
                            } finally {
                                lock1.unlock();
                            }
                        }
                    }finally {
                        lock2.unlock();
                    }
                }
            }
        }
    }

    public static void main(String [] args){
        TryLock lock1 = new TryLock(1);
        TryLock lock2 = new TryLock(2);
        Thread t1 = new Thread(lock1);
        Thread t2 = new Thread(lock2);
        t1.start();
        t2.start();
    }
}

​ 上述代码中,采用了非常容易死锁的加锁顺序。也就是先让线程t1请求lock1,在请求lock2,而让t2先请求lock2,在请求lock1。在一般情况下,这样会导致t1,t2互相等待,从而引起死锁。 ​ 但是采用tryLock后,这种情况得到了改善。由于线程不会傻傻的等待,而是不停的尝试,因此,只要执行足够长的时间,线程总是会获得所需要的资源,从而正常执行(这里以线程能同时获得lock1和lock2两把锁视为正常执行。

代码执行结果如下:

12 my job done
11 my job done

1.3 公平锁

​ 在大多数情况下,锁的申请都是非公平的。也就是说,线程1首先请求了锁A,接着线程2也请求了锁A。那么锁A可用时,线程1可以获得锁还是线程2可以获得锁呢?这是不一定的,系统只会从这个锁的等待队列中随机挑取一个。因此不能保证公平性。 ​ 而接下来要讲的公平锁,他会按照时间的先后顺序,保证先到者先得,后到者后得。所以,公平锁的最大特点就是,他不会产生饥饿现象。 ​ 注意:如果线程采用synchronized进行互斥,那么产生的锁是非公平的。而重入锁允许我们进行公平性设置。他有一个如下的构造函数:

public ReentranLock(boolean fair);

​ 当参数fair为true时,表示锁是公平的。公平锁看起来很优美,但是要实现公平锁,必然要求系统维护一个有序队列,因此对公平锁得到实现成本比较高,意味着公平锁的效率非常低下,因此,在默认情况下,锁是非公平的。如果没有什么特别的需求,尽量别用公平锁。

​ 下面代码能很好的凸显公平锁的特点:

public class FairLock implements Runnable {
    public static ReentrantLock fairLock = new ReentrantLock(true);
//    public static ReentrantLock fairLock = new ReentrantLock();

    public void run() {
        while (true){
            try {
                fairLock.lock();
                System.out.println(Thread.currentThread().getName());
            }finally {
                fairLock.unlock();
            }
        }
    }

    public static void main(String [] args){
        FairLock r1 = new FairLock();
        Thread t1 = new Thread(r1,"Thread_t1");
        Thread t2 = new Thread(r1,"Thread_t2");
        t1.start();
        t2.start();
    }
}

代码执行结果:

Thread_t1
Thread_t2
Thread_t1
Thread_t2
Thread_t1
Thread_t2

可以看到,线程的调度是公平的。

对上面的ReentantLock的几个方法整理如下

  • lock():获得锁,如果锁已经被占用,则等待。
  • lockInterruptibly():获得锁,但优先响应中断。
  • tryLock():尝试获得锁,如果成功,则返回true,失败返回false。该方法不等待,立即返回
  • tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁。
  • unlock():释放锁。

2 重入锁的好搭档:Condition

​ 如果大家了解object.wait()方法和object.notify()方法的,那么就能很容易理解condition对象了。他和wait()和notify()方法的作用是基本相同的。但是wait()和notify()方法是与synchronized关键字组合使用的,而condition是与重入锁相关联的。 Condition接口提供的基本方法如下:

void await() throws InterrupteException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterrupteException;
boolean await(long time, TimeUnit unit) throws InterrupteException;
boolean awaitUntil(Data deadline) throws InterrupteException;
void signal();
void signalAll();

以上方法含义如下:

  • await()方法会使当前线程等待,同时释放当前锁,当其他线程使用signal()或signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和object.wait()方法很相似。

  • awaitUninterruptibly()方法与wait()方法相同,唯一的不同点是,该方法不会再等待的过程中响应中断。

  • signal()方法用于唤醒一个在等待中的线程。signalAll()会唤醒所有正在等待的线程。这和object.notify()方法很相似。

下面代码简单的演示了Condition的作用:

public class ReenterLockCondition implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();

    public void run() {
        try{
            lock.lock();
            condition.await();
            System.out.println("Thread is going on");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String [] args) throws InterruptedException {
        ReenterLockCondition r1 = new ReenterLockCondition();
        Thread t1= new Thread(r1);
        t1.start();
        Thread.sleep(2000);
        lock.lock();
        condition.signal();
        lock.unlock();
    }
}

​ 第3行代码先通过lock生成一个与之绑定的condition对象。第8行代码要求线程在condition对象上进行等待。主线程main在两秒后发出signal通知,告知等待在condition上的线程可以继续执行了。

​ 和object.wait()和object.notify()一样,当线程使用Condition.wait()时,要求线程持有相关的重入锁,在condition.wait()调用后,这个线程会主动释放这把锁。并且,在condition.signal()方法调用时,也要求线程获取相关的锁。注意,在signal()方法调用之后,一定要释放相关的锁第24行,把锁让给其他线程。如果省略了24行,那么,虽然已经唤醒了县城t1,但是由于无法重新获得锁,因为也就无法真正的继续执行。

3 允许多个线程同时访问:信号量(Semaphore)

​ 信号量为多线程协作提供了更为强大的控制方法。广义上说,信号量是对锁的扩展。无论是内部锁synchronized还是重入锁ReentranLock,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问一个资源。信号量主要提供了一下的构造函数:

public Semaphore(int permits);

public Semaphore(int permits, boolean fair); //第二个参数可以指定是否公平

​ 在构造信号量时,必须指定信号量的准入数,即同时能申请几个许可。当每个线程只申请一个许可时,这就相当于指定了同时能有多少个线程可以访问某个资源。信号量的主要逻辑方法有:

public void acquire();

public void acquireUninterruptibly();

public boolean tryAcquire();

public boolean tryAcquire(long timeout, TimeUnit unit);

public void release();

​ acquire()方法尝试获得一个准入的许可。若无法获得,则线程会等待,直到申请到许可或者当前线程被中断。acquireUninterruptibly()方法与acquire()方法类似,但不响应中断。tryAcquire()尝试获得一个许可,成功返回true失败返回false,它不会进行阻塞等待,立即返回。release()用于在线程访问资源结束后,释放一个许可,以使其他等待许可的线程可以进行资源访问。

下面是Semaphore的简单使用:

public class SemapDemo implements Runnable {
    //5个一组输出
    final Semaphore semp = new Semaphore(5);

    public void run() {
        try {
            semp.acquire();
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getId() + " done!");
            semp.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String []args){
        ExecutorService exec = Executors.newFixedThreadPool(20);
        final SemapDemo demo = new SemapDemo();
        for(int i=0;i<20;i++){
            exec.submit(demo);
        }
    }
}

在本例中同时开启了20个线程。观察上述程序的输出,你会发现线程以5个线程为一组依次输出。

4 ReadWriteLock 读写锁

​ ReadWriteLock 是JDK5中提供的读写分离锁。读写锁能有效的帮助减少锁竞争,以提升系统性能。用锁分离的机制来提升性能非常容易理解,比如县城A1、A2、A3进行写操作,线程B1、B2、B3进行读操作,如果使用重入锁或者内部锁,从理论上说所有读之间、读和写之间、写和写之间都是串行操作。当B1进行读时,B2、B3则需要等待锁。由于读操作并不对数据的完整性造成破坏,这种等待显然是不合理的。因此读写所就用了发挥的余地。

​ 在这种情况下,读写所容许多个线程同时读,是的B1、B2、B3之间并行。但是,考虑到数据完整性,写写,和读写操作依然是需要相互等待和持有锁的。总的来说读写锁约束访问情况如下表

非阻塞 阻塞
阻塞 阻塞
  • 读-读不互斥:读读之间不阻塞。

  • 读-写互斥:读阻塞写,写也会阻塞读。

  • 写-写互斥:写写阻塞。

​ 如果系统中,读操作次数远远大于写操作,则读写锁可以发挥最大的功效,提升系统性能。这里给出一个稍微夸张的案例,来说明读写锁对性能的帮助。

public class ReadWriteLockDemo {
    private static Lock lock = new ReentrantLock();
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private int value;

    public Object handleRead(Lock lock) throws InterruptedException {
        try {
            lock.lock();
            Thread.sleep(1000);
            System.out.println("read success");
            return value;
        } finally {
            lock.unlock();
        }
    }

    public void handleWrite(Lock lock, int index) throws InterruptedException {
        try {
            lock.lock();
            Thread.sleep(1000);
            value = index;
            System.out.println("write success");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final ReadWriteLockDemo demo = new ReadWriteLockDemo();
        Runnable readRunnable = new Runnable() {
            public void run() {
                try {
                    demo.handleRead(readLock);
//                    demo.handleRead(lock);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Runnable writeRunnable = new Runnable() {

            public void run() {
                try {
                    demo.handleWrite(writeLock, new Random().nextInt());
//                    demo.handleWrite(lock, new Random().nextInt());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        for (int i = 0; i < 18; i++) {
            new Thread(readRunnable).start();
        }

        for (int i = 18; i < 20; i++) {
            new Thread(writeRunnable).start();
        }
    }
}

​ 上述代码中,比较了使用读写锁和普通锁时,系统完成读写任务所需要的时间,这里设置读任务要比写任务多得多

​ 从执行结果,可以看到,不用读写锁,程序花费了20秒的时间才完成读写任务。采用读写锁,程序需要3秒就完成读写任务了。

5 倒计数器:CoundownLatch

​ CountDownlatch是一个非常实用的多线程控制工具类,这里简单称之为倒数计数器,这个工具通常用来控制线程等待,他可以让某一个线程等待直到计数结束,在开始执行

​ 一种典型的场景就是火箭发射,在火箭发射前,为了保证万无一失,往往做多想检查,引擎才能点火执行,这个场景非常适合CountDownLatch,他可以是点火线程在等待所有检查线程全部完工后在执行.

​ CountDownLatch的构造函数接收一个整数作为参数,即当前这个计数器的计数个数。

    public CountDownLatch(int count)

​ 下面示例演示CountDownLatch使用的方法

public class CountDownLatchDemo implements Runnable {
    static final CountDownLatch end = new CountDownLatch(10);
    static final CountDownLatchDemo demo = new CountDownLatchDemo();


    public void run() {
        try {
            Thread.sleep(new Random().nextInt(10) * 1000);
            System.out.println("check complete!");
            end.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            exec.submit(demo);
        }
        end.await();
        System.out.println("Fire!");
        exec.shutdown();
    }
}

​ 上述代码第2行生成一个CountDownLatch实例,计数数量为10,这表示需要10个线程完成任务后等待在CountDownLatch上的线程才能继续执行,代码第10行使用了CountDownLatch.countDown()方法,也就是通知CountDownLatch,一个线程已经完成了任务,到计数器减1。第21行使用CountDownLatch.await()方法,要求主线程等待所有检查任务全部完成,待10个任务全部完成后,主线程才能继续执行。

上述案例执行逻辑如下图简单表示

主线程在CountDownLatch上等待,当所有的检查线程任务全部完成后,主线程方能继续执行

6 线程阻塞工具类:LockSupport

​ 首先看下suspend()方法卡死线程的例子

public class BadSuspend {
    public static Object u = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");
    static ChangeObjectThread t2 = new ChangeObjectThread("t2");

    public static class ChangeObjectThread extends Thread{
        public ChangeObjectThread(String name) {
            super.setName(name);
        }


        public void run() {
            synchronized (u){
                System.out.println("in "+ getName());
                Thread.currentThread().suspend();
            }
        }

    }

    // 导致resume不生效的执行顺序可能是这样的:
    // 打印t1 => t1在suspend => t2等待u释放 => t1被resume => t2被resume => u释放打印t2 => t2被suspend => 永远无法结束
    public static void main(String []args) throws InterruptedException {
        t1.start();
        Thread.sleep(100);
        t2.start();
        t1.resume();
        t2.resume();
        t1.join();
        t2.join();
    }
}

上述案例执行示意图

​ 主函数调用了resume()方法,但是由于时间先后顺序的缘,那个resume并没有生效!这就导致了线程t2永远被挂起,并且永远占用了对象u的锁,这对于系统来来说可能是致命的。

​ LockSupport是一个非常方便的使用的线程阻塞工具,他可以在线程内任意位置让线程阻塞。与Thread.suspend()方法相比,她弥补了由于resume方法发生导致线程无法继续执行的情况。和Object.wait()方法相比,他不需要获得某个对象的锁,也不会抛出InterruptedExeption异常。

​ 现在使用LockSupport重写这个程序

public class LockSupportDemo {

    public static Object u = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");
    static ChangeObjectThread t2 = new ChangeObjectThread("t2");

    public static class ChangeObjectThread extends Thread {
        public ChangeObjectThread(String name) {
            super.setName(name);
        }


        public void run() {
            synchronized (u) {
                System.out.println("in " + getName());
                LockSupport.park();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(100);
        t2.start();
        LockSupport.unpark(t1);
        LockSupport.unpark(t2);
        t1.join();
        t2.join();
    }
}

​ 这个案例可以正常结束,不会因为pack()方法导致线程永久挂起,这是因为LockSupport类使用类似信号量的机制,他为为每一个线程准备一个了一个许可,如果许可可用那么pack()方法立即返回,并且消费这个许可(也就是把许可变为不可用),如果许可不可用,就会阻塞,而unpack方法则使得一个许可可用,这个特点使得即使unpack发生在pack方法之前,他也可以是下一次的pack()方法立即执行返回。

​ LockSupport.pack()方法还能支持中断影响,但是和其他接受中断函数不一样,LockSupport.pack()方法不会抛出InterruptedException异常。他只会默默返回,但是我们可以从Thread.interrupted()等方法中获得中断标记

public class LockSupportIntDemo {

    public static Object u = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");
    static ChangeObjectThread t2 = new ChangeObjectThread("t2");

    public static class ChangeObjectThread extends Thread {
        public ChangeObjectThread(String name) {
            super.setName(name);
        }


        public void run() {
            synchronized (u) {
                System.out.println("in " + getName());
                LockSupport.park();
                if (Thread.interrupted()) {
                    System.out.println(getName() + "被中断了");
                }
            }
            System.out.println(getName() + "执行结束");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(100);
        t2.start();
        t1.interrupt();
        LockSupport.unpark(t2);
    }
}

​ 上述代码29行中断了出于pack()方法状态的t1,之后,t1可以马上响应这个中断,并且返回,t1返回后外面等待的t2才可以进入临界区,并最终由LockSupport.unpack(t2)操作使其运行结束。