世界热头条丨java实现多线程的几种方式

时间:2023-01-04 16:18:41       来源:PHP中文网

本教程操作环境:windows7系统、java8版、DELL G3电脑。

多线程的形式上实现方式主要有两种,一种是继承Thread类,一种是实现Runnable接口。本质上实现方式都是来实现线程任务,然后启动线程执行线程任务(这里的线程任务实际上就是run方法)。这里所说的6种,实际上都是在以上两种的基础上的一些变形。


(相关资料图)

下面分别就这6中实现方式一一介绍。

第一种方式:继承Thread类

万物皆对象,那么线程也是对象,对象就应该能够抽取其公共特性封装成为类,使用类可以实例化多个对象,那么实现线程的第一种方式就是继承Thread类。继承Thread类是最简单的一种实现线程的方式,通过JDK提供的Thread类,重写Thread类的run方法即可,那么当线程启动的时候,就会执行run方法体的内容。代码如下:

package com.kingh.thread.create;/** * 继承Thread类的方式创建线程 * * @author 登录后复制

运行结果如下

当前运行的线程名为: main当前运行的线程名为: MyThread当前运行的线程名为: main当前运行的线程名为: MyThread当前运行的线程名为: MyThread当前运行的线程名为: main
登录后复制

1. 创建多个线程

上面的例子中除了我们创建的一个线程以外其实还有一个主线程也在执行。那么除了这两个线程以外还有没有其他的线程在执行了呢,其实是有的,比如我们看不到的垃圾回收线程,也在默默的执行。这里我们并不去考虑有多少个线程在执行,上面我们自己创建了一个线程,那么能不能多创建几个一起执行呢,答案是肯定的。一个Thread类就是一个线程对象,那么多创建几个Thread类,并调用其start方法就可以启动多个线程了。代码如下

package com.kingh.thread.create;/** * 创建多个线程同时执行 * * @author 登录后复制

运行结果如下

当前运行的线程名为: main当前运行的线程名为: MyThread-02当前运行的线程名为: MyThread-01当前运行的线程名为: main当前运行的线程名为: MyThread-01当前运行的线程名为: MyThread-02当前运行的线程名为: main
登录后复制

2. 指定线程名称

可以看到,通过创建多个Thread类,并且调用其start方法,启动了多个线程。每个线程都有自己的名字,在上述代码中,分别给创建的线程指定了MyThread-01和MyThread-02这个名字,然后构造方法中通过调用父类的setName方法给线程名字赋值。如果不指定线程名字,系统会默认指定线程名,命名规则是Thread-N的形式。但是为了排查问题方便,建议在创建线程的时候指定一个合理的线程名字。下面的代码是不使用线程名的样子

package com.kingh.thread.create;/** * 创建多个线程同时执行,使用系统默认线程名 * * @author 登录后复制

运行的结果如下:

当前运行的线程名为: main当前运行的线程名为: Thread-1当前运行的线程名为: Thread-0当前运行的线程名为: main当前运行的线程名为: Thread-1当前运行的线程名为: Thread-0
登录后复制

第二种方式:实现Runnable接口

实现Runnable接口也是一种常见的创建线程的方式,使用接口的方式可以让我们的程序降低耦合度。Runnable接口中仅仅定义了一个方法,就是run。我们来看一下Runnable接口的代码。

package java.lang;@FunctionalInterfacepublic interface Runnable {    public abstract void run();}
登录后复制

其实Runnable就是一个线程任务,线程任务和线程的控制分离,这也就是上面所说的解耦。我们要实现一个线程,可以借助Thread类,Thread类要执行的任务就可以由实现了Runnable接口的类来处理。这就是Runnable的精髓之所在!

使用Runnable实现上面的例子步骤如下:

定义一个类实现Runnable接口,作为线程任务类重写run方法,并实现方法体,方法体的代码就是线程所执行的代码定义一个可以运行的类,并在main方法中创建线程任务类创建Thread类,并将线程任务类做为Thread类的构造方法传入启动线程

1. 创建线程任务

线程任务就是线程要做的事情,这里我们让这个线程每隔1s中打印自己的名字

package com.kingh.thread.create;/** * 线程任务 * * @author 登录后复制

2. 创建可运行类

在这里创建线程,并把任务交给线程处理,然后启动线程。

package com.kingh.thread.create;/** * 创建线程 * * @author 登录后复制

3. lambda方式创建线程任务

这里就是为了简化内部类的编写,简化了大量的模板代码,显得更加简洁。如果读者看不明白,可以读完内部类方式之后,回过来再看这段代码。

package com.kingh.thread.create;/** * 创建线程with lambda * * @author 登录后复制

第三种方式:使用内部类的方式

这并不是一种新的实现线程的方式,只是另外的一种写法。比如有些情况我们的线程就想执行一次,以后就用不到了。那么像上面两种方式(继承Thread类和实现Runnable接口)都还要再定义一个类,显得比较麻烦,我们就可以通过匿名内部类的方式来实现。使用内部类实现依然有两种,分别是继承Thread类和实现Runnable接口。代码如下:

package com.kingh.thread.create;/** * 匿名内部类的方式创建线程 * * @author 登录后复制
package com.kingh.thread.create;/** * 匿名内部类的方式创建线程 * * @author 登录后复制

运行结果如下:

sub classsub class
登录后复制

我们可以看到,其实是基于子类的执行了,为什么呢,其实很简单,我们先来看一下为什么不基于子类的时候Runnable的run方法可以执行。这个要从Thread的源码看起,下面是我截取的代码片段。

public Thread(Runnable target)    init(null, target, "Thread-" + nextThreadNum(), 0);}private void init(ThreadGroup g, Runnable target, String name,                  long stackSize) {    init(g, target, name, stackSize, null, true);}private void init(ThreadGroup g, Runnable target, String name,                  long stackSize, AccessControlContext acc,                  boolean inheritThreadLocals) {    if (name == null) {        throw new NullPointerException("name cannot be null");    }    this.name = name;    Thread parent = currentThread();    SecurityManager security = System.getSecurityManager();    if (g == null) {        /* Determine if it"s an applet or not */        /* If there is a security manager, ask the security manager               what to do. */        if (security != null) {            g = security.getThreadGroup();        }        /* If the security doesn"t have a strong opinion of the matter               use the parent thread group. */        if (g == null) {            g = parent.getThreadGroup();        }    }    /* checkAccess regardless of whether or not threadgroup is           explicitly passed in. */    g.checkAccess();    /*         * Do we have the required permissions?         */    if (security != null) {        if (isCCLOverridden(getClass())) {            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);        }    }    g.addUnstarted();    this.group = g;    this.daemon = parent.isDaemon();    this.priority = parent.getPriority();    if (security == null || isCCLOverridden(parent.getClass()))        this.contextClassLoader = parent.getContextClassLoader();    else        this.contextClassLoader = parent.contextClassLoader;    this.inheritedAccessControlContext =        acc != null ? acc : AccessController.getContext();    this.target = target; // 注意这里    setPriority(priority);    if (inheritThreadLocals && parent.inheritableThreadLocals != null)        this.inheritableThreadLocals =        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);    /* Stash the specified stack size in case the VM cares */    this.stackSize = stackSize;    /* Set thread ID */    tid = nextThreadID();}
登录后复制

