三、多线程通讯

1.生产者-消费者模型

1.1 示例

示例:一个线程(input)写入用户,一个线程(out)读取用户,实现读一个,写一个操作

基本实现:

  • 共享资源实体类
class Res {
    sex:String
    userName:String
}
  • 输入线程
class IntThread extends Thread {
    private Res res;
    public IntThread(Res res){
        this.res = res;
    }
    @override run():{
        int count = 0;
        while(true){
            if(count == 0){
                res.userName = "小军";
                res.sex = "男";
            }else{
                res.userName = "小红";
                res.sex = "女"}
            count = (count + 1) % 2;
        }
    }
}
  • 输出线程
class OutThread extends Thread {
    private Res res;
    public OutThread(Res res){
        this.res = res;
    }
    @override run():{
        while(true){
            sysout(res.userName + "--" + res.sex);
        }
    }
}
  • 运行代码:
Res res = new Res();
IntThread intThread = new IntThread();
OutThread outThread = new OutThread();
intThread.start();
outThread.start();
  • 结果:
小红--女
小红--男
小军--女
小军--男

// 共享资源的线程安全问题
  • 解决线程安全问题:
class IntThread extends Thread {
    private Res res;
    public IntThread(Res res){
        this.res = res;
    }
    @override run():{
        int count = 0;
        while(true){
            synchronized(res){
                if(count == 0){
                    res.userName = "小军";
                    res.sex = "男";
                }else{
                    res.userName = "小红";
                    res.sex = "女"}
                count = (count + 1) % 2;
            }
        }
    }
}
class OutThread extends Thread {
    private Res res;
    public OutThread(Res res){
        this.res = res;
    }
    @override run():{
        while(true){
            synchronized(res){
                sysout(res.userName + "--" + res.sex);
            }
        }
    }
}    

解决了线程安全问题,但是并没有做到先写后读,写一个读一个的要求,while是一直读,损耗性能。

使用wait和notify方法

  • 共享资源实体类
class Res {
    sex:String
    userName:String
    flag:boolean //true 可以读,写等待。false 不可以读,可以写
}
  • 输入线程
class IntThread extends Thread {
    private Res res;
    public IntThread(Res res){
        this.res = res;
    }
    @override run():{
        int count = 0;
        while(true){
            synchronized(res){
                if(res.flag){
                    try:res.wait();
                }
                if(count == 0){
                    res.userName = "小军";
                    res.sex = "男";
                }else{
                    res.userName = "小红";
                    res.sex = "女"}
                count = (count + 1) % 2;
                res.flag = true;
                res.notify();
            }
        }
    }
}
  • 输出线程
class OutThread extends Thread {
    private Res res;
    public OutThread(Res res){
        this.res = res;
    }
    @override run():{
        while(true){
            synchronized(res){
                if(!res.flag){
                    try:res.wait();
                }
                sysout(res.userName + "--" + res.sex);
                res.flag = false;
                res.notify();
            }
        }
    }
}    

1.2 wait、notify、notifyAll方法

总则:调用这些方法的线程必须持有监视器锁

  • 1.wait方法会使当前线程进入自己所持有的监视器锁(this object)的等待队列,并且放弃一切已经拥有的(这个监视器锁上的)同步资源,然后挂起当前线程,直到以下四个条件之一发生:

      1)其他线程调用了this object的notify方法,
       并且当前线程恰好是被选中唤醒的那一个
      2)其他线程调用了this object的notifyAll方法
      3)其他线程中断了当前线程
      4)指定的超时时间到了(如果超时时间为0,则该线程会一直等待,直到收到通知)
    
  • 2.当以上4个条件之一满足后,该线程从wait set中移除,重新参与到线程调度中,并且和其他线程一样,竞争锁资源,一旦它又获得了监视器锁,则它在调用wait方法时所有状态都会被恢复。

  • 3.当前线程在进入wait set之前或者在wait set之中时,如果被其他线程中断了,则会抛出Interrupted Exception异常,但是,如果在恢复现场的过程中被中断了,则直到现场恢复完成后才会抛出Interrupted Exception

  • 4.即使wait方法把当前线程放入this object的wait set里,也只会释放当前监视器锁(this object),如果当前线程还持有了其他同步资源,则即使它在this object的等待队列中,也不会释放。

  • 5.notify()方法会在所有等待监视器锁的线程中任意选一个唤醒,具体唤醒哪一个,交由方法的实现者自己决定。

  • 6.线程调用notify方法不会立即释放监视器锁,只有退出同步代码块后,才会释放锁。(与之相对,调用wait方法会立即释放监视器锁)

  • 7.线程被notify或notifyAll唤醒后会继续和其他普通线程一样竞争所资源。

