实现多线程的几种方式

版权声明:本文为博主原创文章,未经博主容许不得转载。 https://blog.csdn.net/king_kgh/article/details/78213576

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

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

第一种方式:继承Thread类

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

package com.kingh.thread.create;

/** * 继承Thread类的方式建立线程 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/13 19:19 */
public class CreateThreadDemo1 extends Thread {

    public CreateThreadDemo1() {
        // 设置当前线程的名字
        this.setName("MyThread");
    }

    @Override
    public void run() {
        // 每隔1s中输出一次当前线程的名字
        while (true) {
            // 输出线程的名字,与主线程名称相区分
            printThreadInfo();
            try {
                // 线程休眠一秒
                Thread.sleep(1000);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        // 注意这里,要调用start方法才能启动线程,不能调用run方法
        new CreateThreadDemo1().start();

        // 演示主线程继续向下执行
        while (true) {
            printThreadInfo();
            Thread.sleep(1000);
        }
    }

    /** * 输出当前线程的信息 */
    private static void printThreadInfo() {
        System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
    }
}

运行结果以下缓存

当前运行的线程名为: main
当前运行的线程名为: MyThread
当前运行的线程名为: main
当前运行的线程名为: MyThread
当前运行的线程名为: MyThread
当前运行的线程名为: main

这里要注意,在启动线程的时候,并非调用线程类的run方法,而是调用了线程类的start方法。那么咱们能不能调用run方法呢?答案是确定的,由于run方法是一个public声明的方法,所以咱们是能够调用的,可是若是咱们调用了run方法,那么这个方法将会做为一个普通的方法被调用,并不会开启线程。这里其实是采用了设计模式中的模板方法模式,Thread类做为模板,而run方法是在变化的,所以放到子类来实现。markdown

1. 建立多个线程

上面的例子中除了咱们建立的一个线程之外其实还有一个主线程也在执行。那么除了这两个线程之外还有没有其余的线程在执行了呢,实际上是有的,好比咱们看不到的垃圾回收线程,也在默默的执行。这里咱们并不去考虑有多少个线程在执行,上面咱们本身建立了一个线程,那么能不能多建立几个一块儿执行呢,答案是确定的。一个Thread类就是一个线程对象,那么多建立几个Thread类,并调用其start方法就能够启动多个线程了。代码以下多线程

package com.kingh.thread.create;

/** * 建立多个线程同时执行 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 9:46 */
public class CreateMultiThreadDemo2 extends Thread {

    public CreateMultiThreadDemo2(String name) {
        // 设置当前线程的名字
        this.setName(name);
    }

    @Override
    public void run() {
        // 每隔1s中输出一次当前线程的名字
        while (true) {
            // 输出线程的名字,与主线程名称相区分
            printThreadInfo();
            try {
                // 线程休眠一秒
                Thread.sleep(1000);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        // 注意这里,要调用start方法才能启动线程,不能调用run方法
        new CreateMultiThreadDemo2("MyThread-01").start();

        // 建立多个线程实例,同时执行
        new CreateMultiThreadDemo2("MyThread-02").start();

        // 演示主线程继续向下执行
        while (true) {
            printThreadInfo();
            Thread.sleep(1000);
        }
    }

    /** * 输出当前线程的信息 */
    private static void printThreadInfo() {
        System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
    }
}

运行结果以下app

当前运行的线程名为: 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 <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 9:46 */
public class CreateMultiThreadDemo3 extends Thread {

    @Override
    public void run() {
        // 每隔1s中输出一次当前线程的名字
        while (true) {
            // 输出线程的名字,与主线程名称相区分
            printThreadInfo();
            try {
                // 线程休眠一秒
                Thread.sleep(1000);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        // 注意这里,要调用start方法才能启动线程,不能调用run方法
        new CreateMultiThreadDemo3().start();

        // 建立多个线程实例,同时执行
        new CreateMultiThreadDemo3().start();

        // 演示主线程继续向下执行
        while (true) {
            printThreadInfo();
            Thread.sleep(1000);
        }
    }

    /** * 输出当前线程的信息 */
    private static void printThreadInfo() {
        System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
    }
}

运行的结果以下:less

当前运行的线程名为: main
当前运行的线程名为: Thread-1
当前运行的线程名为: Thread-0
当前运行的线程名为: main
当前运行的线程名为: Thread-1
当前运行的线程名为: Thread-0

第二种方式:实现Runnable接口

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

package java.lang;

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

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

Runnable 是一个@FunctionalInterface 函数式接口,也就意味了能够利用JDK8提供的lambda的方式来建立线程任务,后面的代码中会给读者演示具体如何使用。

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

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

1. 建立线程任务

线程任务就是线程要作的事情,这里咱们让这个线程每隔1s中打印本身的名字

package com.kingh.thread.create;

/** * 线程任务 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo4_Task implements Runnable {

    @Override
    public void run() {
		// 每隔1s中输出一次当前线程的名字
        while (true) {
            // 输出线程的名字,与主线程名称相区分
            printThreadInfo();
            try {
                // 线程休眠一秒
                Thread.sleep(1000);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    /** * 输出当前线程的信息 */
    private static void printThreadInfo() {
        System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
    }
}

2. 建立可运行类

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

package com.kingh.thread.create;

/** * 建立线程 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo4_Main {

    public static void main(String[] args) throws Exception {
        // 实例化线程任务类
        CreateThreadDemo4_Task task = new CreateThreadDemo4_Task();

        // 建立线程对象,并将线程任务类做为构造方法参数传入
        new Thread(task).start();

        // 主线程的任务,为了演示多个线程一块儿执行
        while (true) {
            printThreadInfo();
            Thread.sleep(1000);
        }
    }

    /** * 输出当前线程的信息 */
    private static void printThreadInfo() {
        System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
    }
}

线程任务和线程的控制分离,那么一个线程任务能够提交给多个线程来执行。这是颇有用的,好比车站的售票窗口,每一个窗口能够看作是一个线程,他们每一个窗口作的事情都是同样的,也就是售票。这样咱们程序在模拟现实的时候就能够定义一个售票任务,让多个窗口同时执行这一个任务。那么若是要改动任务执行计划,只要修改线程任务类,全部的线程就都会按照修改后的来执行。相比较继承Thread类的方式来建立线程的方式,实现Runnable接口是更为经常使用的。

3. lambda方式建立线程任务

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

package com.kingh.thread.create;

/** * 建立线程with lambda * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo5_Lambda {

    public static void main(String[] args) throws Exception {
        // 使用lambda的形式实例化线程任务类
        Runnable task = () -> {
            while (true) {
                // 输出线程的名字
                printThreadInfo();
            }
        };

        // 建立线程对象,并将线程任务类做为构造方法参数传入
        new Thread(task).start();

        // 主线程的任务,为了演示多个线程一块儿执行
        while (true) {
            printThreadInfo();
            Thread.sleep(1000);
        }
    }

    /** * 输出当前线程的信息 */
    private static void printThreadInfo() {
        System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

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

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

package com.kingh.thread.create;

/** * 匿名内部类的方式建立线程 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo6_Anonymous {

    public static void main(String[] args) {
        // 基于子类的方式
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    printThreadInfo();
                }
            }
        }.start();

        // 基于接口的实现
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    printThreadInfo();
                }
            }
        }).start();
    }

    /** * 输出当前线程的信息 */
    private static void printThreadInfo() {
        System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}

能够想象一下,我能不能既基于接口,又基于子类呢?像下面的代码会执行出什么样子呢?

package com.kingh.thread.create;

/** * 匿名内部类的方式建立线程 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo7_Anonymous {

    public static void main(String[] args) {
        // 基于子类和接口的方式
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    printInfo("interface");
                }
            }
        }) {
            @Override
            public void run() {
                while (true) {
                    printInfo("sub class");
                }
            }
        }.start();
    }

    /** * 输出当前线程的信息 */
    private static void printInfo(String text) {
        System.out.println(text);
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}

运行结果以下:

sub class
sub 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方法的内容。

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

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

1. 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 <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo9_Timer {

    private static final SimpleDateFormat format =
            new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public static void main(String[] args) throws Exception {

        // 建立定时器
        Timer timer = new Timer();

        // 提交计划任务
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时任务执行了...");
            }
        }, format.parse("2017-10-11 22:00:00"));
    }
}

2.间隔时间重复执行

package com.kingh.thread.create;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/** * 定时任务 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo10_Timer {

    public static void main(String[] args){

        // 建立定时器
        Timer timer = new Timer();

        // 提交计划任务
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时任务执行了...");
            }
        },
                new Date(), 1000);
    }
}

关于Spring的定时任务,能够参考 《Spring计划任务》

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

咱们发现上面提到的不论是继承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 <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo11_Callable {

    public static void main(String[] args) throws Exception {

        // 建立线程任务
        Callable<Integer> call = () -> {
            System.out.println("线程任务开始执行了....");
            Thread.sleep(2000);
            return 1;
        };

        // 将任务封装为FutureTask
        FutureTask<Integer> task = new FutureTask<>(call);

        // 开启线程,执行线程任务
        new Thread(task).start();

        // ====================
        // 这里是在线程启动以后,线程结果返回以前
        System.out.println("这里能够随心所欲....");
        // ====================

        // 随心所欲完毕以后,拿到线程的执行结果
        Integer result = task.get();
        System.out.println("主线程中拿到异步任务执行的结果为:" + result);
    }
}

执行结果以下:

这里能够随心所欲....
线程任务开始执行了....
主线程中拿到异步任务执行的结果为:1

Callable中能够经过范型参数来指定线程的返回值类型。经过FutureTask的get方法拿到线程的返回值。

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

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

代码以下:

package com.kingh.thread.create;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/** * 线程池 * * @author <a href="https://blog.csdn.net/king_kgh>Kingh</a> * @version 1.0 * @date 2019/3/18 10:04 */
public class CreateThreadDemo12_ThreadPool {

    public static void main(String[] args) throws Exception {

        // 建立固定大小的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        while (true) {
            // 提交多个线程任务,并执行
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    printThreadInfo();
                }
            });
        }
    }

    /** * 输出当前线程的信息 */
    private static void printThreadInfo() {
        System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}

执行结果以下:

当前运行的线程名为: 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

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

Spring方式:使用Spring来实现多线程

这种方式依赖于Spring3以上版本,咱们能够经过Spring的@Async注解很是方便的实现多线程。具体的使用方式见 Spring实现多线程

相关文章
相关标签/搜索