本文是关于JAVA多线程和并发的第四篇,本篇文章主要来看看线程相关的几个重要方法。

wait和sleep

他们最基本的差异是:

  • waitObject的一个方法,sleepThread类的方法
  • sleep可以在任何地方使用,但是wait方法只能在synchronied方法或synchronied块中使用

本质的区别是:

  • Thread.sleep只会让出CPu,不会导致锁行为的改变
  • Object.wait不仅让出CPU,还会释放已经占用的同步资源锁

这个区别也就解释了为什么wait方法只能在synchronied方法或synchronied块中使用,因为没有获取过锁哪里来的释放锁呢?所以释放锁的前提是要获取锁。

下面来验证一下,眼见为实!

image

对于sleep来说是没有锁的要求的,既不用获取锁也不用释放锁,关于这一点就不再验证了。

notify和notifyAll

先来了解一下锁池和等待池的概念。

  • 锁池EntryLisy

假设线程A已经拥有了某个对象(不是类)的锁,而其他线程B、C想要调用这个对象的某个synchronized方法或块,由于B、C线程在进入对象的synchronized方法或块之前必须先获得该对象锁得拥有权,而恰巧该对象的锁正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待所得释放,这个地方就是该对象得锁池。

  • 等待池WaitSet

假设线程A调用了某个对象的wait方法后,线程A就会释放该对象得锁,同时线程A句进入到该对象得等待池中,进入到等待池中得线程不会去竞争该对象的锁。

notify的作用就是随机唤醒一个线程进入等待池的线程,而notifyAll是唤醒所有处于等待池中线程,唤醒之后就可以再去竞争获得锁的机会了。

刚才的例子稍微改造一下,来了解一下notify的作用。还拿刚才那个例子:

image

image

yield

image

当调用Thread.yield()函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。并且它也不会释放当前线程占用的锁。

yield()与无参的wait()的区别:

  • 执行yield()后,当前线程由运行状态变为就绪状态。执行wait后,当前线程会失去对象的锁,状态变为WAITING状态。
  • 执行yield()后,当前线程不会释放锁。执行wait后,当前线程会释放锁。

比较简单,就不举例了。

interrupt

它只是通知线程应该中断了。

  • 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个interruptedException异常
  • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。

也就是说,中断一个线程是由被调用的线程状态和自己程序判断决定的。

  • 阻塞状态下,线程会立即退出,并抛出异常
  • 正常状态下,需要被调用的线程检查中断标志位,然后再根据中断标志位自行地停止线程

下面写一个demo来验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
Runnable interruptTask = new Runnable() {
@Override
public void run() {
int i = 0;
try {
//在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程
while (!Thread.currentThread().isInterrupted()) {
Thread.sleep(100); // 休眠100ms
i++;
System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") loop " + i);
}
} catch (InterruptedException e) {
//在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程。)
System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") catch InterruptedException.");
}
}
};

Thread t1 = new Thread(interruptTask, "t1");
System.out.println(t1.getName() +" ("+t1.getState()+") is new.");

// 1.启动“线程t1”
t1.start();
System.out.println(t1.getName() +" ("+t1.getState()+") is started.");

// 2.主线程休眠300ms,然后主线程给t1发“中断”指令。
Thread.sleep(400);
t1.interrupt();
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted.");

// 3.主线程休眠300ms,然后查看t1的状态。
Thread.sleep(300);
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
}
}

打印结果为:

1
2
3
4
5
6
7
8
t1 (NEW) is new.
t1 (RUNNABLE) is started.
t1 (RUNNABLE) loop 1
t1 (RUNNABLE) loop 2
t1 (RUNNABLE) loop 3
t1 (TIMED_WAITING) is interrupted.
t1 (RUNNABLE) catch InterruptedException.
t1 (TERMINATED) is interrupted now.

首先,是就绪状态,为new;接下来启动这个线程,状态变为started,由于此时一切安好,没有“打扰”这个线程的执行,所以每隔100毫秒打印一句(RUNNABLE) loop i出来;在400毫秒的安好之后,给他一个t1.interrupt();,此时线程可能恰好在执行sleep睡觉呢,这个interrupt一看你在阻塞(睡觉),那还得了,立马停止这个线程并且抛出异常。

但是话说回头,本程序还用了if判断,只要标志位为false就不停循环,一旦标志位变为true则立马退出循环。所以即使你不睡觉,但是我还是能通过这个If来终止你的循环。

join

join是加入的意思,非常形象生动。

1
2
3
4
5
6
7
8
/**
* Waits for this thread to die.
* 调用方线程(调用join方法的线程)执行等待操作,
* 直到被调用的线程(join方法所属的线程)结束,再被唤醒
*/
public final void join() throws InterruptedException {
join(0);
}

具体的实现如下:

image

我们知道wait是需要释放当前线程所占的对象锁的,而join基于wait实现,显然是可以的。

这里判断如果线程还在运行中的话,则继续等待,如果指定时间到了,或者线程运行完成了,则代码继续向下执行,调用线程就可以执行后面的逻辑了。

但是在这里没有看到哪里调用notify或者notifyAll方法,如果没有调用的话,那调用方线程会一直等待下去,那是哪里调用了唤醒它的方法呢?通过查证得知,原来在线程结束时,java虚拟机会执行该线程的本地exit方法,这个exit方法里面会调用notifyAll方法,唤醒所有等待的线程。

下面来两个例子来彻底理解它的用法。

例子一:有耐心的男孩:

image

男孩和女孩准备出去逛街

女孩开始化妆,男孩在等待。。。

女孩化妆完成!,耗时5000

男孩和女孩开始去逛街了

就是男孩和女孩准备去逛街,女孩要化妆先,等女孩化妆完成了,再一起去逛街。

例子二:没有耐心的男孩:

image

男孩和女孩准备出去逛街

女孩开始化妆,男孩在等待。。。

男孩等了2000, 不想再等了,去逛街了

女孩化妆完成!,耗时5000

男孩等了join(time)中的time时间,如果这个time时间到达之后,女孩所在的线程还没执行完,则不等待了,继续执行后面的逻辑,就是不等女孩了,自己去逛街。

总结

了解了这些核心方法之后,就可以对下面这幅图简单说一说啦:

image

首先是new Thread()只是新建状态,只有start之后才会进入runnable状态,注意这个状态里面可能有两种状态,一种是正在运行,即running,还有一种是就绪状态即ready,这两个状态归属于一类的原因是他们之间是在不断切换的,即CPU的时间片内临幸到这个进程,这个进程中有若干个线程的话,就会高速地切换各个线程逐个执行,达到宏观上是并行执行的效果。我们知道yield是给线程调度器一个暗示让出当前执行的线程的时间片,至于这个线程调度器听不听那就不知道了,所以存在一定的随机性。如果正常执行结束就进入最后的终止状态。往右边看,如果发生带时间的超时等待,如sleep(100),本线程会阻塞,让出CPU执行权并且不改变锁状态,与之区别的是wait(100)这个方法不仅让出CPU执行权,还会释放锁,所以要调用wait方法必然要先获取锁,所以一般都是在synchronized中调用它。至于join(100)是指阻塞当前线程,让其他的线程先执行,底层是wait所以也会释放锁。超时等待只要等它时间过了就可以跳出阻塞状态了,或者用notify或者interrupt之类的来唤醒或者打断它。往左下角看,是锁获取的时候可能发生阻塞,这个时候只能等其他线程释放锁才行了。往左边看,是无限期等待的代表,唤醒手段与有限期等待是一样的。