• 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接口的方式来创建多线程。

  • 相关阅读:
    实例讲解虚拟机3种网络模式(桥接、nat、Host-only)
    期刊搜索问题——SCI、EI、IEEE和中文期刊
    面向对象分析与设计(C++)课堂笔记
    windows与VMware ubuntu虚拟机实现文件共享
    GDI+在绘制验证码中的使用
    【转】.net中快捷键的使用
    MD5加密“破解”在.NET平台实现最基本的理解
    UE4 Persona 骨架网格物体动画
    从零开始做3D地图编辑器 基于QT与OGRE
    TBB(Intel Threading Building Blocks)学习笔记
  • 原文地址:https://www.cnblogs.com/weststar/p/12861124.html
Copyright © 2020-2023  润新知