前言
本篇文章是多线程系列的第一篇,主要讲解一些基础知识:包括线程是什么、为什么要使用多线程、怎样使用。
正文
正文之前
首先我们要知道,并不是所有的语言都支持多线程技术。比如:在C++ 11以前,它是没有内置的多线程机制的,因此它必须调用操作系统的多线程功能来进行多线程程序设计(详情可参考链接:http://www.cplusplus.com/reference/thread/thread/?kw=thread)。而Java从出生起就提供了多线程支持。
什么是线程?多线程又是什么?多线程有什么好处和弊端?
在了解"线程"之前,我们必须先了解"进程"。那么进程又是什么呢?顾名思义,进程就是正在进行中的程序。其实经常使用电脑的人都知道"任务管理器"这个东西:
-
这里面的内容就是正在运行中的程序。我们知道,一个程序启动之后就会在内存中开辟空间(因为程序的运行要耗费资源),那么进程其实就是系统进行资源分配和调度的基本单位,线程就是具体负责执行程序中内容的一个执行单元或者说是执行路径。
-
一个程序可以有多个执行单元(eg:360可以同时木马查杀、电脑清理和系统修复),那么也就是说一个进程可以有多个线程,但是不能没有线程(如果没有线程执行程序中的内容,程序就运行不起来)。一个进程可以有多个线程就称之为多线程。
-
通过上面的说明我们可以看出:当我们想要同时执行多部分代码时就可以使用多线程。每一个线程都有自己要执行的内容,这个内容就可以称为线程要执行的任务。
-
多线程技术有很多好处:可以减少程序的响应时间:可将耗时操作单独分配到一个线程去执行,使程序具备更好的交互性;线程的创建和切换开销较小,同时多线程在数据共享上效率更高;在多CPU计算机上使用多线程能提高CPU的利用率;可以简化程序结构,使程序便于理解和维护:一个非常复杂的进程可以分成多个线程来执行
-
我们要注意一个问题:多线程的多个线程真的是在同时运行吗?其实不是的,我们的眼睛有时候欺骗了我们。我们在Windows系统上可以同时运行多个进程(eg:听歌的同时在聊QQ还在给电脑体检),而每个进程都好像在独占地使用系统资源。实际上一个进程和另一个进程是交错执行的,只不过CPU切换的速度相当快,我们根本感觉不到切换,所以我们以为它们是在同时运行。具体来说,CPU是在线程之间进行来回的随机切换。简单来说,在某一个时刻,内存中只有一个线程在执行。多线程的弊端也正在于此,如果内存中同时执行的程序多了之后,CPU切换繁忙就有可能导致宕机。
JVM中的多线程?
说完了一些基础概念之后,我们心中不免会有一个疑惑:Java程序的执行是多线程的吗?我们可以通过分析如下的程序来得到答案:
class Demo
{
}
class ThreadDemo
{
public static void main(String[] args)
{
new Demo();
new Demo();
new Demo();
System.out.println("Hello");
}
}
在上面的程序中我们创建了3个匿名对象,由于匿名对象创建完成后就变成了垃圾,而main函数还需要继续向下执行,所以我们至少可以分析出两条线程:执行main函数的线程和进行垃圾回收的线程。于是我们可以知道:Java程序的执行是多线程的。其实不仅如此,我们上面说过:Java语言是支持多线程技术的语言之一,Java虚拟机的启动本身就依赖了多条线程。我们可以进一步做实验:
class Demo extends Object
{
public void finalize()
{
System.out.println("demo ok");
}
}
class ThreadDemo
{
public static void main(String[] args)
{
new Demo();
new Demo();
new Demo();
System.gc();
System.out.println("Hello World!");
}
}
注:上面代码的运行结果有多种可能。
通过上面多种的运行结果我们可以知道:运行main函数的线程和执行垃圾回收的线程是两条单独的线程,并且main函数执行完毕并不代表着Java虚拟机也结束了。
线程创建的几种方式?
上面说过:当我们想要同时执行多部分代码时就可以使用多线程,那么这样的需求是怎样的呢?
class Demo
{
private String name;
Demo(String name)
{
this.name = name;
}
public void show()
{
for(int x = 0; x < 10; x++)
{
for(int y = -999999999; y < 999999999; y++){}
System.out.println(name + "... ...x=" + x);
}
}
}
class ThreadDemo2
{
public static void main(String[] args)
{
Demo d1 = new Demo("旺财");
Demo d2 = new Demo("xiaoqiang");
d1.show();
d2.show();
}
}
通过上面代码的运行结果我们可以看出:show()中有一个很长的循环并且xiaoqiang只能等待旺财执行完毕后才能开始执行,这样做明显效率就很低。我们希望在xiaoqiang执行循环时,旺财也能执行自己的代码。这个时候我们就可以创建多个线程来执行不同的任务。那么在Java中,具体是怎样创建线程的呢?我们可以通过查看JDK的文档得知(下面的具体步骤均来自于JDK文档):
继承Thread类
- 具体步骤:将一个类声明为Thread的子类。该子类应重写Thread类的run方法。然后可以分配并启动子类的实例。示例代码:例如,计算大于规定值的素数的线程可以写成如下:
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
}
}
// 然后,以下代码将创建一个线程并启动它:
PrimeThread p = new PrimeThread(143);
p.start();
- 知道了怎样通过继承Thread类创建线程之后,我们不免疑惑:为什么要重写run()呢?
我们上面说过:线程被创建出来就是为了执行任务的,这些任务其实就被封装到了Thread类的run()中。所以当我们自定义线程时需要重写run()封装自定义的任务。于是上面"旺财"的例子就可以这样进行改写:
class Demo extends Thread
{
private String name;
Demo(String name)
{
super(name);
}
public void run()
{
for(int x = 0; x < 10; x++)
{
for(int y = -999999999; y < 999999999; y++){}
System.out.println(name + "...x=" + x + "...name=");
}
}
}
class ThreadDemo2
{
public static void main(String[] args)
{
Demo d1 = new Demo("旺财");
Demo d2 = new Demo("xiaoqiang");
d1.start();
d2.start();
System.out.println("over....");
}
}
-
当在程序中创建了多条线程之后,我们如何判断正在执行的线程是哪一个呢?可以查看JDK文档:我们发现了getName()就可以获取到线程的名称。需要注意的是线程的名称在线程一创建就会分配(具体细节可以参考Thread类的源码)。于是我们可以通过"Thread.currentThread().getName()"获取到当前正在执行线程的名称。Thread类自动分配的名称有时候并不直观,但是Thread类提供了带线程名称的构造函数,所以我们在创建线程对象时可以自定义线程名称。
-
上面旺财例子中的三条线程执行的顺序是随机的,那么这些方法在栈区中的空间分配是怎样的呢:
- 线程的状态:
-
那么run()和start()有什么区别呢?若直接调用线程类的run(),这会被当作一个普通的函数调用,程序中仍然只有主线程这一个线程;若直接调用线程类的start(),此时该线程处于就绪状态并可被JVM调度执行。综上,只有通过调用线程类的start()才能真正达到多线程的目的。
-
使用继承方式的好处是:在run()中获取当前线程直接使用this就可以了,无需使用Thread.currentThread()。
实现Runnable接口
-
我们注意到第一种线程创建方法的局限:如果该类已经继承了其他的类就无法再继承Thread类了。鉴于此,创建线程还有第二种方法:
-
具体步骤:声明实现Runnable接口的类。该类然后实现run方法。然后可以分配该类的实例,在创建Thread时作为一个参数来传递并启动。示例代码:与上面相同的素数例子可以写成如下:
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
}
}
// 然后,以下代码将创建一个线程并启动它:
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
- 于是上面"旺财"的例子就还这样进行改写:
class Demo implements Runnable
{
public void run()
{
for(int x = 0; x < 10; x++)
{
for(int y = -999999999; y < 999999999; y++){}
System.out.println(Thread.currentThread().getName() +"... ..." + x);
}
}
}
class ThreadDemo {
public static void main(String[] args) {
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();
}
}
- 我们注意到下面代码:
class Demo implements Runnable {
public void run() {
System.out.println("demo run...");
}
}
Thread t1 = new Thread();
t1.start(); // 什么都不输出
Demo d = new Demo();
Thread t2 = new Thread(d);
t1.start(); // 输出demo run...
其实Thread类中持有了一个Runnable的引用,当向Thread中传入d后,调用的就是d中的run(),如果什么都不传入,那么就什么都不做。这就有点类似于一个类的set方法。
- 我们可以注意到:Runnable接口中只有一个run(),它将线程的任务从线程的子类中分离出来并且按照面向对象的思想进行了单独的封装。而继承Thread类时任务和代码没有分离。同时此种创建线程的方式还避免了单继承的局限,所以创建线程的第二种方式较为常用。
实现Callable接口
上面两种方式都存在一个缺点:那就是任务没有返回值,如果我们需要对任务的返回结果进行操作,就可以使用下面这种方式创建线程:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
// 创建任务类,类似Runnable
public class Test implements Callable<String> {
public String call() throws Exception {
return "hello";
}
public static void main(String[] args) throws InterruptedException {
// 创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new Test());
new Thread(futureTask).start();
try {
// 等待任务执行完毕,并返回结果
String result = futureTask.get();
System.out.println(result);
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}