我们平常接触比较多的创建线程的方式一般有两种,第三种接触的比较少,之前去京东面试被问到过,自己太菜,直接就懵逼了,第三种方式完全不知道,现在统一做个总结。
目录
一、方式一:继承Thread类
这种方式就是我们自己创建一个类然后继承Thread,并覆写Thread中的run()就可以,比较简单,直接看demo:
public class MyThread extends Thread {
//定义指定线程名称的构造方法,构造方法可以不写,默认使用无参构造
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":正在执行!"+i);
}
}
}
测试类:
public class Demo01 {
public static void main(String[] args) {
//创建自定义线程对象
MyThread mt = new MyThread("新的线程!");
//无参构造
//MyThread mt = new MyThread();
//开启新线程
mt.start();
//在主方法中执行for循环
for (int i = 0; i < 10; i++) {
System.out.println("main线程!"+i);
}
}
}
测试结果:
main线程!0
main线程!1
main线程!2
main线程!3
main线程!4
main线程!5
main线程!6
main线程!7
main线程!8
main线程!9
新的线程!:正在执行!0
新的线程!:正在执行!1
新的线程!:正在执行!2
新的线程!:正在执行!3
新的线程!:正在执行!4
新的线程!:正在执行!5
新的线程!:正在执行!6
新的线程!:正在执行!7
新的线程!:正在执行!8
新的线程!:正在执行!9
二、方式二:实现Runnable接口
方式二应该是我们最熟悉的,平常在开发种如果我们有线程方面的需求,一般都是用这种方式来实现:自定义一个类实现Runnable接口,并实现其中的run()方法,之后new出这个类对应的对象,并把这个对象作为Thread的参数传递进去就可以了:
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
public class Demo02 {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr, "小强");
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("旺财 " + i);
}
}
}
测试结果:
旺财 0
旺财 1
旺财 2
旺财 3
旺财 4
小强 0
小强 1
小强 2
小强 3
小强 4
方式一和方式二的区别:
- 因为java是单继承模式,所以方式二可以避免方式一单继承的局限性;
- 增加程序的健壮性,实现解耦,代码可以被多个线程共享,代码和线程独立;
- 线程池中只能放实现了Runnable或Callable接口的线程,不能直接放继承至Thread的类。
Thread和Runnable的关系:
Thread和Runnable的关系我们通过查看源码简单了解一下,首先看一下带Runnable参数的Thread的构造函数:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
发现里面调用了一个init()方法,我们跟下去:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null);
}
继续跟:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
......
this.target = target;
......
}
这个方法中代码比较多,我们只看相关的部分,没错,我们只看这一行就行。这个this.target就是Thread类里的一个Runnable类型的成员变量,这行代码就是把我们刚开始传进来的Runnable对象赋值给了Thread类中的target变量,而这个变量也是一个Runnable类型的。继续往下看代码,有这么一行:
@Override
public void run() {
if (target != null) {
target.run();
}
}
着就是Thread中的run()方法,我们可以看到,它首先会判断target是否为空(这个target就是我们上面将的那个Thread类中的成员变量),不为空的话就会调用tartget的run方法,也就是接口Runnable的run方法。到这里我们基本就理清方式一和方式二在创建线程时的联系了:如果我们用方式一继承Thread类的方式来创建线程,我们覆写Thread里面的run方法,也就是上面代码的那个部分,这个时候线程启动后就直接执行的是我们覆写过的run方法;如果我们用的是方式二实现Runnable接口的方式创建线程,那么线程启动后也会走Thread本身的run方法,只不过是在这个run方法内部又进一步调用了Runnable接口的run方法来执行具体的逻辑,而这个Runnable接口的run方法就是我们在实现Runnable接口时,必须实现的那个方法。
三、Callable加FutureTask方式
方式三其实是对方式二的进一步加强,所以在创建的时候和方式二有相似之处,方式二需要我们自定义一个类实现Runnable接口并实现其中的run方法来创建线程任务,最后把这个线程任务作为参数传递给Thread;而方式三是需要我们:
1.自定义一个类实现Callable接口并实现其中的call方法来创建线程任务(Runnable对Callable,run()方法对call()方法);
2.接着把这个线程任务以参数的形式传递给FutureTask,并new出一个FutureTask对象;
3.最后将这个FutureTas对象继续以参数的形式传递给Thread。
通过上面的步骤我们可以看出,相比方式二就是多了一步步骤2的过程。方式三与方式二相比,方式二中的run方法中不能有返回值,也不可在run方法中抛出异常,而方式三的call方法却可以有返回值,也可以抛出异常,这就是方式三相对与方式二加强的点。抛出异常很好理解,方法在执行的过程中有时候难免会抛出异常,所以方式三在方式二的基础上做了相关的完善。可能你会问那有返回值有什么用呢,这一点也很好理解,现在假如有两个线程A和B,而线程B在执行的时候需要用到线程A运行结束后的执行结果,那这个时候我们就可以用方式三来实现线程A和B,将线程A执行结束后返回的结果保存起来,在线程B运行的时候,把这个结果传给它就可以了。下面我们来看方式三的Demo:
/**
* <Integer>这个泛型就表示call方法返回值得类型,如果你call方法返回的是字符串
* 那你就在这里写<String>,因为我这里是返回的100,s所以就写的<Integer>
*/
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() +":call方法执行了...");
return 100;
}
}
public class Demo3 {
public static void main(String[] args) {
//步骤1
MyCallable myCallable = new MyCallable();
//步骤2。同样这里的泛型<Integer>也指的是call方法的返回值类型
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
//步骤3
Thread threadA = new Thread(futureTask,"threadA");
//开启线程
threadA.start();
try {
/**
* 这个FutureTask的get()方法的作用就是获取线程A的返回值;
* 它是一个阻塞方法,即在那个线程中调用,那个线程就会阻塞,直到线程执行完了并返回结果
* 后。我们现在在主线程中调用了这个方法,所以在线程A执行完毕并返回结果之前,主线程会一直
* 阻塞这这里,直到线程A执行完后,下面的代码才可执行,并达到线程A的执行结果
*/
Integer num = futureTask.get();
//在主线程中打印从线程A中返回的值
System.out.println(Thread.currentThread().getName() +":在主线程中打印从线程A中返回的值num=" + num);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
输出结果:
threadA:call方法执行了...
main:在主线程中打印从线程A中返回的值num=100
通过输出结果的顺序我们可以看到,线程A先执行完后,主线程才执行,并且在主线程中拿到了线程A的执行结果。
到此,线程的三种方式就讲解完毕了。