13.2 线程的创建与启动

一、继承Thread类创建线程类

通过Thread类来创建并启动多线程的步骤如下:
1、定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表线程需要完成的任务。因此把run()方法称为线程执行体。
2、创建Thread子类的实例,即创建线程对象。
3、调用线程对象的start()方法来启动该线程。

package section2;

public class FirstThread extends Thread
{
    private int i;
    //重写run()方法,run()方法的方法体就是线程执行体
    @Override
    public void run()
    {
        for(;i<100;i++)
        {
            //当线程继承Thread类时,直接使用this即可获取当前线程
            //Thread对象的getName()返回当前线程的名字
            //因此可以直接调用getName()方法返回当前线程的名字
            System.out.println(getName()+" "+i);
        }
    }
    public static void main(String[] args)
    {
        for(var i=0;i<100;i++)
        {
            //调用Thread的currentThread()方法获取当前线程
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i==20)
            {
                //创建并启动第一个线程
                new FirstThread().start();
                //常见并启动第二个线程
                new FirstThread().start();
            }
        }
    }
}

上面FirstThread类继承了Thread类,并实现了run()方法,该run()方法里的代码执行流就是该线程所需要完成的任务。程序的主方法中也包括一个循环,当循环变量i等于20时创建并启动两个新线程。运行上面的程序将可以看到如下图所示的输出。

上面程序显示创建了2个线程,但还含有一个主线程,主线程的执行体不是由run()方法确定的——main()方法的方法体代表主线程的执行体。
上面程序用到了线程的两个方法:
1、Thread.currentThread():currentThread()是Thread类的静态方法,该方法总是返回正在执行的线程对象。
2、getName():该方法时Thread类的实例方法,该方法返回调用该方法的线程名字。
注意:程序可以通过setName(String name)方法为线程设置名称,也可以通过getName()方法返回指定线程的名字。在默认情况下,主线程名字为main,用户启动的多个线程的名字依次为Thread-0、Thread-1...Thread-n等
从上面的输出结果可以看出,Thread-0和Thread-1两个线程输出的i变量不连续——注意:i变量是FirstThread的实例变量,而不是局部变量,但因为程序每次创建线程对象时都需要创建一个FirstThread对象,所以Thread-0和Thread-1不能共享该实例变量。

二、实现Runnable接口创建线程类

实现Runnable接口来创建并启动多线程的步骤如下:
1、定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
2、创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。代码如下:

//创建Runnable实现类对象
var st=new SecondThread();
//以Runnable实现类对象作为Thread的target创建Thread对象,即线程对象
new Thread(st);

也可以在创建Thread对象时为Thread对象指定一个名字,代码如下:

new Thread(st,"新线程1");

提示:Runnale对象不仅作为Thread对象的Target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。
3、调用线程对象的start()方法来启动该线程。
下面程序示范了通过Runnable接口来创建并启动多线程

package section2;

public class SecondThread implements Runnable
{
    private int i;

