java多线程之创建一个线程
今天来介绍创建线程的几种方式。相信大家学习了java之后很快就能说出两种方式,继承Thread类,或者实现Runnable接口,但是今天除了介绍这两种方式之外还介绍其他两种线程的创建方式。
一、继承Thread类
首先介绍第一种方式,继承Thread类。
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 这是MyThread线程");
System.out.println("继承Thread类方式实现一个线程");
}
public MyThread(String name) {
this.setName(name);
}
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + " 这是main线程");
MyThread myThread = new MyThread("myThread");
myThread.start();
}
}
继承Thread类之后重写Thread类的run方法。并调用其start方法,就会新创建一个线程。这里说一下调用run方法和调用start方法的区别,调用run方法只是简单的掉用一个对象实例方法一样,不会创建线程。调用start方法则会通知java虚拟机,由java虚拟机新建一个线程并执行run方法。
为了证明我们是新启动了一个线程,而不是在main线程里进行。我们使用Thread.currentThread().getName()获取当前执行的线程的名称,并打印在控制台进行比较。
这里如果你想验证start方法和run方法的区别,你可以将myThread.start()更改为myThread.run()观看控制台打印出的线程名称。
二、实现Runnable接口
我们现在介绍第二种方式,实现Runnable接口。
public class MyThread02 implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName() + " 这是MyThread02线程");
System.out.println("继承Thread类方式实现一个线程");
}
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + " 这是main线程");
MyThread02 myThread02 = new MyThread02();
new Thread(myThread02,"myThread02").start();
}
}
同样,我们实现Runnable接口后,重写他的run方法。
但是在创建线程时略有不同,继承Thread类,我们直接new出对应的实例对象,就可以使用他的start方法来启动线程。
但是我们发现Runnable接口内只声明了一个run方法,并没有start方法。所以线程类(Thread)给我们提供了新的构造方法,那就是这两个构造方法:
public Thread(Runnable target, String name) {
this(null, target, name, 0);
}
public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}
我们只需要传一个实现了Runnable接口的对象,就可以获得一个线程,并且该线程的run方法就是该对象重写之后的run方法。同时为了证明我们是新启动了一个线程,我们继续使用Thread.currentThread().getName()来证明
三、实现Callable接口
现在来到我们今天的重点介绍,Callable接口。首先我们实现Callable接口看一下。
public class MyThread03 implements Callable<T>{
}
这时候我们发现,Callable接口有一个泛型。这个泛型代表了什么呢,我们等下讲,我们先进到Callable的源码里看一下。
package java.util.concurrent;
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
我把多余的注释删除后复制出来贴在这,我们发现他和Runnable接口很相似,也是只有一个方法,但是这个方法叫做call。神奇之处在于,call方法是一个有返回值的方法。我们之前无论是继承Thread类还是实现Runnable接口,我们都知道他的run方法是没有返回值的。这个返回值呢,也是Callable接口的独特之处。有了这个返回值,我们就能让一个线程在运行结束之后,我们可以得到一个线程运行的结果。这个结果也就是Callable接口实现时声明的泛型。
那好,现在我们先随便声明一个返回值,然后我们来使用Callable接口来得到一个线程。
import java.util.concurrent.Callable;
public class MyThread03 implements Callable<Integer>{
public static final Integer i = new Integer(0);
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() +":"+ i);
return i;
}
public static void main(String[] args) {
}
}
我们先写到这,然后发现,傻了。为什么呢?因为我们发现Thread类没有提供Callable的构造方法。
那怎么办呢?这时候我们就用一个新的类叫FutureTask。我们来观察一下这个FutureTask类发现,他实现了一个叫RunnableFuture的接口,而这个接口继承于Runnable, Future这两个接口,也就是说,他的最上层依旧是一个Runnable接口,这也就是我们常说的设计模式中的适配器模式。而正好FutureTask为我们提供Callable接口的构造方法。
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
好了,回到我们线程创建的代码来。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class MyThread03 implements Callable<Integer> {
public static final Integer i = new Integer(0);
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() +":"+ i);
return i;
}
public static void main(String[] args) {
MyThread03 myThread03 = new MyThread03();
System.out.println(Thread.currentThread().getName()+"我是main线程");
FutureTask<Integer> futureTask = new FutureTask<Integer>(myThread03);
new Thread(futureTask).start();
}
}
我们实例化实现了Callable接口的对象,并使用其实例化FutureTask。使用继承了Runnable接口的FutureTask来实例化线程,并查看控制台打印结果。
这里正好说一下FutureTask的几个方法。我们知道Callable接口和Runnable的接口的方法区别重点在于返回值。那么我们如何来得到这个返回值呢,我们就需要调用FutureTask的get方法。
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
/**
* @throws CancellationException {@inheritDoc}
*/
public V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (unit == null)
throw new NullPointerException();
int s = state;
if (s <= COMPLETING &&
(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
throw new TimeoutException();
return report(s);
}
FutureTask,为我们提供了两种get的方式,一种是不带参数的get,就是当一个线程去拿这个返回值时,如果call方法还没有执行完毕,那么该线程陷入阻塞状态等待call方法执行完毕。另一种是带了一个等待时间的参数,也就是说我等你call方法一段时间,如果你在这段时间内执行好了,那么我就继续执行。如果超出这个时间你没执行好,那么我就抛出一个TimeoutException。
接下来我们修改一下代码来体现这两个方法。首先是不带参数的方法
public class MyThread03 implements Callable<Integer> {
public static final Integer i = new Integer(0);
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() +":"+ i);
Thread.sleep(3000);
return i;
}
public static void main(String[] args) {
MyThread03 myThread03 = new MyThread03();
System.out.println(Thread.currentThread().getName()+"我是main线程");
FutureTask<Integer> futureTask = new FutureTask<Integer>(myThread03);
new Thread(futureTask).start();
try {
System.out.println(futureTask.get());
} catch (InterruptedException | ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"我是main线程");
}
}
我们让call方法睡眠3秒钟,模拟业务逻辑占用时间。然后在main方法中调用get方法,并在调用get方法后打印信息。执行后发现3秒后,call方法执行完毕后才打印出get方法的返回值和main方法后续信息。也就是说main方法调用了get后,陷入了等待,等待call方法执行完毕后才继续执行。
那么我们再来实验第二个方法,带有超时时间参数的方法。
public class MyThread03 implements Callable<Integer> {
public static final Integer i = new Integer(0);
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + ":" + i);
Thread.sleep(3000);
return i;
}
public static void main(String[] args) {
MyThread03 myThread03 = new MyThread03();
System.out.println(Thread.currentThread().getName() + "我是main线程");
FutureTask<Integer> futureTask = new FutureTask<Integer>(myThread03);
new Thread(futureTask).start();
try {
System.out.println(futureTask.get(1, TimeUnit.SECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "我是main线程");
// while (futureTask.isDone()) {
// try {
// futureTask.get();
// } catch (InterruptedException | ExecutionException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
// }
}
}
运行后发现,控制台没有正确打印出call方法返回值,并打印出了catch到的TimeoutException。说明main线程等待了call方法1秒钟后发现没有执行完毕,于是抛出异常。
Callable接口呢,我们就说到这里。更多Callable的信息大家可以阅读FutureTask的源码来进行了解,并不困难。
四、线程池
接下来介绍第四种线程创建的方式: 线程池。
线程池的方式相对来说比较简单。先贴代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThread04 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
System.out.println("我是t1线程");
}, "t1");
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(t1);
}
}
我们new 一个线程对象。然后调用线程池的submit方法就可以通过使用线程池的方式来创建一个线程。
这里只是演示如何创建线程,不过多的解释线程池的具体信息。