多线程之join() 小白级别理解

346 阅读3分钟

一、简单介绍

以下对join进行简单的介绍,相关示例的代码就不多加引用了。

大多情况下,主线程创建并启动子线程,如果子线程进行大量的耗时计算,则主线程往往早于子线程结束。这时如果主线程想等待子线程执行完成后再结束,就要用到join的方法。join作用是等待线程对象销毁。

在当前线程使用join方法时,若该线程被中断,则当前线程会出现异常

join与synchronized的区别:join再内部使用wait等待,synchronized使用的是对象监视器原理做同步。

join与sleep的区别

Thread.sleep(long) 方法的作用是让当前执行的线程休眠据,且该线程不丢失任何监视器的所属权,即 sleep 方法并不释放锁,但是会让出 CPU 资源

而join方法是会释放锁的

二、join代码提前运行问题

public class TestThread {
    public static void main(String[] args) {
        try {
            ThreadB b = new ThreadB();
            ThreadA a = new ThreadA(b);
            a.start();
            b.start();
            b.join(2000);
            System.out.println("main end " + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
class RunFirst {
    public static void main(String[] args) {
        ThreadB b = new ThreadB();
        ThreadA a = new ThreadA(b);
        a.start();
        b.start();
        System.out.println("main end = " + System.currentTimeMillis());
    }
}
class ThreadA extends Thread {
    private ThreadB b;

    public ThreadA(ThreadB b) {
        this.b = b;
    }

    @Override
    public void run() {
        super.run();
        try {
            synchronized (b) {
                System.out.println("begin A thread name = " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
                Thread.sleep(5000);
                System.out.println("  end A thread name = " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
class ThreadB extends Thread {
    @Override
    synchronized public void run() {
        super.run();
        try {
            System.out.println("begin B thread name = " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("  end B thread name = " + Thread.currentThread().getName() + " " + System.currentTimeMillis());

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

分析完代码,我以为运行TestThread的主方法时,控制台打印的顺序应该是,先执行线程A的run方法,然后执行B方法,最后再执行主方法的打印。

而多次运行结果是

begin A thread name = Thread-1 1587651053517
  end A thread name = Thread-1 1587651058517
main end 1587651058517
begin B thread name = Thread-0 1587651058517
  end B thread name = Thread-0 1587651063517

接着多次执行RunFirst的主方法,结果为

main end = 1587651105291
begin A thread name = Thread-1 1587651105291
  end A thread name = Thread-1 1587651110291
begin B thread name = Thread-0 1587651110291
  end B thread name = Thread-0 1587651115292

主方法中的打印结果,往往都是先执行的。书本便给出了结论:方法join大部分是先运行的,即先抢到ThreadB的锁,然后快速进行释放。(说不出什么道理,但好像是这么一回事??)

这时再看join的源码。

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

结合书本对TestThread主方法第二轮分析:

1、b.join先抢到了B锁,此时join中的while(isAlive)判断的是B线程是否允许,isAlive的结果为true,运行到了wait(delay)。这时释放B锁。

2、ThreadA抢到了锁,并打印,再次释放B锁。

3、这时join和threadB争抢锁,而join再次抢到锁,运行

now = System.currentTimeMillis() - base;

long delay = millis - now; if (delay <= 0) { break; }由于delay<=0故停止循环。

4、ThreadB抢到锁,打印结束。

那么如何才能让结果顺着我的第一个思路进行下去呢?

结合源码,这里假设t1为第一次运行到join的时间,则t1=base,假设t2为第二次运行到join的时间,则

t2=now(now = System.currentTimeMillis() - t1;),

要想先执行ThreadB中的内容,则需要delay的值大于0.故dealy=miles-now=miles-System.currentTimeMillis() + t1

可以通过改变ThreadA运行的时间改变System.currentTimeMillis()的值,也可以增大miles的值。

之后将ThreadA run方法中的sleep改为1000,结果如料想的一样。

以上均是个人理解,如果有误,或有描述不贴切的地方,希望能有大神指出。

参考资料:《Java多线程编程核心技术》