2.wait、notify常见问题

2.1 丢失的信号

notify()和notifyAll()方法不会保存调用它们的方法,因为当这两个方法被调用时,有可能没有线程处于等待状态,通知信号过后便丢弃了。因此,如果一个线程先于被通知线程调用wait()前调用了notify(),等待的线程将错过这个信号。

这可能不是个问题,不过,在某些情况下,这可能使等待线程永远在等待,不再醒来,因为线程错过了唤醒信号。

为了避免丢失信号,必须把它们保存在信号类里。在MyWaitNotify例子中,通知信号被存储在MyWaitNotify实例的一个成员变量里。

public class MyWaitNotify2{
    MonitorObject myMonitorObject = new MonitorObject();

    boolean wasSignalled = false;

    public void doWait(){
        synchronized(myMonitorObject){
            if(!wasSignalled){
                try:myMonitorObject.wait();
            }
            wasSignalled = false;
        }
    }

    public void doNotify(){
        synchronized(myMonitorObject){
            wasSignalled = true;
            myMonitorObject.notify();
        }
    }
}

(标注)为了避免信号丢失,用一个变量来保存是否被通知过,在notify前,设置自己已经被通知过。在wait后,设置自己没有被通知过,需要等待通知。

2.2 假唤醒

由于莫名其妙的原因,线程有可能在没有调用过notify()和notifyAll()的情况下醒来,这就是所谓的假唤醒。
如果在MyWaitNotify2的doWait()方法里发生了假唤醒,等待线程即使没有收到正确的信号,也能够执行后续的操作,这可能导致你的应用程序出现严重问题。

为了防止假唤醒,保存信号的成员变量将在一个while循环里接受检查,而不是在if表达式里。这样的一个while循环叫做自旋锁(校注:这种做法要慎重,目前的JVM实现自旋会消耗CPU,如果长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大)

public class MyWaitNotify3{
    MonitorObject myMonitorObject = new MonitorObject();

    boolean wasSignalled = false;

    public void doWait(){
        synchronized(myMonitorObject){
            while(!wasSignalled){
                try:myMonitorObject.wait();
            }
            wasSignalled = false;
        }
    }

    public void doNotify(){
        synchronized(myMonitorObject){
            wasSignalled = true;
            myMonitorObject.notify();
        }
    }
}

如果等待线程没有收到信号就唤醒,wasSignalled变量为false,while循环会在执行一次,促使醒来的线程回到等待状态。

2.3 while循环其他场景:多个线程等待相同信号

如果你有多个线程在等待,被notifyAll()唤醒,但只有一个被允许继续执行,使用while循环也是一个好方法,每次只有一个线程可以获得监视器锁,意味着只有一个线程可以退出wait()调用并清除wasSignalled标志(设为false)。一旦这个线程退出doWait()的同步块,其他线程退出wait()调用,并在while循环里检查wasSignalled变量值。但是,这个标志已经被第一个唤醒的线程清除了,所以其余醒来的线程将回到等待状态,直到下次信号到来。

2.4 不要在字符串常量或全局对象中调用wait()

示例:

public class MyWaitNotify{
    String myMonitorObject = "";

    boolean wasSignalled = false;

