一、线程创建的三种方式
Java中有三中线程创建方式,分别是实现Runnable接口的run方法,继承Thread类并重写run的方法,使用FutureTask方式。
继承Thread类并重写run的方法
当创建完thread对象后该线程并没有被启动执行,知道调用了start方法后才真正启动了线程。
其实调用start方法后线程并没有马上执行,而是处于就绪状态,这个就绪状态是指该线程已经获取了除CPU之外的其他资源,等待获取CPU资源后才会真正处于运行状态。一旦run方法执行完毕,该线程就处于终止状态。
优点:
- 在run方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法。
- 方便传参,可以在子类里面添加成员变量,通过set方法设置参数或通过构造函数进行传递。
缺点:
- Java不支持多继承,如果继承了Thread类,那么就不能再基础其他类。
- 任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而Runable则没有这个限制。
代码实现:
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();
}
}
复制代码
实现Runnable接口的run方法
缺点:
如果要传递参数,只能使用主线程里面被声明为final的变量。
代码实现
public class ThreadTest {
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方式
上面介绍的两种方式都有一个缺点,就是任务没有返回值。
代码实现
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest {
//创建任务类,类似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();
}
}
}
复制代码
二、线程通知和等待
Java中的Object类是所有类的父类,鉴于继承机制,Java把所有类都需要的方法放到了Object类里面,其中就包含了本节要讲的通知和等待系列函数———wait()、notify()、notifyAll()
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方法只会释放当前共享变量上的锁
当前线程调用共享变量的wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。
举例
public class ThreadTest {
//创建资源
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();
public static void main(String[] args) throws InterruptedException{
//创建线程threadA
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try{
//获取resourceA共享资源的监视器锁
synchronized(resourceA){
System.out.println("threadA get resourceA lock");
synchronized(resourceB){
System.out.println("threadA get resourceB lock");
//线程A阻塞,并释放(release)获取到的resourceA的锁
System.out.println("threadA release resourceA lock");
resourceA.wait();
}
}
}catch (Exception e){
e.printStackTrace();
}
}
});
//创建线程threadB
Thread threadB = new Thread(new Runnable() {
@Override
public void run(){
try{
//休眠1秒
Thread.sleep(1000);
//获取resourceA共享资源的监视器锁
synchronized(resourceA){
System.out.println("threadB get resourceA lock");
System.out.println("threadB try get resouceB lock...");
//获取resourceB共享资源的监视器锁
synchronized(resourceB){
System.out.println("threadB get resourceB lock");
//线程B阻塞,并释放获取到的resourceA 的锁
System.out.println("threadB release resourceA lock");
resourceA.wait();
}
}
}catch(Exception 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 ThreadTest {
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(Exception e){
e.printStackTrace();
}
}
});
threadA.start();
Thread.sleep(1000);
System.out.println("---begin interrupt threadA---");
threadA.interrupt(); //主线程中断了线程threadA
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 < 0,则会抛出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()函数
notifyAll()方法会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。
需要注意的是:在共享变量上掉员工notifyAll()方法只会唤醒调用这个方法前调用了wait系列函数而被放入共享变量等待集合里面的线程。 如果调用notifyAll()方法后,一个线程调用了该共享变量的wait()方法而被放入阻塞集合,则该线程是不会被唤醒的。
示例
public class NotifyAndNotifyAll {
//创建资源
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(Exception e){
e.printStackTrace();
}
}
}
});
//创建线程
Thread threadB = new Thread(new Runnable() {
public void run(){
//获取resourceA共享资源的监视器锁
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(Exception e){
e.printStackTrace();
}
}
}
});
//创建线程
Thread threadC = new Thread(new Runnable() {
public void run(){
synchronized(resourceA){
System.out.println("threadC begin notify");
//resourceA.notify(); //唤醒一个在共享变量resourceA上调用wait方法所阻塞的线程
resourceA.notifyAll(); //唤醒所有在共享变量resourceA上调用wait方法所阻塞的线程
}
}
});
//启动线程
threadA.start();
threadB.start();
Thread.sleep(1000); //主线程休眠1s
threadC.start();
//等待线程结束
threadA.join();
threadB.join();
threadC.join();
System.out.println("main over");
}
}
复制代码
调用notify()的运行结果
只有一个线程A被唤醒,线程B没有被唤醒
调用notifyAll()的运行结果
线程A和线程B都会被唤醒