现在的开发行业对程序员的水准可以说是越来越高,如果只是在小公司每天还竟是crud并且还不想转行,那么笔者劝你多学些技术,去外边多面试这样才会知道自己的不足去提升自己,跳槽涨工资是很香的。
今天主要是对Java的线程创建方式做一个讲解,后续还会对线程的方面做文章。
-
java线程的创建方式共4种。
- extend Thread
- implement Runnable
- Callable和FutureTask组合
- 线程池ThreadPool
-
第一种Thread的方式
- 通过new Thread();方式创建线程,如下图
1 Thread threadA = new Thread(); //调用Thread的无参构造方法创建一个线程threadA
2 threadA.setName("thread-A"); // threadA线程起个名字:thread-A, 通过setName方法给Thread的name属性赋值
3 threadA.start(); //调用Thread的start方法启动线程
但经过以上创建线程的方式是没有执行实例的,也就是说创建了一个空线程,线程启动后没有具体的执行逻辑
2. 通过继承Thread的方式创建线程,如下列代码
public class Demo {
static class ChildThread extends Thread {
@Override
public void run() { // 重写Thread中的run方法,
System.out.println(Thread.currentThread().getName()); //打印子线程名称
}
}
public static void main(String[] args) {
ChildThread childThread = new ChildThread(); //创建线程
childThread.start(); // 启动线程
}
}
如上代码所述,静态内部类ChildThread继承了Thread类,那么就可以通过new ChildThread()方式创建线程了,不同的是run方法可以写在ChildThread里,然后通过start方式去启动线程
这种方式有一点需要注意的,run方法不能直接调用,因为start方法才是启动线程的方法,也就是说start方法启动后创建的子线程就会和主线程并行,所以如果直接调run方法就不会启动子线程
如图 调用start方法的线程走势:start启动子线程,之后run方法也会被执行(是由子线程执行),此为异步执行
如图 直接调用run方法的线程走势:没有调用start方法启动子线程,所以还是主线程执行的run方法
-
第二种:实现Runnable的方式
1. 实现Runnable接口方式
public class Demo {
static class ChildRunable implements Runnable {//实现Runnable接口
@Override
public void run() { // 重写Thread中的run方法,
System.out.println(Thread.currentThread().getName()); //打印子线程名称
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new ChildRunable());//创建线程
t1.start();
System.out.println(Thread.currentThread().getName());//打印主线程名称
}
}
由上述代码所示,ChildThread类实现了Runnable接口并重写了run方法,run方法的内容是打印子线程名称,在主线程中依然是用new Thread创建了线程,不过其中的执行实例是ChildRunable类的实例,不过这里的run也是不可以直接调用的理由同上。
到这里可能会有人有疑问,为什么无论继承Thread还是实现Runnable都要重写run方法还有为什么创建线程都要用到Thread?
首先,无论是继承Thread还是Runnable都要重写run方法是因为Thread类是实现了Runnable接口的,在其中就重写了run方法,而这个run方发会去执行新建线程的执行实例的逻辑,如果像上面的ChildRunable 或ChildThread没有重写run方法,那么就等于是创建了空线程没有意义反而会占用cpu资源,看以下Thread源码就懂了,
public class Thread implements Runnable //这是Thread类头部,实现了Runnable接口public Thread() { //Thread的无参构造方法 init(null, null, "Thread-" + nextThreadNum(), 0); }public Thread(Runnable target) { //Thread的有参构造方法,上面用到的创建线程的方式就用的这个方法 init(null, target, "Thread-" + nextThreadNum(), 0); }
private Runnable target; // target可以理解为线程的执行实例 @Override public void run() { // 这是Thread类中重写的Runnable的run方法,是执行实例的具体逻辑 if (target != null) { // 线程的执行实力是否为空,这里指的是通过有参构造的Thread方法传的Runnable实例 target.run();// 调用实例中的run方法执行具体逻辑 } }
然后讲下为什么每次创建线程都需要用到Thread,要知道Thread才是java中创建线程的核心,而Runnable接口中只有一个run方法,作用就是在实现类中重写run方法编写线程的执行逻辑,通过上面的Thread的run方法的源码可见会先判断线程执行实例是否为空,不为空则直接调用实例的run方法执行,不过在执行之前需要先调用Thread的start方法,才能启动新建的线程,线程启动后才会去执行run方法。
2.匿名函数的方式和lamda表达式写法
//匿名类的写法
Thread threadA = new Thread(new Runnable() { // 调用了Thread的有参构造方法,参数是Runnable接口实例
@Override
public void run() { // 实现Runnable的run方法 target:线程执行实例
System.out.println("创建了一个线程;");
}
});
threadA.start();// 启动线程
//lamda表达式写法
Thread threadA = new Thread(() -> { // lamda表达式编写执行逻辑
System.out.println(Thread.currentThread().getName());//打印子线程名称
});
threadA.start(); //启动线程
System.out.println(Thread.currentThread().getName());//打印主线程名称
到这里先简单总结下Thread和Runnable,Thread和Runnable可以说是一个很好的组合(Thread中只支持Runnable的执行实例),由Thread创建线程和启动线程,由Runnable提供的run方法编写执行逻辑代码,但是Runnable的run方法是没有返回值的,那么这就导致线程异步执行的结果无法被获取,那这样的弊端如何消除呢,接着往下看。
-
第三种 Callable和FutureTask创建线程
1.上面已经介绍过Thread和Runnable创建线程的方式,但是他们都有一个缺陷,就是没有返回值,不能获取异步执行后的结果
我们先简单了解下Callable接口
(1)首先Callable接口,它是一个泛型接口,也是一个函数式接口,唯一的抽象方法call()有返回值,返回类型是Callable接口的泛型形参类型。而且call()方法有异常声明throw Exception,允许内部异常直接抛出。
@FunctionalInterface public interface Callable<V> { V call() throws Exception; // 返回值是泛型 }
虽然Callable有返回值,但是它并不能和Runnable实例一样,作为Thread线程实例的target来使用,因为Thread的target属性的类型是Runnable,而且Callable接口和Runnable之间没有任何继承关系。既然如此,那Callable接口怎么创建线程来执行呢?继续看下去
(2)RunnableFuture接口
这是一个在Callable和Thread之间起到衔接作用的接口,那具体是怎么衔接的呢,RunnableFuture接口实现了两个目标,既可以作为Thread线程实例的target,又可以获取异步执行结果。具体原因:
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
通过RunnableFuture源码可以看出:它继承了Runnable、Future接口,跟它的名字一样RunnableFuture,继承Runnable保证了可以作为Thread线程实例的target目标;继承Future接口,保证了可以获取异步执行结果。
在这里再给大家介绍下Future接口
(3)Future接口提供了这么几个功能
能够取消异步执行中的任务。
判断异步任务是否执行完成。
获取异步任务完成后的返回结果。
Future接口源码和主要方法详细说明:
public interface Future<V> {
//取消异步执行中的任务
boolean cancel(boolean mayInterruptIfRunning);
//获取异步任务的取消状态。如果任务完成前被取消,就返回true。
boolean isCancelled()+
//获取异步任务的执行状态。如果任务执行结束,就会返回true
boolean isDone();
//获取异步任务执行结果,这个方法时阻塞性的。如果任务没有执行完成,异步结果获取线程会一直被阻塞,直到异步任务执行完成,结果返回给调用线程
V get() throws InterruptedException, ExecutionException;
//设置时限,跟上面的get方法一样,只是有了等待时间,到时间就会抛出异常。
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
Future是一个对异步任务进行交互、操作的接口。但是它只仅仅是一个接口,没有实现类肯定是没有办法完成对异步任务的操作的,所以jdk提供了一个实现类——FutureTask。
(4)FutureTask类
FutureTask类是Future接口的实现类,提供了对异步任务操作的具体实现。但是FutureTask不仅实现了Future,还有RunnableFuture接口。
RunnableFuture接口既可以作为Thread线程实例的target目标,又可以获取并发任务执行的结果,是Thread和Callable之间一个非常重要的接口。但是RunnableFuture只是个接口,需要结合它的实现类FutureTask创建对象,也就是说真正衔接Callable和Thread的是FutureTask这个类。
Future类的继承关系图
FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承了Runnable和Future,所以FutureTask既能作为一个Runnable类型的执行目标直接被Thread执行,又能作为Future异步任务来获取Callable的计算结果。
FutureTask如何完成多线程的并发执行、任务结果的异步获取呢?FutureTask内部有一个Callable类型的成员callable实例属性,如下:
private Callable<V> callable;
callable实例属性来保存Callable类型的任务,并且callable实例属性需要在FutureTask实例构造时进行初始化。FutureTask实现了Runnable接口,在其run方法的实现中执行callable成员的call方法。
此外,FutureTask中内部还有一个非常重要的Object类型的成员outcome实例属性
private Object outcome;
这是用来保存callable成员call()方法的异步执行结果的。供FutureTask的get()方法获取
(5)使用Callable和FutureTask创建线程
public class Test {
//步骤1
static class TaskTest implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("执行call方法");
Thread.sleep(3000);
return "call";
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
//步骤2
TaskTest taskTest = new TaskTest();
//步骤3
FutureTask<String> futureTask = new FutureTask(taskTest);
//步骤4
Thread thread = new Thread(futureTask);
thread.start();
//步骤5
String s = futureTask.get();
System.out.println(s);
}
}
步骤1:先创建一个实现Callable接口的TaskTest类,并实现call方法编写具体逻辑。
步骤2:使用Callable实现类TaskTest构造一个实例
步骤3:使用FutureTask构造一个实例作为Thread构造函数的入参
步骤4:调用Thread的start方法启动线程,启动新线程的run方法并发执行。其内部的执行过程:启动Thread实例的run方法并发执行后,会执行FutureTask实例的run方法,最终会并发执行Callable实现类的call方法。
步骤5:调用FutureTask对象的get方法阻塞性地获得并发线程的执行结果
-
第四种:线程池创建线程
关于线程池的创建线程的方式,先给大家展示一个标准的创建方式。
创建一个继承ThreadPoolExecutor的类,属于自定义方式的线程池。
public class Test {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ThreadPoolTest threadPoolTest = new ThreadPoolTest();
//提交无返回值的任务,任务类型是Runnable
threadPoolTest.execute(new Runnable() {
@Override
public void run() {
System.out.println("无返回值的任务");
}
});
//提交有返回值的任务,任务类型是Callable
threadPoolTest.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "有返回值的任务";
}
});
}
//继承ThreadPoolExecutor类
static class ThreadPoolTest extends ThreadPoolExecutor {
public ThreadPoolTest() {
//构建线程池初始参数,
//从左到右:核心线程数、线程池线程数最大值、非核心线程的空闲时间、时间单位、任务队列类型ArrayBlockingQueue是数组类型的
super(5, 10, 5, TimeUnit.MINUTES, new ArrayBlockingQueue(100));
}
//重写submit方法,有返回值的任务提交时用的方法
@Override
public Future<?> submit(Runnable task) {
return super.submit(task);
}
//重写executor方法,无返回值的任务提交时用的方法
@Override
public void execute(Runnable command) {
super.execute(command);
}
}
}
后续会就线程池单独写一篇详细的文章,谢谢观赏!