线程创建与运行
Java 中有三种线程创建方式:
- 实现 Runnable 接口的 run 方法
- 继承 Thread 类并重写 run 的方法
- 使用 FutureTask 方式。
首先看继承 Thread 类方式的实现。
public class ThreadTest {
//继承 Thread 类并重写 run 方法
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println(「I am a child thread」);
}
}
public static void main(String[] args) {
// 创建线程
MyThread thread = new MyThread();
// 启动线程
thread.start();
}
}
使用继承方式的好处是,在 run()方法内获取当前线程直接使用 this 就可以了,无须使用 Thread.currentThread()方法;而如果使用 Runnable 方式,则只能使用主线程里面被声明为 final 的变量。
不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而 Runable 则没有这个限制。
下面看实现 Runnable 接口的 run 方法方式。
public static class RunableTask implements Runnable{
@Override
public void run() {
System.out.println("I am a child thread");
}
}
public static void main(String[] args) throws InterruptedException{
RunableTask task = new RunableTask();
new Thread(task).start();
new Thread(task).start();
}
但是上面介绍的两种方式都有一个缺点,就是任务没有返回值。下面看最后一种,即使用 FutureTask 的方式。
//创建任务类,类似 Runable
public static class CallerTask implements Callable<String>{
@Override
public String call() throws Exception {
return 「hello」;
}
}
public static void main(String[] args) throws InterruptedException {
// 创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
//启动线程
new Thread(futureTask).start();
try {
//等待任务执行完毕,并返回结果
String result = futureTask.get();
System.out.println(result);
} catch (ExecutionException e) {
e.printStackTrace();
}
}
线程通知与等待
1.wait()函数
当一个线程调用一个共享变量的 wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:(1)其他线程调用了该共享对象的 notify()或者 notifyAll()方法;(2)其他线程调用了该线程的 interrupt()方法,该线程抛出 InterruptedException 异常返回。
另外需要注意的是,如果调用 wait()方法的线程没有事先获取该对象的监视器锁,则调用 wait()方法时调用线程会抛出 IllegalMonitorStateException 异常。
那么一个线程如何才能获取一个共享变量的监视器锁呢?
(1)执行 synchronized 同步代码块时,使用该共享变量作为参数。
synchronized(共享变量){
//doSomething
}
(2)调用该共享变量的方法,并且该方法使用了 synchronized 修饰。
synchronized void add(int a, int b){
//doSomething
}
另外需要注意的是,一个线程可以从挂起状态变为可以运行状态(也就是被唤醒),即使该线程没有被其他线程调用 notify()、notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。
虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用 wait()方法进行防范。退出循环的条件是满足了唤醒该线程的条件。
synchronized (obj) {
while (条件不满足){
obj.wait();
}
}
另外需要注意的是,当前线程调用共享变量的 wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。下面来看一个例子。
// 创建资源
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();
public static void main(String[] args) throws InterruptedException {
// 创建线程
Thread threadA = new Thread(new Runnable() {
public void run() {
try {
// 获取 resourceA 共享资源的监视器锁
synchronized (resourceA) {
System.out.println(「threadA get resourceA lock」);
// 获取 resourceB 共享资源的监视器锁
synchronized (resourceB) {
System.out.println(「threadA get resourceB lock」);
// 线程 A 阻塞,并释放获取到的 resourceA 的锁
System.out.println(「threadA release resourceA lock」);
resourceA.wait();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 创建线程
Thread threadB = new Thread(new Runnable() {
public void run() {
try {
//休眠 1s
Thread.sleep(1000);
// 获取 resourceA 共享资源的监视器锁
synchronized (resourceA) {
System.out.println(「threadB get resourceA lock」);
System.out.println(「threadB try get resourceB lock...」);
// 获取 resourceB 共享资源的监视器锁
synchronized (resourceB) {
System.out.println(「threadB get resourceB lock」);
// 线程 B 阻塞,并释放获取到的 resourceA 的锁
System.out.println(「threadB release resourceA lock」);
resourceA.wait();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动线程
threadA.start();
threadB.start();
// 等待两个线程结束
threadA.join();
threadB.join();
System.out.println(「main over」);
}
输出结果如下:
如上代码中,在 main 函数里面启动了线程 A 和线程 B,为了让线程 A 先获取到锁,这里让线程 B 先休眠了 1s,线程 A 先后获取到共享变量 resourceA 和共享变量 resourceB 上的锁,然后调用了 resourceA 的 wait()方法阻塞自己,阻塞自己后线程 A 释放掉获取的 resourceA 上的锁。
线程 B 休眠结束后会首先尝试获取 resourceA 上的锁,如果当时线程 A 还没有调用 wait()方法释放该锁,那么线程 B 会被阻塞,当线程 A 释放了 resourceA 上的锁后,线程 B 就会获取到 resourceA 上的锁,然后尝试获取 resourceB 上的锁。由于线程 A 调用的是 resourceA 上的 wait()方法,所以线程 A 挂起自己后并没有释放获取到的 resourceB 上的锁,所以线程 B 尝试获取 resourceB 上的锁时会被阻塞。
这就证明了当线程调用共享对象的 wait()方法时,当前线程只会释放当前共享对象的锁,当前线程持有的其他共享对象的监视器锁并不会被释放。
当一个线程调用共享对象的 wait()方法被阻塞挂起后,如果其他线程中断了该线程,则该线程会抛出 InterruptedException 异常并返回。
public class WaitNotifyInterupt {
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
//创建线程
Thread threadA = new Thread(new Runnable() {
public void run() {
try {
System.out.println(「---begin---」);
//阻塞当前线程
synchronized (obj) {
obj.wait();
}
System.out.println("---end---");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadA.start();
Thread.sleep(1000);
System.out.println("---begin interrupt threadA---");
threadA.interrupt();
System.out.println("---end interrupt threadA---");
}
}
输出如下。
在如上代码中,threadA 调用共享对象 obj 的 wait()方法后阻塞挂起了自己,然后主线程在休眠 1s 后中断了 threadA 线程,中断后 threadA 在 obj.wait()处抛出 java.lang. InterruptedException 异常而返回并终止。
2.wait(long timeout)函数
该方法相比 wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的 timeout ms 时间内被其他线程调用该共享变量的 notify()或者 notifyAll()方法唤醒,那么该函数还是会因为超时而返回。如果将 timeout 设置为 0 则和 wait 方法效果一样,因为在 wait 方法内部就是调用了 wait(0)。需要注意的是,如果在调用该函数时,传递了一个负的 timeout 则会抛出 IllegalArgumentException 异常。
3.wait(long timeout, int nanos)函数
在其内部调用的是 wait(long timeout)函数,如下代码只有在 nanos>0 时才使参数 timeout 递增 1。
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
4.notify() 函数
一个线程调用共享对象的 notify()方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
此外,被唤醒的线程不能马上从 wait 方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
类似 wait 系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的 notify()方法,否则会抛出 IllegalMonitorStateException 异常。
5.notifyAll() 函数
不同于在共享变量上调用 notify()函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。
下面举一个例子来说明 notify()和 notifyAll()方法的具体含义及一些需要注意的地方,代码如下。
// 创建资源
private static volatile Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
// 创建线程
Thread threadA = new Thread(new Runnable() {
public void run() {
// 获取 resourceA 共享资源的监视器锁
synchronized (resourceA) {
System.out.println(「threadA get resourceA lock」);
try {
System.out.println(「threadA begin wait」);
resourceA.wait();
System.out.println(「threadA end wait」);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
});
// 创建线程
Thread threadB = new Thread(new Runnable() {
public void run() {
synchronized (resourceA) {
System.out.println(「threadB get resourceA lock」);
try {
System.out.println(「threadB begin wait」);
resourceA.wait();
System.out.println(「threadB end wait」);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
});
// 创建线程
Thread threadC = new Thread(new Runnable() {
public void run() {
synchronized (resourceA) {
System.out.println(「threadC begin notify」);
resourceA.notify();
}
}
});
// 启动线程
threadA.start();
threadB.start();
Thread.sleep(1000);
threadC.start();
// 等待线程结束
threadA.join();
threadB.join();
threadC.join();
System.out.println(「main over」);
}
输出结果如下。
从输出结果可知线程调度器这次先调度了线程 A 占用 CPU 来运行,线程 A 首先获取 resourceA 上面的锁,然后调用 resourceA 的 wait()方法挂起当前线程并释放获取到的锁,然后线程 B 获取到 resourceA 上的锁并调用 resourceA 的 wait()方法,此时线程 B 也被阻塞挂起并释放了 resourceA 上的锁,到这里线程 A 和线程 B 都被放到了 resourceA 的阻塞集合里面。线程 C 休眠结束后在共享资源 resourceA 上调用了 notify()方法,这会激活 resourceA 的阻塞集合里面的一个线程,这里激活了线程 A,所以线程 A 调用的 wait()方法返回了,线程 A 执行完毕。而线程 B 还处于阻塞状态。如果把线程 C 调用的 notify()方法改为调用 notifyAll()方法,则执行结果如下。
从输入结果可知线程 A 和线程 B 被挂起后,线程 C 调用 notifyAll()方法会唤醒 resourceA 的等待集合里面的所有线程,这里线程 A 和线程 B 都会被唤醒,只是线程 B 先获取到 resourceA 上的锁,然后从 wait()方法返回。线程 B 执行完毕后,线程 A 又获取了 resourceA 上的锁,然后从 wait()方法返回。线程 A 执行完毕后,主线程返回,然后打印输出。
一个需要注意的地方是,在共享变量上调用 notifyAll()方法只会唤醒调用这个方法前调用了 wait 系列函数而被放入共享变量等待集合里面的线程。
等待线程执行终止的 join 方法
在项目实践中经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理。Thread 类中有一个 join 方法就可以做这个事情,join 方法是 Thread 类直接提供的。join 是无参且返回值为 void 的方法。
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(「child threadOne over! 」);
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(「child threadTwo over! 」);
}
});
//启动子线程
threadOne.start();
threadTwo.start();
System.out.println(「wait all child thread over! 」);
//等待子线程执行完毕,返回
threadOne.join();
threadTwo.join();
System.out.println("all child thread over! ");
}
另外,线程 A 调用线程 B 的 join 方法后会被阻塞,当其他线程调用了线程 A 的 interrupt()方法中断了线程 A 时,线程 A 会抛出 InterruptedException 异常而返回。下面通过一个例子来加深理解。
public static void main(String[] args) throws InterruptedException {
//线程 one
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(「threadOne begin run! 」);
for (; ; ) {
}
}
});
//获取主线程
final Thread mainThread = Thread.currentThread();
//线程 two
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
//休眠 1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中断主线程
mainThread.interrupt();
}
});
// 启动子线程
threadOne.start();
//延迟 1s 启动线程
threadTwo.start();
try{//等待线程 one 执行结束
threadOne.join();
}catch(InterruptedException e){
System.out.println(「main thread:」 + e);
}
}
输出结果如下。
这里需要注意的是,在 threadTwo 里面调用的是主线程的 interrupt()方法,而不是线程 threadOne 的。