其实上面的众多代码就是为了表现 this.target = target 那么target是什么呢,是Thread类的成员变量。那么在什么地方用到了target呢?下面是run方法的内容。

@Overridepublic void run() {    if (target != null) {        target.run();    }}
登录后复制

我们可以看到,如果通过上面的构造方法传入target,那么就会执行target中的run方法。可能有朋友就会问了,我们同时继承Thread类和实现Runnable接口,target不为空,那么为何不执行target的run呢。不要忘记了,我们在子类中已经重写了Thread类的run方法,因此run方法已经不在是我们看到的这样了。那当然也就不回执行target的run方法。

lambda 方式改造

刚才使用匿名内部类,会发现代码还是比较冗余的,lambda可以大大简化代码的编写。用lambda来改写上面的基于接口的形式的代码,如下

// 使用lambda的形式new Thread(() -> {    while (true) {        printThreadInfo();    }}).start();// 对比不使用lambda的形式new Thread(new Runnable() {    @Override    public void run() {        while (true) {            printThreadInfo();        }    }}).start();
登录后复制

第四种方式:定时器

定时器可以说是一种基于线程的一个工具类,可以定时的来执行某个任务。在应用中经常需要定期执行一些操作,比如要在凌晨的时候汇总一些数据,比如要每隔10分钟抓取一次某个网站上的数据等等,总之计时器无处不在。

