5、Java 线程类常用方法

概述

线程在运行过程中可以通过调用方法来修改状态和属性。本篇我们主要介绍 Java 线程常用的方法


Java 线程常用方法

我打算从以下三个模块出发,依次介绍常用的线程方法:

  1. 实例方法
  2. 静态方法
  3. Object 继承方法

1、实例方法

Java 线程类常用的实例方法有以下这些:

  • start()
  • run()
  • interrupt()
  • isInterrupted()
  • isAlive()
  • join()
  • isDaemon()
  • getState()

1-1、start()

public synchronized void start()

通过调用该方法,线程对象被启动,jvm 虚拟机将调用该对象的 run() 方法。其中调用后,我们会得到两个同时运行的线程,一个是调用 start() 方法的线程,另一就是我们新启动的线程。

该方法最终会调用 start0() 方法,start0() 是一个 native 方法。

start() 方法被 synchronized 修饰,也就是说同时只能有一个线程执行当前对象的 start() 方法。通过该关键字保证:一个线程只能启动一次,即使该线程已经执行完成。当我们再次调 start() 方法时,会抛出以下异常:

java.lang.IllegalThreadStateException

1-2、run()

public void run()

该方法是线程的执行体,也就是线程运行的内容。通过继承 Thread 类实现的线程需要重写该方法,通过 Runnable 接口实现的线程,会直接调用参数对象的 run()方法。

start() 方法启动线程后,执行的内容就是该 run() 方法。


1-3、interrupt()

public void interrupt()

调用该方法,尝试 停止某个线程。一般在实际应用中有以下两种情况:

  1. 停止阻塞的线程,会抛出异常
  2. 停止正在运行的线程,一般被调线程无响应

首先我们验证抛出异常能否中断线程,具体代码如下:

private class Worker implements Runnable {
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        while (true) {
            System.out.println(threadName + "正在运行");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
@Test
public void test() throws InterruptedException {
    Thread t = new Thread(new Worker());
    t.start();
    t.interrupt();
    t.join();
}

执行结果

Thread-0正在运行
java.lang.InterruptedException: sleep interrupted
Thread-0正在运行
Thread-0正在运行
...

从结果来看,抛出异常后,线程并没有停止,而是继续执行。也就是说:interrupt() 方法并不能停止阻塞线程。并且在测试过程中,我发现了一个奇怪的现象:线程阻塞时,如果调用 interrupt() 方法,线程抛出异常后会直接执行,从而跳过阻塞过程。具体我们看代码:

private class Worker implements Runnable {
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        while (true) {
            System.out.println(threadName + "正在运行,当前时间:" + System.currentTimeMillis());
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
@Test
public void test() throws InterruptedException {
    Thread t = new Thread(new Worker());
    t.start();
    while (true) {
        Thread.sleep(1000);
        t.interrupt();
    }
}

执行结果

Thread-0正在运行,当前时间:1591276900354
java.lang.InterruptedException: sleep interrupted
Thread-0正在运行,当前时间:1591276901353
java.lang.InterruptedException: sleep interrupted
Thread-0正在运行,当前时间:1591276902353
...

在上述代码中,Worker 线程每次循环睡眠 10秒,主线程每秒执行一次 interrupt() 方法。从运行结果来看,每秒都会打印信息。也就是说 interrupt() 方法会使被调线程跳出阻塞状态,直接向下执行(锁情况除外,线程没有获取到锁资源时,永远无法向下执行)。

为什么 interrupt() 方法会跳出阻塞呢?其实是因为每个线程都有一个 中断状态标识,调用 sleep() 方法会将该标识置位 true,表示线程处于阻塞状态。调用 interrupt() 方法后,该标识被重置为 false,而sleep() 方法中有循环判断该标识。当校验到该标识为 false 后抛出异常,直接向下执行。

既然 interrupt() 无法停止阻塞的线程,那我们看看它能否停止正在运行的线程。具体我们看代码:

@Test
public void test() throws InterruptedException {
    Thread t = new Thread() {
        @Override
        public void run() {
            while (true) {
                System.out.println("我还没停止");
            }
        }
    };
    t.start();
    t.interrupt();
    t.join();
}

执行结果

我还没停止
我还没停止
我还没停止
...

通过运行结果我们可以看出,该线程永远无法停止。其实原因是这样的:在JAVA语言中,无论阻塞线程还是运行线程,是否停止只能由自己来决定,interrupt() 只能修改线程的中断标识,提示它应该停止了

这种场景就类似:有个人在跑步,我过去告诉他:“你该停下了”。具体停不停由他自己决定。

然而线程本身是不带有思想的,因此我们可以通过编码,让线程定时检查自己是否停止了。检查线程是否停止的方法是 isInterrupted()。具体代码如下:

@Test
public void test() throws InterruptedException {
    Thread t = new Thread() {
        @Override
        public void run() {
            while (true) {
                if (this.isInterrupted()) {
                    System.out.println("我该停止了");
                    return;
                }
                System.out.println("我还没停止");
            }
        }
    };
    t.start();
    Thread.sleep(1);
    t.interrupt();
    t.join();
}

执行结果

我还没停止
我还没停止
...
我该停止了

从运行结果可以看出,最终该线程调用 isInterrupted() 方法判断已停止,通过 return 结束。


1-4、isInterrupted()

public boolean isInterrupted();

通过该方法判断线程是否停止,需要注意的一点是:它只根据中断标识判断,具体有没有停止并不一定,举个简单的例子:

public class IsInterruptedTest {
    class Worker implements Runnable {
        @Override
        public void run() {
            while (true) {
                System.out.println("我没停止");
            }
        }
    }
    @Test
    public void test() throws InterruptedException {
        Thread t = new Thread(new Worker());
        t.start();
        t.interrupt();
        System.out.println(t.isInterrupted());
    }
}

执行结果

true
我没停止
我没停止
...

从上面的结果我们可以看出,即使线程已经知道自己处于停止状态,也不会主动停止,必须通过 return 或执行完线程体代码来结束

实际应用中,在线程体中通过该方法判断来决定是否停止线程,也就是上面提到的自己中断自己,一般写法如下:

@Override
public void run() {
    while (true) {
        System.out.println("我没停止");
        if(Thread.currentThread().isInterrupted()){
        	System.out.println("我该停止了");
        	return;
        }
    }
}

最后需要注意的一点是:isInterrupted() 方法不会清除中断标识,也就是说如果一个线程的状态没有发生变化,连续调用该方法获取的结果永远是一致的,关于这点我们在静态方法 interrupted() 模块中详细说明。


1-5、isAlive()

public final native boolean isAlive();

通过该方法判断一个线程是否处于活跃状态,如果活跃返回 true,否则返回false。

什么样的线程处于活跃状态呢?关于这点我是这样理解的:启动并且没有停止的线程都属于活跃状态。

也就是说 sleep() 睡眠,抢锁失败阻塞的线程,都属于活跃状态。下面我们通过简单Demo验证:

public class IsAliveTest {

    class Worker implements Runnable {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + "尝试抢锁");
            synchronized (Worker.class) {
                System.out.println(threadName + "获取到锁,开始sleep");
                try {
                    Thread.sleep(10000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test() throws InterruptedException {
        Thread t1 = new Thread(new Worker());
        Thread t2 = new Thread(new Worker());
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println(t1.getName() + "的状态:" + t1.isAlive());
        System.out.println(t2.getName() + "的状态:" + t2.isAlive());
    }

}

执行结果

Thread-0尝试抢锁
Thread-0获取到锁,开始sleep
Thread-1尝试抢锁
Thread-0的状态:true
Thread-1的状态:true

在上述代码中,线程0获取到锁后执行sleep()方法阻塞。线程1抢锁失败阻塞。从结果来看两个线程都属于活跃状态。通俗点来说就是:如果一个线程还能执行(未启动线程除外),那么它就是活跃的


1-6、join()

public final void join() throws InterruptedException
public final synchronized void join(long millis)

通过该方法,让当前线程阻塞,直到被调用线程执行完毕或等待足够时间后才向下执行。因为存在阻塞的原因,该方法也会抛出 InterruptedException 异常。

上述方法1会调用方法2,默认参数为0。当参数为0时表示,直到被调用线程执行完毕后才向下执行。参数不为0时表示,最多等待XXX毫秒,等待足够的时间也会继续向下执行。

举个简答的例子:A和B去郊游,出发的时候B发现自己忘记带钱包。假设A和B关系特别好,此时A对B说,你先回去取吧,我会一直等你回来。此时就对应 join() 方法参数为0的情况,即一直等待你执行完毕。假设A和B关系一般,那么A对B说,你先回去取吧,我最多等你10分钟。此时就对应 join() 方法带参数且不为0的场景,等待足够的时间后继续向下执行。 下面我们具体看代码:

public class JoinTest {

    private class Worker implements Runnable {
        @Override
        public void run() {
            System.out.println("我执行了,接下来我要休息10S。当前时间:" + System.currentTimeMillis());
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我执行完了");
        }
    }

    @Test
    public void test() throws InterruptedException {
        Thread workerThread = new Thread(new Worker());
        System.out.println("启动worker线程时间:" + System.currentTimeMillis());
        workerThread.start();
        workerThread.join();
        System.out.println("执行完worker线程时间:" + System.currentTimeMillis());
    }

}

执行结果

启动worker线程时间:1591326308027
我执行了,接下来我要休息10S。当前时间:1591326308027
我执行完了
执行完worker线程时间:1591326318027

从结果来看,本地线程调用 worker 对象的 join() 方法后阻塞,直到 worker() 线程执行完毕才向下执行。

既然出现阻塞等待,就可能产生死锁。即 A 等待 B,B 等待 C,C 等待 A。当出现上述情况时,所有线程都无法向下执行,产生死锁。下面觉个最简单的例子:我等待我自己

public void run() {
    Thread t = Thread.currentThread();
    try {
        t.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("我执行完了");
}

执行结果:无

上述代码中,线程必须阻塞并等待自己执行完,然而阻塞永远无法执行,因此产生死锁。

join() 方法源码中通过调用 wait() 方法实现,在下面的 wait() 方法中我们重点讨论。


1-7、isDaemon()

public final boolean isDaemon()

Java 语言中有两种类型的线程:普通线程和守护线程。通过该方法判断一个线程是否守护线程

关于守护线程我是这样理解的:守护线程就是为其他普通线程提供服务的线程。只要还存在普通线程,所有守护线程都必须工作,只有当所有普通线程执行完毕后,JVM和守护线程才一起停止,因为此时已经没有需要服务的线程了。垃圾回收器 就是最典型的守护线程。

在本模块的测试案例开始之前,我首先强调非常重要的一点:isDaemon() 只能通过 main() 方法测试,不能通过 JUnit 方法测试

因为 JUnit 不支持多线程,不会等待其它线程执行完毕。JUnit 线程执行完毕后,所有线程自然也会停止,我们无法判断其它线程的停止原因。其它模块可以测试是因为我们通过 join() 或 CountDownLatch 保证主线程不结束,但是在这里控制 JUnit 线程不结束的话,就无法满足所有普通线程都停止的条件,更加无法验证。

下面我们看具体代码:

public class IsDaemonTest {

    private class Worker implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println(i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        IsDaemonTest isDaemonTest = new IsDaemonTest();
        Worker worker = isDaemonTest.new Worker();
        Thread t = new Thread(worker);
        t.start();
    }
}

执行结果

0
1
2
3
...

main() 方法本身也是一个普通线程。当 mian() 方法对应的线程执行完毕后,新创建的 worker 线程还在执行,终端每秒都会打印出最新 i 变量的值。下面我们尝试将 worker 线程修改为守护线程。需要注意的一点是,设置守护线程必须在线程启动前操作,正在运行的线程状态无法改变。修改后的新代码为:

public static void main(String[] args) {
    IsDaemonTest isDaemonTest = new IsDaemonTest();
    Worker worker = isDaemonTest.new Worker();
    Thread t = new Thread(worker);
    t.setDaemon(true);
    t.start();
}

执行结果

0
Process finished with exit code 0
或
Process finished with exit code 0

通过结果我们可以发现,当所有普通线程执行完毕后,守护线程也会随着自动停止。


1-8、getState()

public State getState()

通过该方法可以获取 Java 线程的状态。其中 State 是一个枚举类。下面我们依次介绍每种状态:

  • NEW:刚刚创建,还没有启动的线程。即 new 出来的 Thread 对象。

  • RUNNABLE:正在运行的线程,即使CPU没有调度,也视为 RUNNABLE 状态。

  • BLOCKED:线程竞争锁失败,阻塞等待锁时的状态

  • WAITING:没有最大等待时长的阻塞状态,等待其他线程唤醒。调用无参的 join() 、wait() 都会进入该状态

  • TIMED_WAITING:有最大等待时长的阻塞状态,如调用 thread.sleep(1000)、thread.join(1000)

  • TERMINATED:已经死亡(执行完毕)的线程

关于线程状态之间转移关系,后面我们通过其他博客专门整理。


2、静态方法

Java 线程类常用的静态方法有以下这些:

  • currentThread()
  • yield()
  • sleep()
  • interrupted()

2-1、currentThread()

public static native Thread currentThread();

该方法是 native 方法。通过它可以获取当前执行该段代码的线程。一般在代码中这样使用:

Thread t = Thread.currentThread();

该方法一般在实现 Runnable 接口类的 run() 方法中使用。因为实现 Runnable 接口的类本身不是线程类,线程体/run()方法 中无法直接获取当前线程,因此可以通过上述静态方法获取线程。而继承 Thread 的类本身就是线程类,可以在方法体中直接通过 this 关键字获取当前线程对象。


2-2、yield()

public static native void yield();

该方法是 native() 方法。通过调度该方法向调度程序发送信号,表示自己愿意放弃当前对处理器资源的使用。当然处理器也可以忽略该方法继续执行。下面我们通过一个简单的 Demo 了解yield()方法:

public class YieldTest{

    CountDownLatch countDownLatch = new CountDownLatch(2);

    private class WorkerA implements Runnable {
        @Override
        public void run() {
            String tName = Thread.currentThread().getName();
            for (int i = 1; i < 11; i++) {
                System.out.println(tName + ":执行第" + i + "次循环");
                if (i == 2) {
                    Thread.yield();
                }
            }
            countDownLatch.countDown();
        }
    }

    private class WorkerB implements Runnable {
        @Override
        public void run() {
            String tName = Thread.currentThread().getName();
            for (int i = 1; i < 11; i++) {
                System.out.println(tName + ":执行第" + i + "次循环");
            }
            countDownLatch.countDown();
        }
    }

    @Test
    public void test() {
        new Thread(new WorkerA()).start();
        new Thread(new WorkerB()).start();
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行结果:该方法运行结果有很多种,我列出其中两个足以说明问题即可。

Thread-0:执行第1次循环
Thread-0:执行第2次循环
Thread-1:执行第1次循环
Thread-1:执行第2次循环
...

Thread-0:执行第1次循环
Thread-0:执行第2次循环
Thread-0:执行第3次循环
Thread-0:执行第4次循环
...

在上述代码中,我们创建两个线程分别打印循环信息。线程A在第二轮循环时执行 yield() 方法,放弃CPU资源。此时需要注意的一点是:放弃之后,不代表线程A不竞争,而是说重新开始竞争。也就是说,线程A也有可能重新获得CPU资源。关于这点从运行结果我们也可以看出。

举个例子:张三赢得冠军之后觉得没意思,没有达到预期的效果,因此申请重新比赛。重新比赛意味着张三有可能再次夺冠,当然其他人也有可能夺冠,而不是说张三让出冠军给其他人竞争。

最后我们来谈谈 yield() 方法的作用:在实际应用中,假如系统现在提供两种服务,服务A特别重要,服务B相比一般。当服务A请求较多时,我们就可以在服务B的代码中循环执行 yield() 方法释放CPU资源,让比较重要的服务A先执行。但是一般情况我不建议使用,因为它会影响效率执行相同数量的任务,增加了CPU上下文切换的时间


2-3、sleep()

public static void sleep(long millis, int nanos) throws InterruptedException

public static native void sleep(long millis) throws InterruptedException;

上述方法1最终也会调用方法2,该方法也是native()方法。方法1中的参数分别表示毫秒和纳秒,也就是说方法1只是多了一个单位,让线程多睡眠一会。

通过 sleep() 方法使当前线程放弃CPU调度,睡眠(暂时停止执行)指定的毫秒数(线程睡眠不会精确到纳秒,上述方法1最终会四舍五入到毫秒)。其中sleep方法不会失去状态监视,当休眠够指定的时间后恢复运行状态等待CPU调度。下面我们通过简单 Demo 实践 sleep() 方法:

@Test
public void test() {
    System.out.println(System.currentTimeMillis());
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(System.currentTimeMillis());
}

执行结果

1591250232213
1591250235214

我们通过 System.currentTimeMillis() 获取系统时间,当线程在执行到 sleep() 方法后暂停,3000毫秒后继续向下执行。

在使用sleep()方法时,我们需要手动通过 try-catch 方法捕获 InterruptedException 异常。这是由于在 native 源码中,sleep() 方法中有判断线程状态标识,如果线程状态标识显示已中断:就抛出异常。下面我们通过简单 Demo 测试该场景:

@Test
    @Test
    public void test2() {
        Thread t = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("我被终止啦");
                }
            }
        };
        t.start();
        t.interrupt();
    }

执行结果

java.lang.InterruptedException: sleep interrupted
我被终止啦

上述案例中,在线程休眠期间我们调用它的 interrupt() 方法中断线程,系统抛出异常。这里需要注意的一点是,finally 代码块中代码仍然会被执行,在线程池源码我们经常会看到这种操作。

最后,我们来聊一下该方法重要特性:sleep() 方法不会放弃锁资源。 也就是说如果一个线程占有锁资源,在该线程调用 sleep() 方法阻塞后,它不会放弃锁资源,直到执行完并发代码块。下面我来看一个简单示例:

private class Worker implements Runnable {
    @Override
    public void run() {
        synchronized (Worker.class) {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + "获取到锁啦,获取到锁时间:" + System.currentTimeMillis());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadName + "睡眠结束");
        }
    }
}

@Test
public void test3() {
    new Thread(new Worker()).start();
    new Thread(new Worker()).start();
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

执行结果

Thread-0获取到锁啦,获取到锁时间:1591252519454
Thread-0睡眠结束
Thread-1获取到锁啦,获取到锁时间:1591252521455
Thread-1睡眠结束

通过结果我可以看出,只有在 Thread-0 线程睡眠结束后,Thread-1 才能获取到锁,也就是说 sleep() 方法不会释放锁资源。


2-4、interrupted()

public static boolean interrupted()

该方法和 isInterrupted() 方法功能相同,都是判断线程是否中断。主要区别有以下几点:

  • isInterrupted() 是实例方法,判断调用方法的对象线程是否停止
  • interrupted() 是静态方法,判断当前线程是否停止
  • isInterrupted() 不会处理线程中断标识,interrupted()方法会清理线程状态标识

关于区别一和区别二,根据方法修饰我们就能看出。我们主要验证方案三,具体我们看代码:

    @Test
public void test() {
    Thread thread1 = new Thread() {
        @Override
        public void run() {
            while (true) {
            }
        }
    };
    thread1.start();
    thread1.interrupt();
    System.out.println("第一次调用isInterrupted()方法,判断线程状态:" + thread1.isInterrupted());
    System.out.println("第二次调用isInterrupted()方法,判断线程状态:" + thread1.isInterrupted());
    Thread.currentThread().interrupt();
    System.out.println("第一次调用interrupted()方法,判断线程状态:" + Thread.interrupted());
    System.out.println("第二次调用interrupted()方法,判断线程状态:" + Thread.interrupted());
}

执行结果

第一次调用isInterrupted()方法,判断线程状态:true
第二次调用isInterrupted()方法,判断线程状态:true
第一次调用interrupted()方法,判断线程状态:true
第二次调用interrupted()方法,判断线程状态:false

通过执行结果我们可以看出,isInterrupted() 方法前后线程的状态没有变化。interrupted() 方法前后坐席的状态由 true 变为 false。

关于该方法的作用我是这样理解的:通过该方法判断线程标识为停止时,具体是否停止还是由代码逻辑决定。如果此时需要停止线程,就在判断为 true 时通过 return 或其他方法停止线程。如果线程还有用,不能停止,就不做任何处理,线程停止标识重置为 false,防止后续调用阻塞方法时抛出异常。


3、Object 继承方法

线程常用 Object 方法有以下几种:

  • wait()
  • notify()
  • notifyAll()

3-1、wait()

public final void wait() throws InterruptedException

wait() 方法和 sleep() 方法都会让当前线程阻塞。主要区别有以下几点:

  • wait() 方法是 Object 方法,sleep() 方法是 Thread 方法。

  • wait() 方法必须放弃锁资源,也就是说调用 wait() 方法必须先获取锁才行,否则抛出异常。sleep()方法不会释放锁资源,也不强制要求必须含有锁资源。

  • wait() 方法无参时表示无限期等待,等待其他线程调用 notify() 方法,sleep() 必须有参数。

  • wait() 方法有参时,除了等待具体的时长外,其他线程调用 notify() 方法也可以唤醒,而 sleep()只能等待参数时长。

  • wait() 线程唤醒后进入锁池,争夺锁资源。sleep() 线程唤醒后只要CPU调度就可以运行。

wait() 会抛出 InterruptedException 异常,也就是调用 wait() 方法阻塞时调用 interrupt() 方法也会跳过阻塞过程,不过它不能直接执行,因为还要获取锁资源。

在示例开始之前我先简单的介绍两个概念:锁池等待池

  • 锁池:所有竞争某个 synchronized 锁的线程都会进入这个对象的锁池。也就是说锁池中的线程都是阻塞竞争锁的线程

  • 等待池:所有竞争某个 synchronized 锁但 暂不抢占锁 的线程都会进入这个对象的等待池。等待池中的线程必须等待足够的时候或者被 notify() 唤醒后才会进入锁池,开始竞争锁。

在线程中调用某个对象的 wait() 方法会让当前线程放弃锁,进入该对象的锁池。下面我们看具体案例:

public class WaitTest {

    Object lock = new Object();

    private class WorkerA implements Runnable {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            try {
                synchronized (lock) {
                    System.out.println(threadName + "获取到锁的时间:" + System.currentTimeMillis());
                    lock.wait();
                }
                System.out.println(threadName + "执行完的时间" + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private class WorkerB implements Runnable {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            try {
                Thread.sleep(1000);
                synchronized (lock) {
                    System.out.println(threadName + "获取到锁的时间:" + System.currentTimeMillis());
                    lock.notify();
                }
                System.out.println(threadName + "执行完的时间" + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Test
    public void test() throws InterruptedException {
        new Thread(new WorkerA()).start();
        new Thread(new WorkerB()).start();
        Thread.sleep(3000);
    }
}

执行结果

Thread-0获取到锁的时间:1591413700745
Thread-1获取到锁的时间:1591413701745
Thread-1执行完的时间1591413701745
Thread-0执行完的时间1591413701745

上述案例中,WorkerB 线程首先睡眠一段时间,保证 WorkerA 线程抢占到锁。WorkerA 线程抢占到锁后调用 wait() 方法阻塞,放弃锁资源。此时 WorkerB 线程取到锁资源,执行 notify() 方法唤醒 WorkerA 线程,两个线程最终都执行完毕。

这里需要注意的一点是:wait() 方法会抛弃所有锁资源,而不仅仅是被调用的对象。具体我们看代码:

synchronized (lock)
修改为-synchronized (WaitTest.class)

执行结果

Thread-0获取到锁的时间:1591414650563
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
Thread-1获取到锁的时间:1591414651564
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException

从结果我们可以看出:wait() 方法调用后,WorkerA 线程抛出异常,并释放锁资源。WorkerB 线程获取锁资源,调用 notify() 方法后抛出异常结束。

IllegalMonitorStateException无论 wait()、notify() 还是 notifyAll() 方法都需要获取被调用对象的锁资源,如果没有获取到,就会抛出该异常,调用 wait() 方法的线程不会进入等待池,调用 notify() 方法的线程也不会唤醒对象等待池中的线程。

  • 前文我们提到 join() 方法的原理是调用 wait() 方法,但是在使用 join() 方法时我们不需要加锁,这是为什么呢?

    其实是因为 join() 方法本身就是 synchronized 修饰的,也就是说:我们调用 threadA.join() 方法后,会进入 threadA 对象的等待池。而 threadA对象本身也是线程,它执行完毕后会唤醒等待池中的线程,当前线程就可以继续向下执行。这也就是join() 方法的原理。

最后我再来谈谈我对 wait() 方法的理解(可能有误):调用 wait() 方法阻塞的线程被唤醒后,如果抢占到锁资源。它会根据程序计数器的指示执行对应行的代码,也就是说 synchronized() 方法不是在方法体的第一步获取锁的,而是每一步执行时都需要判断锁资源


3-2、notify()

public final native void notify();

notify() 也是 native 方法。关于 notify() 方法的作用已经在 wait() 方法模块介绍。这里需要补充的一点是:notify() 会唤醒等待池中随机一个线程。下面我们具体看案例:

public class NotifyTest {

    Object lock = new Object();

    private class Worker implements Runnable {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            try {
                synchronized (lock) {
                    System.out.println(threadName + "获取到锁");
                    lock.wait();
                }
                System.out.println(threadName + "被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Test
        @Test
    public void test() throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            new Thread(new Worker()).start();
        }
        Thread.sleep(2000);
        for (int i = 0; i < 5; i++) {
            synchronized (lock) {
                lock.notify();
            }
        }
        Thread.sleep(1000);
    }
}

执行结果

Thread-0获取到锁
Thread-2获取到锁
Thread-3获取到锁
Thread-1获取到锁
Thread-4获取到锁
Thread-0被唤醒
Thread-1被唤醒
Thread-2被唤醒
Thread-4被唤醒
Thread-3被唤醒

上述代码中,我们通过循环依次从 lock 对象的等待池中唤醒线程。从结果可以看出,notify() 方法唤醒线程是随机的,并且每次只唤醒一个线程。

在实际应用中,一般不使用 notify() 方法,因为它无法保证可以唤醒我们需要的那个线程。下面我们来看 notifyAll() 方法


3-3、notifyAll()

public final native void notifyAll();

notifyAll() 也是 native 方法。顾名思义,notifyAll() 方法可以唤醒所有线程。下面我们修改 notify() 测试用例的 JUnit 方法:

@Test
public void test() throws InterruptedException {
    for (int i = 0; i < 5; i++) {
        new Thread(new Worker()).start();
    }
    Thread.sleep(2000);
    synchronized (lock) {
        lock.notifyAll();
    }
    Thread.sleep(1000);
}

执行结果

Thread-0获取到锁
Thread-4获取到锁
Thread-2获取到锁
Thread-3获取到锁
Thread-1获取到锁
Thread-3被唤醒
Thread-2被唤醒
Thread-1被唤醒
Thread-4被唤醒
Thread-0被唤醒

从结果来看:调用 notifyAll() 方法可以唤醒所有线程。 一般实际应用中更偏向使用 notifyAll() 方法,保证需要使用的线程被唤醒。


参考:
https://www.cnblogs.com/java-spring/p/8309931.html
https://www.cnblogs.com/hongten/p/hongten_java_sleep_wait.html
https://www.cnblogs.com/jenkov/p/juc_interrupt.html
https://www.cnblogs.com/2015110615L/p/6736323.html
https://blog.csdn.net/qq_32679835/article/details/90174955
https://blog.csdn.net/weixin_42862834/article/details/106427027
https://blog.csdn.net/djzhao/article/details/79410229
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章