    //run()方法同样是线程的执行体
    @Override
    public void run()
    {
        for(;i<100;i++)
        {
            //因为getName()是Thread类的方法,当线程类实现Runnable接口时
            //如果想获取当前线程对象,只能用Thread.currentThread()
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
    public static void main(String[] args)
    {
        for(var i=0;i<100;i++)
        {
            System.out.println(Thread.currentThread().getName());
            if(i==20)
            {
                var st=new SecondThread();//①
                //通过Thread(target,name)方法创建新线程
                new Thread(st,"新线程1").start();
                new Thread(st,"新线程2").start();
            }
        }
    }
}

对比FirstThread中的run()方法体和SecondThread中的run()方法体不难发现,通过Thread类来获取当前线程对象比较简单,直接使用this.getName即可;但是实现Runnable接口来获取当前线程对象,则必须使用Thread.currentThread()方法。
注意:Runnable接口只有一个抽象方法,从Java8开始,Runnable接口使用@FunctionalInterface修饰。
在FirstThread和SecondThread中创建线程对象的方式有所区别:前者直接创建Thread子类的实例即可代表线程对象;后者创建Runnable对象只能作为线程对象的target。
运行上面程序可以看到如下所示输出:

从上面输出结果可以看出两个子线程的i变量是连续的,也就是采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量。这是因为在这种方式下,程序创建的Runnable对象只是线程的target,而多个线程可以共享同一个线程类(实际上应该是线程的target类)的实例变量。

三、使用Callable和Future创建线程

通过实现Runnable接口创建多线程,Thread类的作用就是把run()方法包装成线程执行体。
Java 5开始,Java提供了Callable()接口,该接口怎么看就像是Runnable的增强版,Callable接口提供了一个call()方法作为线程执行体,但call()方法比run()方法功能更强大:
★call()方法可以有返回值。
★call()可以声明抛出异常
因此完全可以提供一个Callable对象作为Thread的target,而该线程的执行体就是Callable对象的call()方法。问题是:Callable接口时Java 5新增的,而且他不是Runnable接口的子接口,所以Callable接口对象不能作为Thread的target。而且call()方法还有一个返回值——call()方法并不是直接调用,他作为线程的执行体被调用。
Java 5提供了Future接口来代表Callable接口里的call()方法的返回值,并为Future接口提供一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口——可以作为Thread类的target。
在Future接口里定义了如下几个公共方法来控制它关联的Callable任务。
1、boolean cancel(boolean mayInterruptIfRunning):试图取消该Future里关联的Callable任务。
2、V get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值。
3、V get(long timeout,TimeUnit unit):返回Callable任务中call()方法的返回值。该方法让程序最多阻塞timeout和unit指定时间,如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException。
4、boolean isCancelled():如果Callable任务正常被取消,则返回true。
5、boolean isDone():如果Callable任务以完成,则返回true。
Callable接口有泛型限制,Callable接口里的泛型参数类型与call()方法返回值类型相同。而且Callable接口是函数式接口,因此可以使用Lambda表达式创建Callable对象。
创建并启动有返回值的线程的步骤:
(1)定义Callable接口的实现类,并重写该接口的call方法,该call方法的方法体同样是该线程的线程执行体。
(2)使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动线程。
(4)调用FutureTask对象的get()方法来获得子线程结束后的返回值。
下面程序示范了实现Callable接口来实现线程类,并启动该线程:

package section2;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThirdThread
{
    public static void main(String[] args)
    {
        //创建Callable对象
        var rt=new ThirdThread();
        //使用Lambda表达式创建Callable<Integer>对象
        //使用FutrueTask来包装Callable对象
        FutureTask<Integer> task=new FutureTask<>((Callable<Integer>)()->
        {
            var i=0;
            for(;i<100;i++)
            {
                System.out.println(Thread.currentThread().getName()+"循环变量i的值"+i);
            }
            //call方法可以有返回值
            return i;
        });
        for(var i=0;i<100;i++)
        {
            System.out.println(Thread.currentThread().getName()+"循环变量i的值:"+i);
            if(i==20)
            {
                //实质还是以Callable对象来创建并启动线程的
                new Thread(task,"有返回值的线程").start();
            }
        }
        try {
            //获取线程返回值
            System.out.println("子线程的返回值:" + task.get());
        }
        catch(Exception ex)
        {
            ex.printStackTrace();
        }

    }
}

上面程序使用了Lambda表达式直接创建Callable对象,这样无须先创建Callable实现类,再创建Callable对象了。实现Callable对象和实现Runnable接口并没有什么太大的区别,只是Callable的call()方法允许声明抛出异常,而且允许带有返回值。
上面程序先使用Lambda表达式创建了一个Callable对象,然后将该实例包装成一个FutrueTask对象。当主线程中循环遍历i等于20时,程序启动以FutureTask对象为Target的线程。程序最后调用FutureTask对象的get()犯法来返回call()方法的返回值——该方法将导致主线程被阻塞,直到call()方法结束并返回为止。
运行上面的程序,将看到主线程和call()方法所代表的线程交替执行的情形,程序最后还会输出call()方法的返回值。

四、创建线程的三种方式对比

通过继承Thread类或实现Runnable、Callable接口的方式都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常。因此可以将实现Runnable接口、实现Callable接口归为一种方式。

4.1 实现Runnable接口、实现Callable接口创建多线程的优缺点:

(1)线程类只是实现Runnable接口、实现Callable接口,还可以继承其他类
(2)在这种方式下,多个线程可以共享同一个Target对象,所以非常适合多个线程处理同一份资源的情况,从而可以将CPU、代码、数据分开,形成清晰模型,较好体现了面向对象的思想。
(3)劣势是,编程稍微复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。

4.2 继承Thread类的方式创建多线程的优缺点

(1)劣势是,因为线程类已经继承了Thread类,所以不能再继承其他父类。
(2)优势是,编程简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this.getName()即可获得当前线程。
一般推荐使用实现Runnable接口、Callable接口的方式来创建多线程。

posted @ 2020-05-10 15:33  小新和风间  阅读(206)  评论(0编辑  收藏  举报