并发编程(八)java中的join方法深入理解

403 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情

这一章节是紧跟上一章节的,对于join方法同样有三个问题:1.是否释放锁?2.是否对中断敏感 ?3.是否释放CPU?

我们先来看一下源码

public final void join() throws InterruptedException {
    join(0);
}
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;
        }
    }
}

1.join方法是否对中断敏感

这次我们为了方便理解,先从是否对中断敏感,可以看到这个方法抛出InterruptedException异常,说明是对中断敏感的,也就是说你可以在现场join过程中去打断它。

2是否释放cpu

public final synchronized void join(long millis)
throws InterruptedException {
...
}

我们先从源码进行解析,首先看到这个这个方法体是synchronized关键字修饰的,而且锁对象就是谁调用谁就是锁对象。说明为了防止并发调用的情况。

long base = System.currentTimeMillis();
long now = 0;

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

这几行比较简单,就是记录当前时间戳和设置一个记录经过多久时间的差值now(后面会讲解now)

if (millis == 0) {
    while (isAlive()) {
        wait(0);
    }
} 

如果当前传进来的形参millis是0则进入if方法,我们的join方法默认调用的就是wait(0),接下来看while判断,isAlive()方法表示当前线程是否存活,如果调用者的线程不存活了(线程执行完毕或者未开启),则直接跳出while循环;如果存活就继续等待,这里的wait(0)

这里面有两个隐藏的知识点:

我们先简单写一个线程的例子:

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("线程执行");
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("线程结束");
        }
    });
    t1.start();
    t1.join();
    System.out.println("主线程执行完毕");
}

1.join方法是线程对象才能调用的这个方法。源代码中这个wait是调用的Object的,但是这是父类。其实这个wait方法前边有一个隐含的意义: this.wait(),this指的就是例子中的t1,还记得上面说的在方法中加锁吗?锁对象就是这个t1,那么这句话的含义就是释放了t1这个锁对象,并且当前线程进入了等待中。

2.这里源代码中为什么用while判断呢,这里不得不说假唤醒(虚假唤醒),在一个线程没有被其他线程调用notify()、notifyAll()方法进行通知,或者被中断,或者等待超时,这个线程仍然可以从挂起状态变为可以运行状态( 也就是被唤醒),这就是所谓的虚假唤醒。 为的就是防止虚假唤醒。

从这里我们也可以得知,当前线程是释放cpu的。

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

这里的else中,首先判断是否存活,跟上面一样存活的线程往下执行。代码比较简单,大概意思就是算出我要等待的时间和和已经过了多久的时间差值,如果是负数,说明已经过去了预期的millis时间了,跳出循环。如果不是负数,则等待相应的差值时间后再继续计算已经过了多久,重复这几个步骤,完成预期时间等待。

3.是否释放锁

我们下面写一个简单的代码做例子:

public class JoinRelase {
    static Object object = new Object();
    public static void main(String[] args) throws InterruptedException {

        for(int i = 0; i < 2; i++) {
            Thread thread = new Thread(new SubThread(),"Daemon Thread!"+i);
            thread.setName("thread-" + i);
            thread.start();
            Thread.sleep(100);
        }
        System.out.println("主线程执行完毕");
    }
    static class SubThread implements Runnable {
        @SneakyThrows
        @Override
        public void run() {
            System.out.println("未获取到锁"+Thread.currentThread().getName());
            synchronized (object) {
                System.out.println("获取到锁!!!ThreadName: " + Thread.currentThread().getName());
                Thread.currentThread().join();
            }
        }
    }
}

执行效果如下:

image.png 程序为什么不结束呢? 可以看到thread-1线程被卡住了,因为1线程一直未获取到object锁对象,还记得我们刚刚看的源码,线程0执行join时源码wait方法释放的是什么锁呢,在这个例子中释放的是调用对象,也就是Thread.currentThread()对象,也就是名字是thread-0 这个对象!!!并不释放object这个锁对象!

同理我们如果把加锁synchronized (object) 换成了线程一释放的锁 ,那么我们执行效果是什么样子呢? 可以先想一下,示例代码如下:

public class JoinRelase {
    static Object object = new Object();
    static Thread tempThread;
    public static void main(String[] args) throws InterruptedException {

        for(int i = 0; i < 2; i++) {
            Thread thread = new Thread(new SubThread(),"Daemon Thread!"+i);
            thread.setName("thread-" + i);
            thread.start();
            tempThread=thread;
            Thread.sleep(100);
        }
        System.out.println("主线程执行完毕");
    }
    static class SubThread implements Runnable {
        @SneakyThrows
        @Override
        public void run() {
            System.out.println("未获取到锁"+Thread.currentThread().getName());
            synchronized (tempThread) {
                System.out.println("获取到锁!!!ThreadName: " + Thread.currentThread().getName());
                Thread.currentThread().join();
            }
        }
    }
}

效果如下:

image.png 线程0和线程1都获取到了锁,因为线程0 wait方法释放的就是Thread.currentThread()锁。具体要看当前的锁对象是谁。如果是调用join方法的锁对象,则释放。

所以join方法是否释放锁,可以说是释放调用对象这个对象锁!