    public void doWait(){
        synchronized(myMonitorObject){
            while(!wasSignalled){
                try:myMonitorObject.wait();
            }
            wasSignalled = false;
        }
    }

    public void doNotify(){
        synchronized(myMonitorObject){
            wasSignalled = true;
            myMonitorObject.notify();
        }
    }
}

在空字符串作为锁的同步块(或者其他常量字符串)里调用wait()或者notify()产生的问题是:

JVM编译器内部会把常量字符串转换成同一个对象。这意味着,即使你有2个不同的MyWaitNotify实例,也存在这样的风险:

在第一个MyWaitNotify实例上调用doWait()的线程会被在第二个MyWaitNotify上调用doNotify()的线程唤醒。——假唤醒 ——信号丢失
所以,在wait()、notify机制中,不要使用全局对象,字符串常量等。应该是用唯一的对象。

2.5 wait、sleep、yeild方法的区别

参考文章:https://www.jianshu.com/p/25e959037eed

3 Lock锁

3.1 synchronized与Lock对比

如果一个代码块被synchronized关键字修饰,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待直至占有锁的线程释放锁。
事实上,占有锁的线程释放锁一般会是以下三种情况之一:

  • 1.占有锁的线程执行完了该代码块,然后释放对锁的占有。
  • 2.占有锁的线程执行发生异常,此时JVM会让线程自动释放锁
  • 3.占有锁线程进入WAITING从而释放锁,例如在该线程中调用wait()方法等。

synchronized 是Java语言的内置特性,可以轻松实现对临界资源的同步互斥访问,那么,为什么还会出现Lock?

