创建线程的本质上只有继承Thread类 和 实现Runnable接口两种方式,其他方式如通过线程池创建线程、通过Callable 和 FutureTask创建线程、通过定时器创建线程等,其本质还是通过上述两种方式进行创建线程,他们都只不过是包装了new Thread( )。
多线程的实现方式,在代码中写法千变万化,但是其本质万变不离其宗。
public class ThreadStyle extends Thread{
//重写Thread类的run方法
@Override
public void run() {
System.out.println("通过继承Thread类实现线程");
}
public static void main(String[] args) {
/*
因为继承Thread类之后重写了Thread类的run方法,所以这里调用的是继承Thread类的子类
重写的run方法,这里即ThreadStyle中的run方法
*/
//直接创建继承Thread的子类的实例,然后通过实例对象调用start方法开启线程
ThreadStyle threadStyle = new ThreadStyle();
threadStyle.start();
}
}
public class RunnableStyle implements Runnable{
//实现Runnable接口中的run方法
@Override
public void run() {
System.out.println("通过Runnable接口实现线程");
}
public static void main(String[] args) {
/*
@Override
public void run() {
if (target != null) {
target.run();
}
}
所以实现Runnable接口的方式实现线程,最终调用的目标对象的run方法,
这里即new RunnableStyle()对象的run方法,即上面的run方法
*/
//传入Runnable接口的实现类对象作为target参数值,然后创建一个Thread对象
Thread thread = new Thread(new RunnableStyle());
thread.start();
}
}
/**
* 通过线程池的方式创建线程
* 本质还是通过继承Thread类和实现Runnable接口两种方式
*/
public class ThreadPool5 {
public static void main(String[] args) {
/**
* 深入源码可以看出,线程池创建线程的本质还是通过new Thread的方法
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
*/
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++){
executorService.submit(new Tasktest(){
});
}
}
}
class Tasktest implements Runnable{
@Override
public void run() {
//线程休眠
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印当前线程的名字
System.out.println(Thread.currentThread().getName());
}
}
聚合关系:强调整体与部分的关系,整体由部分构成,比如一个部门有多个员工组成;
与组合关系不同的是,整体和部分不是强依赖的,即使整体不存在了,部分依然存在;
例如: 部门撤销了,员工依然在;
组合关系:与聚合关系一样,表示整体由部分构成,比如公司有多个部门组成;
但是组合关系是一种强依赖的特殊聚合关系,如果整体不在了,则部门也不在了;
例如:公司不在了,部门也将不在了;
聚合关系:用一条带空心菱形箭头的直线表示;
组合关系:用一条带实心菱形箭头的直线表示;参考:
五分钟读懂UML类图
看懂UML类图和时序图
类图
通过上面的类图分析,可以知道:通过Callable 和 FutureTask创建线程,实质上底层还是通过继承Thread 和 实现Runnable接口这两种方式创建线程。
主要步骤:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 通过Callable和FutureTask创建线程
* 万变不离其宗
*/
public class CallableandFutureTask implements Callable<Integer> {
public static void main(String[] args) {
//创建Callable接口实现类的实例
CallableandFutureTask callableandFutureTask = new CallableandFutureTask();
//使用FutureTask包装Callable对象(其包装了Callable对象的call()方法的返回值)
FutureTask<Integer> futureTask = new FutureTask<>(callableandFutureTask);
for (int i = 0; i < 100; i++){
System.out.println(Thread.currentThread().getName()+" 的循环变量i的值 "+i);
if (i == 20){//i等于20的时候开始执行子线程
//将FutureTask类的实例作为target传入Thread类中,创建并启动线程(类似实现Runnable接口方式创建线程)
new Thread(futureTask,"有返回值的线程").start();
}
}
try {
//调用FutureTask对象的get()方法来获取子线程执行结束后的返回值
System.out.println("子线程的返回值:"+ futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
// 实现Callable接口的call方法
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 100; i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
参考:
java创建线程的三种方式及其对比
通过定时器创建线程,本质其实还是通过继承Thread类 和 Runnable接口两种方式创建线程
import java.util.Timer;
import java.util.TimerTask;
public class DemoTimerTask {
public static void main(String[] args) {
Timer timer = new Timer();
/*
用于定时按周期做任务
第一个参数是task: 表示执行的任务
第二个参数是delay:表示初始化延时,即初始化延迟多少时间开始执行;
第三个参数是period:表示每个多少时间执行一次任务(周期)
注意:这里的period表示的是相邻两个任务开始之间的时间,因此执行时间不会延后
总结起来就是:启动后过了delay时间之后,开始以period为间隔执行task任务
还需注意schedule和scheduleAtFixedRate的区别
*/
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
},5000,1000);
}
}
说明一下schedule方法和scheduleAtFixedRate方法的区别
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
},5000,1000);
参考:
Java定时任务调度详解
实现Runnable接口方式相比继承Thread类的优点:
继承Thread类的子类的内部变量不会直接共享
当要新建一个任务时,如果是继承Thread类的方式,则需要new一个类的对象,但是这样做的话损耗比较大,这样我们每次都需要去新建一个线程,执行完之后还需要去销毁。但是如果我们采用实现Runnable接口的方式,传入target,传入实现Runnable接口的类的实例的方法,这样我们就可以反复地利用同一个线程。比如,线程池就是这样做的。
总结:
继承Thread类的方式,线程不可复用;
实现Runnable接口的方式,线程可以复用;
继承Thread类方式也有几个小小的好处,但相对于其缺点来说,其优点不值一提:
public class BothRunnableThread {
public static void main(String[] args) {
//使用匿名内部类(两个:一个是Runnable类,一个是Thread类)
new Thread(new Runnable() {
//实现Runnable接口的run方法
@Override
public void run() {
System.out.println("来自Runnable接口的实现类的run方法!");
}
}){
//重写父类Thread类的run方法
@Override
public void run() {
System.out.println("来自继承Thread的子类重写之后的run方法");
}
}.start();
}
}
输出结果:
来自继承Thread的子类重写之后的run方法
因为继承Thread类的子类重写了run方法,调用的时候重写的run方法会覆盖Runnable接口实现类中实现的run方法,所以最终调用的是重写的run方法。