在Java中实现定时任务有很多种方式,JDK提供了Timer类来帮助开发者创建定时任务,另外也有很多的第三方框架提供了对定时任务的支持,比如Spring的schedule以及著名的quartz等等。因为Spring和quartz实现都比较重,依赖其他的包,上手稍微有些难度,不在本篇博客的讨论范围之内,这里就看一下JDK所给我们提供的API来实现定时任务。

1. 指定时间点执行

package com.kingh.thread.create;import java.text.SimpleDateFormat;import java.util.Timer;import java.util.TimerTask;/** * 定时任务 * * @author 登录后复制

2.间隔时间重复执行

package com.kingh.thread.create;import java.text.SimpleDateFormat;import java.util.Date;import java.util.Timer;import java.util.TimerTask;/** * 定时任务 * * @author 登录后复制

第五种方式:带返回值的线程实现方式

我们发现上面提到的不管是继承Thread类还是实现Runnable接口,发现有两个问题,第一个是无法抛出更多的异常,第二个是线程执行完毕之后并无法获得线程的返回值。那么下面的这种实现方式就可以完成我们的需求。这种方式的实现就是我们后面要详细介绍的Future模式,只是在jdk5的时候,官方给我们提供了可用的API,我们可以直接使用。但是使用这种方式创建线程比上面两种方式要复杂一些,步骤如下。

创建一个类实现Callable接口,实现call方法。这个接口类似于Runnable接口,但比Runnable接口更加强大,增加了异常和返回值。

创建一个FutureTask,指定Callable对象,做为线程任务。

创建线程,指定线程任务。

启动线程

代码如下:

package com.kingh.thread.create;import java.util.concurrent.Callable;import java.util.concurrent.FutureTask;/** * 带返回值的方式 * * @author  task = new FutureTask<>(call);        // 开启线程,执行线程任务        new Thread(task).start();        // ====================        // 这里是在线程启动之后,线程结果返回之前        System.out.println("这里可以为所欲为....");        // ====================        // 为所欲为完毕之后,拿到线程的执行结果        Integer result = task.get();        System.out.println("主线程中拿到异步任务执行的结果为:" + result);    }}
登录后复制

执行结果如下:

这里可以为所欲为....线程任务开始执行了....主线程中拿到异步任务执行的结果为:1
登录后复制

第六种方式:基于线程池的方式

我们知道,线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。当然了,线程池也不需要我们来实现,jdk的官方也给我们提供了API。

代码如下:

package com.kingh.thread.create;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/** * 线程池 * * @author 登录后复制

执行结果如下:

当前运行的线程名为: pool-1-thread-1当前运行的线程名为: pool-1-thread-2当前运行的线程名为: pool-1-thread-4当前运行的线程名为: pool-1-thread-3当前运行的线程名为: pool-1-thread-7当前运行的线程名为: pool-1-thread-8当前运行的线程名为: pool-1-thread-9当前运行的线程名为: pool-1-thread-6当前运行的线程名为: pool-1-thread-5当前运行的线程名为: pool-1-thread-10
登录后复制

线程池的内容还有非常多,这里不再详细地讲解。

更多编程相关知识,请访问:编程教学!!

以上就是java实现多线程的几种方式的详细内容,更多请关注php中文网其它相关文章!

关键词: 定时任务 启动线程