线程入门


十月的第四周,学习多线程。

借着工作需求入门了线程,但是需求并不用考虑并发风险,使用的也是封装好的对象,因此这次入门入得比较浅。

这篇博文偏实用,有关概念解释的部分就略过不提了。


原始的创建线程的方法有三种,分别是:

  1. 继承 Thread 类
  2. 实现 Runnable 接口
  3. 实现 Callable 接口

注意,这里说的是创建线程,因此只总结怎么【搭架子】,不总结怎么【使用线程】。

这一部分基本全部参考博客《CS-Notes》对于 Java 并发的梳理,这个博客的内容非常优质(不只是 Java 多线程与并发的部分),十分推荐。

继承 Thread 类

这是一种最为原始的方式,继承 Thread 类,重写 run 方法以实现线程功能。

1
2
3
4
5
6
public class PzThread extends Thread {
@Override
public void run(){
System.out.println("hello,pz.");
}
}

上面的代码是指,我创建了一个名为 PzThread 的类,该类由于继承 Thread 类因而是一个线程类,它重写了父类的 run 方法,打印了一句话:【hello,pz.】。

使用时也异常简单:

1
2
Thread thread = new PzThread();
thread.start();

首先实例化一个线程出来,然后启动线程,当线程启动之后,控制台上打印了一句话:【hello,pz.】。


一般情况下不要使用这种方式来创建线程,因为通过继承来实现实在是太臃肿了,很多场景下我们只需要让线程跑起来,实现某个功能(即重写 run 方法),但是继承会实现 Thread 类的全部信息,性能消耗太大。而且 Java 是单继承的,继承了 Thread 类就不能继承其他类了。


实现 Runnable 接口

Runnable 接口是一切线程创建的根源,其实上面【继承 Thread 类】的途径,也是间接使用了本途径来创建线程的。


比较传统的实现 Runnable 接口的方式是,创建一个类,该类 implements Runnable 来实现 Runnable 接口。

1
2
3
public class PzRunnable implements Runnable {
// ...
}

但是创建一个类,未免太大张旗鼓了些,还要新建一个类,设置好类名,实现接口,之后再实例化,兴师动众。其实实例化对象并不需要创建一个类出来,实现接口就行,用匿名内部类。

1
2
3
Runnable pzRunnable = new Runnable() {
// ...
};

此外,Runnable 接口是一个函数式接口,只定义了 run 方法,可以使用 lambda 表达式的方式来实例化,那就更简单了。

1
2
3
Runnable pzRunnable = () -> {
// ...
};

上面三种实现,都只是写了外壳,里面没有写具体的实现过程,具体的实现是要重写 Runnable 接口的 run 方法的。

我写了三种实现 Runnable 接口的代码,第一种最容易懂,后面两种如果有困惑,看一看 lambda 表达式就能理解了。


实现了 Runnable 接口之后,把它作为参数,放进 Thread 的构造方法里就可以了。

1
2
Thread thread = new Thread(runnable);
thread.start();

这样就可以了。


要不再完整地走一遍?

1
2
3
4
5
6
7
8
9
10
11
12
13
// 实现 Runnable 接口,重写 run 方法 (这里使用匿名内部类的方式,即上面的第二种)
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("by runnable");
}
};

// 创建线程并开启
Thread thread = new Thread(runnable);
thread.start();

// 控制台上会打印出这样一句话:by runnable

实现 Callable 接口

以上两种方式,都没有任何的返回值,线程执行动作,执行完就结束了,无声无息。实现 Callable 接口的目的,就是为了让线程执行完之后,能返回信息。

简单对比一下,Runnable 接口和 Callable 接口,在代码上的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 实现 Runnable 接口
Runnable runnable = new Runnable() {
@Override
public void run() {
// ...
}
};

// 实现 Callable 接口
Callable callable = new Callable() {
@Override
public Object call() throws Exception {
// ...
return null;
}
};

你会发现,实现两个接口都只需要重写一个方法:

  • 实现 Runnable 接口需要重写【没有返回值】的 run 方法
  • 实现 Callable 接口需要实现【返回一个对象】的 call 方法。

其他的地方,在用法上仿佛没有什么不同。

实际上,Callable 接口还支持泛型,你可以指定返回值的数据类型:

1
2
3
4
5
6
7
8
// 指定返回 String 类型
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
// ...
return null;
}
};

传统的线程设计,是没有返回值的概念的,因此没办法用线程类来获得返回值。JUC 包设计了一个新的接口:Future,来接收线程的返回值(和其他的功能)。Future 类是一个接口,无法直接实例化,因此又设计了一个名为 FutureTask 的类,该类实现了Future 接口和 Runnable 接口,打通了【线程功能】和【返回值功能】。

1
2
FutureTask futureTask = new FutureTask(callable);
Thread thread = new Thread(futureTask);

上面这两行代码,是将刚才写好的 callable 对象,放进 futureTask 中,辗转放进线程中。你可以感受到,FutureTask 类是一个中介,它也支持泛型(不过上面这两行代码没写泛型)。

FutureTask 类有一个 get 方法,用于获取 callable 的返回值。

1
Object returnStr = futureTask.get();

(如果指定了 FutureTask 的泛型,上面还可以更确切地指定数据类型,例如把上面代码的 Object 改成 String)

这个 get 方法需要处理两类异常:InterruptedException 和 ExecutionException。


需求一不小心写完了……啥都没学到,算了先写到这里叭。