case1:
在使用synchronized关键字的情况下,假如占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待。
因此,就需要有一种机制可以不让等待的线程无限期地等待下去,比如只等待一定的时间(解决方案:tryLock(long time, TimeUnit unit)或者能够响应中断的方案(解决方案:lockInterruptibly()),这种情况可以通过lock来解决。

case2:
当多个线程读写文件时,读和写会发生冲突,写和写也会发生冲突,但是读和读不会产生冲突现象。如果采用 synchronized 关键字,当多个线程只是进行读操作时,也只有一个线程可以进行读操作。
因此,需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突现象。同样地,Lock可以解决这种情况(ReentrantReadWriteLock)

case3:
我们可以通过Lock得知线程有没有成功获取到锁,这个synchronized 无法办到。

上面提到的三种情形,我们都可以通过Lock来解决,但是synchronized却无能为力。

Lock实现提供了比 synchronized 关键字更灵活、更广泛、粒度更细的操作,它能以更优雅的方式处理线程同步问题。

注意以下几点:

  • 1.synchronized是Java关键字,因此是Java的内置特性,基于JVM层实现,(monitorEnter、monitorExit);
    而Lock是一个Java接口,是基于JDK层面实现的。

  • 2.采用 synchronized 方式不需要用户手动释放锁,而Lock则必须要用户手动释放锁,(即使发生异常,也不会自动释放锁),如果没有主动释放锁,就有可能导致死锁现象。

lock

3.1 lock():

// 用来获取锁,如果锁已被其他线程获取,则进行等待
// 采用Lock必须主动释放锁,并且在发生异常时,
// 不会自动释放锁
Lock lock = ...;
lock.lock();
try:处理任务
catch:
finally:lock.unlock();

3.2 tryLock() & tryLock(long time, TimeUnit unit)

tryLock()是有返回值的,它表示用来尝试获取锁,获取成功,返回true;获取失败(即锁已被其他线程获取),则返回false,也就是说,这个方法无论如何都会立即返回,在拿不到锁时不会一直在那等待。

tryLock(long time, TimeUnit unit)方法和tryLock方法是类似的,区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断,如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

Lock lock = ...;
if(lock.tryLock()){
    try:处理任务
    catch:
    finally:lock.unlock
}else{
    // 如果不能获取到锁,则直接做其他事情
}

3.3 lockInterruptibly()

lockInterruptibly()方法比较特殊,当通过这个方法获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。
例如,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若线程A获取到了锁,而线程B只有在等待,那么在对线程B调用threadB.interrupt方法能够中断B的等待过程

public void method throws InterruptedException {
    lock.lockInterruptibly();
    try:
    catch:
    finally:lock.unlock();
}

注意,当一个线程获取到了锁之后,是不会被interrupt()方法中断的,因为interrupt()方法只能中断阻塞过程中的等待线程而不能中断正在运行过程中的线程。

4 ReentrantLock

可重入锁,唯一实现了Lock接口的类

4.1 Lock的正确使用

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();

    public void insert(Thread thread){
        // 注意这里lock为局部变量
        Lock lock = new ReentrantLock();
        lock.lock();
        try:sysout("线程" + thread.getName() + "得到了锁");
            for(int i = 0; i < 5; i++){
                arrayList.add(i);
            }
        catch:
        finally:{
            sysout("线程" + thread.getName() + "释放了锁");
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final Test test = new Test();

        new Thread("A"){
            public void run(){
                test.insert(Thread.currentThread());
            }
        }.start();

        new Thread("B"){
            public void run(){
                test.insert(Thread.currentThread());
            }
        }.start();
    }
}

结果:
线程A得到了锁…
线程B得到了锁…
线程A释放了锁…
线程B释放了锁…

第二个线程怎么会在第一个线程释放锁之前得到了锁?原因在于,在insert方法中的lock是局部变量,每个线程执行到lock.lock()获取的是不同的锁,所以就不会对临界资源形成同步互斥访问。

因此,我们只需要将lock声明为成员变量即可。

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();

    private Lock lock = new ReentrantLock();

    public void insert(Thread thread){
        // 注意这里lock为局部变量
        
        lock.lock();
        try:sysout("线程" + thread.getName() + "得到了锁");
            for(int i = 0; i < 5; i++){
                arrayList.add(i);
            }
        catch:
        finally:{
            sysout("线程" + thread.getName() + "释放了锁");
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final Test test = new Test();

        new Thread("A"){
            public void run(){
                test.insert(Thread.currentThread());
            }
        }.start();

        new Thread("B"){
            public void run(){
                test.insert(Thread.currentThread());
            }
        }.start();
    }
}

4.2 tryLock() & tryLock(long time, TimeUnit unit)示例

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock(); // 注意这个地方:lock 被声明为成员变量

    public static void main(String[] args) {
        final Test test = new Test();

        new Thread("A") {
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();

        new Thread("B") {
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }

    public void insert(Thread thread) {
        if (lock.tryLock()) {     // 使用 tryLock()
            try {
                System.out.println("线程" + thread.getName() + "得到了锁...");
                for (int i = 0; i < 5; i++) {
                    arrayList.add(i);
                }
            } catch (Exception e) {

            } finally {
                System.out.println("线程" + thread.getName() + "释放了锁...");
                lock.unlock();
            }
        } else {
            System.out.println("线程" + thread.getName() + "获取锁失败...");
        }
    }
}/* Output: 
        线程A得到了锁...
        线程B获取锁失败...
        线程A释放了锁...
 *///:

4.3 使用 lockInterruptibly() 响应中断

public class Test {
    private Lock lock = new ReentrantLock();   
    public static void main(String[] args)  {
        Test test = new Test();
        MyThread thread1 = new MyThread(test,"A");
        MyThread thread2 = new MyThread(test,"B");
        thread1.start();
        thread2.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    }  

    public void insert(Thread thread) throws InterruptedException{
        //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将 InterruptedException 抛出
        lock.lockInterruptibly(); 
        try {  
            System.out.println("线程 " + thread.getName()+"得到了锁...");
            long startTime = System.currentTimeMillis();
            for(    ;     ;) {              // 耗时操作
                if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
                    break;
                //插入数据
            }
        }finally {
            System.out.println(Thread.currentThread().getName()+"执行finally...");
            lock.unlock();
            System.out.println("线程 " + thread.getName()+"释放了锁");
        } 
        System.out.println("over");
    }
}

class MyThread extends Thread {
    private Test test = null;

    public MyThread(Test test,String name) {
        super(name);
        this.test = test;
    }

    @Override
    public void run() {
        try {
            test.insert(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println("线程 " + Thread.currentThread().getName() + "被中断...");
        }
    }
}/* Output: 
        线程 A得到了锁...
        线程 B被中断...
 *///:~

4.4 ReentrantReadWriteLock示例

public class Test {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        final Test test = new Test();

        new Thread("A") {
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();

        new Thread("B") {
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
    }

    public void get(Thread thread) {
        rwl.readLock().lock(); // 在外面获取锁
        try {
            long start = System.currentTimeMillis();
            System.out.println("线程" + thread.getName() + "开始读操作...");
            while (System.currentTimeMillis() - start <= 1) {
                System.out.println("线程" + thread.getName() + "正在进行读操作...");
            }
            System.out.println("线程" + thread.getName() + "读操作完毕...");
        } finally {
            rwl.readLock().unlock();
        }
    }
}/* Output: 
        线程A开始读操作...
        线程B开始读操作...
        线程A正在进行读操作...
        线程A正在进行读操作...
        线程B正在进行读操作...
        ...
        线程A读操作完毕...
        线程B读操作完毕...
 *///:~

5 Condition

5.1 基本示例

synchronized 与 wait()和notify()/notifyAll方法相结合可以实现等待/通知模型,ReentrantLock同样可以,但是需要借助Condition,且Condition有更好的灵活性

  • 1.1个Lock里可以创建多个Condition实例,实现多路通知
  • 2.notify()方法进行通知时,被通知的线程是随机的,但是ReentrantLock结合Condition可以实现有选择性的通知。

基本示例:

public class ConditionDemo{
    final ReentrantLock lock = new ReentrantLock();
    final Condition condition = lock.newCondition();

    new Thread(() -> {
        @override 
        public void run(){
            lock.lock();
            try{
                sysout(Thread.currentThread().getName() + "在等待被唤醒");
                condition.await();
                sysout(Thread.currentThread().getName() + "恢复执行");
            }
            catch:
            finally:lock.unlock();
        } 
    }).start();

    new Thread(() -> {
        @override 
        public void run(){
            lock.lock();
            try{
                sysout(Thread.currentThread().getName() + "抢到了锁");
                condition.signal();
                sysout(Thread.currentThread().getName() + "唤醒其他线程");
            }
            catch:
            finally:lock.unlock();
        }
    }).start();
}

5.2 常用方法

5.2.1 await()

调用await()方法后,当前线程在接收到唤醒信号之前或被中断之前一直处于休眠状态。

5.2.2 await(longTime, TimeUnit unit)

调用此方法后,会造成当前线程在接收到唤醒信号之前、被中断之前或到达指定等待时间之前一直处于等待状态。

5.2.3 awaitUninterruptibly()

调用此方法后,会造成当前线程在接收到唤醒信号之前一直处于等待状态,如果在进入此方法时设置了当前线程的中断状态,或者在等待时,线程被中断,那么在接收到唤醒信号之前,它将继续等待。

5.2.4 awaitUntil(Date dealine)

调用此方法后,会造成当前线程在接收到唤醒信号之前,被中断之前,或到达最后期限之前一直处于等待休眠状态。

5.2.5 signal()

唤醒一个等待线程,如果所有的线程都在等待此条件,则选中其中一个唤醒。

5.2.6 signalAll()

唤醒所有等待线程,如果所有的线程都在等待此条件,则唤醒所有线程。

6 面试题

编写两个线程,1个线程打印1-25,另一个线程打印字母A-Z,打印顺序为12A34B56C…5152Z,要求使用线程间通信。

通用代码

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public enum Helper {
 
    instance;
 
    private static final ExecutorService tPool = Executors.newFixedThreadPool(2);
 
    public static String[] buildNoArr(int max) {
        String[] noArr = new String[max];
        for(int i=0;i<max;i++){
            noArr[i] = Integer.toString(i+1);
        }
        return noArr;
    }
 
    public static String[] buildCharArr(int max) {
        String[] charArr = new String[max];
        int tmp = 65;
        for(int i=0;i<max;i++){
            charArr[i] = String.valueOf((char)(tmp+i));
        }
        return charArr;
    }
 
    public static void print(String... input){
        if(input==null)
            return;
        for(String each:input){
            System.out.print(each);
        }
    }
 
    public void run(Runnable r){
        tPool.submit(r);
    }
 
    public void shutdown(){
        tPool.shutdown();
    }
}

6.1 利用最基本的synchronized、notify、wait

public class MethodOne {
    private final ThreadToGo threadToGo = new ThreadToGo();
    public Runnable newThreadOne() {
        final String[] inputArr = Helper.buildNoArr(52);
        return new Runnable() {
            private String[] arr = inputArr;
            public void run() {
                try {
                    for (int i = 0; i < arr.length; i=i+2) {
                        synchronized (threadToGo) {
                            while (threadToGo.value == 2)
                                threadToGo.wait();
                            Helper.print(arr[i], arr[i + 1]);
                            threadToGo.value = 2;
                            threadToGo.notify();
                        }
                    }
                } catch (InterruptedException e) {
                    System.out.println("Oops...");
                }
            }
        };
    }
    public Runnable newThreadTwo() {
        final String[] inputArr = Helper.buildCharArr(26);
        return new Runnable() {
            private String[] arr = inputArr;
            public void run() {
                try {
                    for (int i = 0; i < arr.length; i++) {
                        synchronized (threadToGo) {
                            while (threadToGo.value == 1)
                                threadToGo.wait();
                            Helper.print(arr[i]);
                            threadToGo.value = 1;
                            threadToGo.notify();
                        }
                    }
                } catch (InterruptedException e) {
                    System.out.println("Oops...");
                }
            }
        };
    }
    class ThreadToGo {
        int value = 1;
    }
    public static void main(String args[]) throws InterruptedException {
        MethodOne one = new MethodOne();
        Helper.instance.run(one.newThreadOne());
        Helper.instance.run(one.newThreadTwo());
        Helper.instance.shutdown();
    }
}

6.2 利用Lock和Condition

public class MethodTwo {
    private Lock lock = new ReentrantLock(true);
    private Condition condition = lock.newCondition();
    private final ThreadToGo threadToGo = new ThreadToGo();
    public Runnable newThreadOne() {
        final String[] inputArr = Helper.buildNoArr(52);
        return new Runnable() {
            private String[] arr = inputArr;
            public void run() {
                for (int i = 0; i < arr.length; i=i+2) {
                    try {
                        lock.lock();
                        while(threadToGo.value == 2)
                            condition.await();
                        Helper.print(arr[i], arr[i + 1]);
                        threadToGo.value = 2;
                        condition.signal();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                    }
                }
            }
        };
    }
    public Runnable newThreadTwo() {
        final String[] inputArr = Helper.buildCharArr(26);
        return new Runnable() {
            private String[] arr = inputArr;
            public void run() {
                for (int i = 0; i < arr.length; i++) {
                    try {
                        lock.lock();
                        while(threadToGo.value == 1)
                            condition.await();
                        Helper.print(arr[i]);
                        threadToGo.value = 1;
                        condition.signal();
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                    }
                }
            }
        };
    }
    class ThreadToGo {
        int value = 1;
    }
    public static void main(String args[]) throws InterruptedException {
        MethodTwo two = new MethodTwo();
        Helper.instance.run(two.newThreadOne());
        Helper.instance.run(two.newThreadTwo());
        Helper.instance.shutdown();
    }
}

其余更多解法参考 https://blog.csdn.net/u011514810/article/details/77131296

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值