java多线程的6种实现方式详解、线程池、定时器

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

继承Thread类

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

package com.hy.thread.t1;

/**
 * 继承Thread类的方式实现多线程演示
 * 
 * @author 007
 *
 */
public class ThreadDemo extends Thread {

    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
            try {
                Thread.sleep(1000); // 休息1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        td.start(); // 启动线程

        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
            try {
                Thread.sleep(1000); // 休息1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

运行结果如下

main is running ... 
Thread-0 is running ... 
main is running ... 
Thread-0 is running ... 
Thread-0 is running ... 
main is running ... 
Thread-0 is running ... 
main is running ... 
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这里要注意,在启动线程的时候,我们并不是调用线程类的run方法,而是调用了线程类的start方法。那么我们能不能调用run方法呢?答案是肯定的,因为run方法是一个public声明的方法,因此我们是可以调用的,但是如果我们调用了run方法,那么这个方法将会作为一个普通的方法被调用,并不会开启线程。这里实际上是采用了设计模式中的模板方法模式,Thread类作为模板,而run方法是在变化的因此放到子类。

创建多个线程

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

package com.hy.thread.t1;

public class MultiThreadDemo extends Thread {

    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
            try {
                Thread.sleep(1000); // 休息1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {

        // 创建四个线程对象,代表四个线程
        MultiThreadDemo td1 = new MultiThreadDemo();
        MultiThreadDemo td2 = new MultiThreadDemo();
        MultiThreadDemo td3 = new MultiThreadDemo();
        MultiThreadDemo td4 = new MultiThreadDemo();

        td1.start(); // 启动线程
        td2.start(); // 启动线程
        td3.start(); // 启动线程
        td4.start(); // 启动线程

        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
            try {
                Thread.sleep(1000); // 休息1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

运行结果如下

main is running ... 
Thread-2 is running ... 
Thread-1 is running ... 
Thread-3 is running ... 
Thread-0 is running ... 
Thread-3 is running ... 
Thread-2 is running ... 
main is running ... 
Thread-1 is running ... 
Thread-0 is running ... 
Thread-1 is running ... 
main is running ... 
Thread-2 is running ... 
Thread-0 is running ... 
Thread-3 is running ... 
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

我们发现这里有个问题,多个线程的名字都是系统定义好的,就是Thread-开头,后面跟数字,如果我们每个线程处理不同的任务,那么我们能不能给线程起上不同的名字,方便我们排查问题呢?答案是可以的。只要在创建线程实例的时候,在构造方法中传入指定的线程名称即可。如下

package com.hy.thread.t1;

public class MultiThreadDemo extends Thread {

    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
            try {
                Thread.sleep(1000); // 休息1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 指定线程名称的构造方法
     * 
     * @param name
     */
    public MultiThreadDemo(String name) {
        super(name);
    }

    public static void main(String[] args) {

        // 创建四个线程对象,代表四个线程
        MultiThreadDemo td1 = new MultiThreadDemo("t1"); // 指定线程的名字
        MultiThreadDemo td2 = new MultiThreadDemo("t2");
        MultiThreadDemo td3 = new MultiThreadDemo("t3");
        MultiThreadDemo td4 = new MultiThreadDemo("t4");

        td1.start(); // 启动线程
        td2.start(); // 启动线程
        td3.start(); // 启动线程
        td4.start(); // 启动线程

        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
            try {
                Thread.sleep(1000); // 休息1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

运行的结果如下:

main is running ... 
t1 is running ... 
t2 is running ... 
t3 is running ... 
t4 is running ... 
main is running ... 
t1 is running ... 
t2 is running ... 
t4 is running ... 
t3 is running ... 
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

实现Runnable接口

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

package java.lang;

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

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

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

线程任务类代码如下

package com.hy.thread.t2;

public class ThreadTarget implements Runnable {

    @Override
    public void run() {
        while(true) {
            System.out.println(Thread.currentThread().getName() + " is running .. ");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

可运行类代码如下

package com.hy.thread.t2;

public class Main {

    public static void main(String[] args) {

        ThreadTarget tt = new ThreadTarget(); // 实例化线程任务类
        Thread t = new Thread(tt); // 创建线程对象,并将线程任务类作为构造方法参数传入
        t.start(); // 启动线程

        // 主线程的任务,为了演示多个线程一起执行
        while(true) {
            System.out.println(Thread.currentThread().getName() + " is running .. ");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

线程任务和线程的控制分离,那么一个线程任务可以提交给多个线程来执行。这是很有用的,比如车站的售票窗口,每个窗口可以看做是一个线程,他们每个窗口做的事情都是一样的,也就是售票。这样我们程序在模拟现实的时候就可以定义一个售票任务,让多个窗口同时执行这一个任务。那么如果要改动任务执行计划,只要修改线程任务类,所有的线程就都会按照修改后的来执行。相比较继承Thread类的方式来创建线程的方式,实现Runnable接口是更为常用的。

使用内部类的方式

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

package com.hy.thread.t3;

public class DemoThread {

    public static void main(String[] args) {

        // 基于子类的实现
        new Thread() {
            public void run() {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
                    try {
                        Thread.sleep(1000); // 休息1000ms
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        }.start();

        // 基于接口的实现
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
                    try {
                        Thread.sleep(1000); // 休息1000ms
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        // 主线程的方法
        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
            try {
                Thread.sleep(1000); // 休息1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

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

package com.hy.thread.t3;

public class DemoThred2 {

    public static void main(String[] args) {


        new Thread(new Runnable() {

            @Override
            public void run() {
                while (true) {
                    System.out.println("runnable is running ... "); // 打印当前线程的名字
                    try {
                        Thread.sleep(1000); // 休息1000ms
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }) {
            public void run() {
                while (true) {
                    System.out.println("sub is running ... "); // 打印当前线程的名字
                    try {
                        Thread.sleep(1000); // 休息1000ms
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        }.start();


    }

}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

运行结果如下:

sub is running ... 
sub is running ... 
sub is running ... 

    
    
  • 1
  • 2
  • 3
  • 4

我们可以看到,其实是基于子类的执行了,为什么呢,其实很简单,我们先来看一下为什么不基于子类的时候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();
    }
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

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

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

定时器

定时器可以说是一种基于线程的一个工具类。可以定时的来执行某个任务。比如要在凌晨的时候汇总一些数据,比如要每隔10分钟抓取一次某个网站上的数据等等,总之计时器无处不在。我们一般将需要定时完成的任务称之为计划任务,这在很多的系统中是非常常见的,比如linux的计划任务,比如Windows下的任务计划等等。我们自己的系统中也需要很多定时执行的也都需要计划任务。最简单的计划任务就可以通过jdk给我提供的API来实现,当然也有很多的计划任务的框架,比如spring的schedule以及著名的quartz。我们这里不去讨论其他的计划任务框架,我们就来看一下jdk所给我们提供的API来实现定时任务。

  • 例1:在2017年10月11日晚上10点执行任务。
package com.roocon.thread.t3;

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

/**
 * 定时器举例
 * 
 */
public class TimerDemo {

    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"));
    }

}

    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 例2: 每隔5s执行一次
package com.roocon.thread.t3;

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

public class TimerDemo2 {

    public static void main(String[] args) {
        Timer timer = new Timer();

        timer.schedule(new TimerTask() {

            @Override
            public void run() {
                System.out.println("Hello");
            }
        }, new Date(), 5000);
    }

}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

关于Spring的定时任务,可以通过spring的教程来学习。

带返回值的线程实现方式

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

1. 创建一个类实现Callable接口,实现call方法。这个接口类似于Runnable接口,但比Runnable接口更加强大,增加了异常和返回值。
2. 创建一个FutureTask,指定Callable对象,做为线程任务。
3. 创建线程,指定线程任务。
4. 启动线程

    
    
  • 1
  • 2
  • 3
  • 4
  • 5

代码如下:

package com.roocon.thread.t4;

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

public class CallableTest {

    public static void main(String[] args) throws Exception {
        Callable<Integer> call = new Callable<Integer>() {

            @Override
            public Integer call() throws Exception {
                System.out.println("thread start .. ");
                Thread.sleep(2000);
                return 1;
            }
        };

        FutureTask<Integer> task = new FutureTask<>(call);
        Thread t =  new Thread(task);

        t.start();
        System.out.println("do other thing .. ");
        System.out.println("拿到线程的执行结果 : " + task.get());
    }

}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

执行结果如下:

do other thing .. 
thread start .. 
拿到线程的执行结果 : 1
    
    
  • 1
  • 2
  • 3

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

基于线程池的方式

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

代码如下:

package com.roocon.thread.t5;

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

public class ThreadPoolDemo {

    public static void main(String[] args) {

        // 创建线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        while(true) {
            threadPool.execute(new Runnable() { // 提交多个线程任务,并执行

                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " is running ..");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

执行结果如下:

pool-1-thread-4 is running ..
pool-1-thread-1 is running ..
pool-1-thread-6 is running ..
pool-1-thread-2 is running ..
pool-1-thread-8 is running ..
pool-1-thread-3 is running ..
pool-1-thread-5 is running ..
pool-1-thread-9 is running ..
pool-1-thread-10 is running ..
pool-1-thread-7 is running ..
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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

        <link href="https://csdnimg.cn/release/phoenix/template/css/markdown_views-ea0013b516.css" rel="stylesheet">
            </div>

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

继承Thread类

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

package com.hy.thread.t1;

/**
 * 继承Thread类的方式实现多线程演示
 * 
 * @author 007
 *
 */
public class ThreadDemo extends Thread {

    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
            try {
                Thread.sleep(1000); // 休息1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        td.start(); // 启动线程

        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
            try {
                Thread.sleep(1000); // 休息1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

运行结果如下

main is running ... 
Thread-0 is running ... 
main is running ... 
Thread-0 is running ... 
Thread-0 is running ... 
main is running ... 
Thread-0 is running ... 
main is running ... 
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这里要注意,在启动线程的时候,我们并不是调用线程类的run方法,而是调用了线程类的start方法。那么我们能不能调用run方法呢?答案是肯定的,因为run方法是一个public声明的方法,因此我们是可以调用的,但是如果我们调用了run方法,那么这个方法将会作为一个普通的方法被调用,并不会开启线程。这里实际上是采用了设计模式中的模板方法模式,Thread类作为模板,而run方法是在变化的因此放到子类。

创建多个线程

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

package com.hy.thread.t1;

public class MultiThreadDemo extends Thread {

    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
            try {
                Thread.sleep(1000); // 休息1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {

        // 创建四个线程对象,代表四个线程
        MultiThreadDemo td1 = new MultiThreadDemo();
        MultiThreadDemo td2 = new MultiThreadDemo();
        MultiThreadDemo td3 = new MultiThreadDemo();
        MultiThreadDemo td4 = new MultiThreadDemo();

        td1.start(); // 启动线程
        td2.start(); // 启动线程
        td3.start(); // 启动线程
        td4.start(); // 启动线程

        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
            try {
                Thread.sleep(1000); // 休息1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

运行结果如下

main is running ... 
Thread-2 is running ... 
Thread-1 is running ... 
Thread-3 is running ... 
Thread-0 is running ... 
Thread-3 is running ... 
Thread-2 is running ... 
main is running ... 
Thread-1 is running ... 
Thread-0 is running ... 
Thread-1 is running ... 
main is running ... 
Thread-2 is running ... 
Thread-0 is running ... 
Thread-3 is running ... 
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

我们发现这里有个问题,多个线程的名字都是系统定义好的,就是Thread-开头,后面跟数字,如果我们每个线程处理不同的任务,那么我们能不能给线程起上不同的名字,方便我们排查问题呢?答案是可以的。只要在创建线程实例的时候,在构造方法中传入指定的线程名称即可。如下

package com.hy.thread.t1;

public class MultiThreadDemo extends Thread {

    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
            try {
                Thread.sleep(1000); // 休息1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 指定线程名称的构造方法
     * 
     * @param name
     */
    public MultiThreadDemo(String name) {
        super(name);
    }

    public static void main(String[] args) {

        // 创建四个线程对象,代表四个线程
        MultiThreadDemo td1 = new MultiThreadDemo("t1"); // 指定线程的名字
        MultiThreadDemo td2 = new MultiThreadDemo("t2");
        MultiThreadDemo td3 = new MultiThreadDemo("t3");
        MultiThreadDemo td4 = new MultiThreadDemo("t4");

        td1.start(); // 启动线程
        td2.start(); // 启动线程
        td3.start(); // 启动线程
        td4.start(); // 启动线程

        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
            try {
                Thread.sleep(1000); // 休息1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

运行的结果如下:

main is running ... 
t1 is running ... 
t2 is running ... 
t3 is running ... 
t4 is running ... 
main is running ... 
t1 is running ... 
t2 is running ... 
t4 is running ... 
t3 is running ... 
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

实现Runnable接口

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

package java.lang;

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

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

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

线程任务类代码如下

package com.hy.thread.t2;

public class ThreadTarget implements Runnable {

    @Override
    public void run() {
        while(true) {
            System.out.println(Thread.currentThread().getName() + " is running .. ");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

可运行类代码如下

package com.hy.thread.t2;

public class Main {

    public static void main(String[] args) {

        ThreadTarget tt = new ThreadTarget(); // 实例化线程任务类
        Thread t = new Thread(tt); // 创建线程对象,并将线程任务类作为构造方法参数传入
        t.start(); // 启动线程

        // 主线程的任务,为了演示多个线程一起执行
        while(true) {
            System.out.println(Thread.currentThread().getName() + " is running .. ");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

线程任务和线程的控制分离,那么一个线程任务可以提交给多个线程来执行。这是很有用的,比如车站的售票窗口,每个窗口可以看做是一个线程,他们每个窗口做的事情都是一样的,也就是售票。这样我们程序在模拟现实的时候就可以定义一个售票任务,让多个窗口同时执行这一个任务。那么如果要改动任务执行计划,只要修改线程任务类,所有的线程就都会按照修改后的来执行。相比较继承Thread类的方式来创建线程的方式,实现Runnable接口是更为常用的。

使用内部类的方式

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

package com.hy.thread.t3;

public class DemoThread {

    public static void main(String[] args) {

        // 基于子类的实现
        new Thread() {
            public void run() {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
                    try {
                        Thread.sleep(1000); // 休息1000ms
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        }.start();

        // 基于接口的实现
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
                    try {
                        Thread.sleep(1000); // 休息1000ms
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        // 主线程的方法
        while (true) {
            System.out.println(Thread.currentThread().getName() + " is running ... "); // 打印当前线程的名字
            try {
                Thread.sleep(1000); // 休息1000ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

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

package com.hy.thread.t3;

public class DemoThred2 {

    public static void main(String[] args) {


        new Thread(new Runnable() {

            @Override
            public void run() {
                while (true) {
                    System.out.println("runnable is running ... "); // 打印当前线程的名字
                    try {
                        Thread.sleep(1000); // 休息1000ms
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }) {
            public void run() {
                while (true) {
                    System.out.println("sub is running ... "); // 打印当前线程的名字
                    try {
                        Thread.sleep(1000); // 休息1000ms
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        }.start();


    }

}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

运行结果如下:

sub is running ... 
sub is running ... 
sub is running ... 

  
  
  • 1
  • 2
  • 3
  • 4

我们可以看到,其实是基于子类的执行了,为什么呢,其实很简单,我们先来看一下为什么不基于子类的时候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();
    }
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

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

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

定时器

定时器可以说是一种基于线程的一个工具类。可以定时的来执行某个任务。比如要在凌晨的时候汇总一些数据,比如要每隔10分钟抓取一次某个网站上的数据等等,总之计时器无处不在。我们一般将需要定时完成的任务称之为计划任务,这在很多的系统中是非常常见的,比如linux的计划任务,比如Windows下的任务计划等等。我们自己的系统中也需要很多定时执行的也都需要计划任务。最简单的计划任务就可以通过jdk给我提供的API来实现,当然也有很多的计划任务的框架,比如spring的schedule以及著名的quartz。我们这里不去讨论其他的计划任务框架,我们就来看一下jdk所给我们提供的API来实现定时任务。

  • 例1:在2017年10月11日晚上10点执行任务。
package com.roocon.thread.t3;

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

/**
 * 定时器举例
 * 
 */
public class TimerDemo {

    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"));
    }

}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 例2: 每隔5s执行一次
package com.roocon.thread.t3;

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

public class TimerDemo2 {

    public static void main(String[] args) {
        Timer timer = new Timer();

        timer.schedule(new TimerTask() {

            @Override
            public void run() {
                System.out.println("Hello");
            }
        }, new Date(), 5000);
    }

}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

关于Spring的定时任务,可以通过spring的教程来学习。

带返回值的线程实现方式

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

1. 创建一个类实现Callable接口,实现call方法。这个接口类似于Runnable接口,但比Runnable接口更加强大,增加了异常和返回值。
2. 创建一个FutureTask,指定Callable对象,做为线程任务。
3. 创建线程,指定线程任务。
4. 启动线程

  
  
  • 1
  • 2
  • 3
  • 4
  • 5

代码如下:

package com.roocon.thread.t4;

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

public class CallableTest {

    public static void main(String[] args) throws Exception {
        Callable<Integer> call = new Callable<Integer>() {

            @Override
            public Integer call() throws Exception {
                System.out.println("thread start .. ");
                Thread.sleep(2000);
                return 1;
            }
        };

        FutureTask<Integer> task = new FutureTask<>(call);
        Thread t =  new Thread(task);

        t.start();
        System.out.println("do other thing .. ");
        System.out.println("拿到线程的执行结果 : " + task.get());
    }

}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

执行结果如下:

do other thing .. 
thread start .. 
拿到线程的执行结果 : 1
  
  
  • 1
  • 2
  • 3

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

基于线程池的方式

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

代码如下:

package com.roocon.thread.t5;

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

public class ThreadPoolDemo {

    public static void main(String[] args) {

        // 创建线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        while(true) {
            threadPool.execute(new Runnable() { // 提交多个线程任务,并执行

                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " is running ..");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

执行结果如下:

pool-1-thread-4 is running ..
pool-1-thread-1 is running ..
pool-1-thread-6 is running ..
pool-1-thread-2 is running ..
pool-1-thread-8 is running ..
pool-1-thread-3 is running ..
pool-1-thread-5 is running ..
pool-1-thread-9 is running ..
pool-1-thread-10 is running ..
pool-1-thread-7 is running ..
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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

        <link href="https://csdnimg.cn/release/phoenix/template/css/markdown_views-ea0013b516.css" rel="stylesheet">
            </div>

猜你喜欢

转载自blog.csdn.net/qq_22771739/article/details/81462059