JAVA 多线程创建的方式

2020年9月23日 1 作者 折纸

一、概述

​ 在JAVA中,用Thread类代表线程,所有的线程对象都必须是Thread类或子类的实例。每个线程的任务是执行一段顺序执行的代码,JAVA使用线程执行体容纳这段代码。创建线程时根据实际需求在执行体内编写所要完成的代码。

二、8种创建方式

2.1 继承Thread类 重写Run方法实现

​ 通过继承Thread类创建并启动多线程的步骤如下:

​ 1、定义一个类继承Thread类 并重写run()方法,run方法的方法体就是该线程所要完成的任务,即线程的执行体

​ 2、创建该类的实例,即创建线程对象

​ 3、调用线程的start()方法启动线程

代码示例:

public class ExtendThread extends Thread {

    private int i;

    public static void main(String[] args) {
        for(int j = 0;j < 30;j++) {
            //调用Thread类的currentThread()方法获取当前线程
            System.out.println(Thread.currentThread().getName() + " " + j);

            if(j == 2) {
                //创建并启动第一个线程
                new ExtendThread().start();

                //创建并启动第二个线程
                new ExtendThread().start();
            }

//            System.out.println(Thread.currentThread().toString());
        }
    }
    public void run() {
        for(;i < 10;i++) {
            //当通过继承Thread类的方式实现多线程时,可以直接使用this获取当前执行的线程
            System.out.println(this.getName() + " "  + i);
        }
    }
}

 

代码相关:

1、getName()返回当前线程名,setName()设置当前线程名

2、JAVA程序运行后,程序会自动地至少创建1个主线程主线程的线程执行体main()方法确定

3、默认情况下,主线程的线程名为main(),用户创建的线程依次为Thread-0、Thread-1、···

4、线程的执行是抢占式的

2.2 实现Runnable接口创建线程类

1、定义一个类实现Runnable接口

2、创建该类的实例对象obj

3、将obj作为构造器参数 传入Thread类实例对象(真正的线程对象)

4、调用线程对象的start()方法启动线程

public class ImpRunnable implements Runnable {

    private int i;

    @Override
    public  void run() {
        for(;i < 50;i++) {
            //当线程类实现Runnable接口时,要获取当前线程对象只有通过Thread.currentThread()获取
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for(int j = 0;j < 30;j++) {
            System.out.println(Thread.currentThread().getName() + " " + j);
            if(j == 10) {
                ImpRunnable thread_target = new ImpRunnable();
                //通过new Thread(target,name)的方式创建线程
                new Thread(thread_target,"线程1").start();
                new Thread(thread_target,"线程2").start();
            }

        }

    }

}

 

代码相关:

1、实现(implements)Runnable接口的类的实例对象仅作为Thread对象的target,实现Runnable的类中的run()方法仅作为线程执行体,实际的线程对象依然是Thread实例,负责执行target的run()方法

2、通过实现Runnable接口实现多线程时,要获取当前线程对象只能通过Thread,currentThread()方法,而不能通过this关键字获取

3、从JAVA8开始,Runnable接口使用了@FunctionInterface修饰,即Runnable接口是函数式接口,可以使用lambda表达式创建对象

4、与方法1不同,通过这种方式创建线程可以使得多线程共享线程类的实例变量,因为这里多个线程都用了同一个target实例变量然而当多个线程访问同一未加锁资源时,可能出现线程安全问题,输出结果并不连续,后续学习

2.3 通过Callable和FutureTask创建线程

通过实现Runnable接口创建多线程时,Thread类的作用就是把run()方法包装成线程的执行体;

是不是可以直接把任意方法都包装成线程的执行体呢?从JAVA5开始,JAVA提供提供了Callable接口,该接口是Runnable接口的增强版,Callable接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大,call()方法的功能的强大体现在:

1、call()方法可以有返回值

2、call()方法可以抛出异常

从这里可以看出,完全可以提供一个Callable对象作为Thread的target,而该线程的线程执行体就是call()方法。但问题是:Callable接口是JAVA新增的接口,而且它不是Runnable接口的子接口,所以Callable对象不能直接作为Thread的target。还有一个原因就是:call()方法有返回值,call()方法不是直接调用,而是作为线程执行体被调用的,所以这里涉及获取call()方法返回值的问题。

于是,JAVA5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该类实现了Future接口,并实现了Runnable接口,所以FutureTask可以作为Thread类的target,同时也解决了Callable对象不能作为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;

代码示例:

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

public class ThirdThreadImp {
    public static void main(String[] args) {
        //这里call()方法的重写是采用lambda表达式,没有新建一个Callable接口的实现类
        FutureTask<Integer> task =  new FutureTask<Integer>((Callable<Integer>)()->{
            int i = 0;
            for(;i < 50;i++){
            System.out.println(Thread.currentThread().getName() + "的线程执行体内的循环变量i的值为:" + i);
            }
            //call()方法的返回值
            return i;
        });
        for(int j = 0;j < 50;j++) {
            System.out.println(Thread.currentThread().getName() + 
                    " 大循环的循环变量j的值为:" + j);
            if(j == 20) {
                new Thread(task,"有返回值的线程").start();
            }
        }
        try {
            System.out.println("子线程的返回值:" + task.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

代码相关:

调用FutureTask对象的get()方法,必须等到子线程结束以后,才会有返回值。

2.4 通过线程池创建线程

降低了创建线程和销毁线程的时间开销以及资源浪费


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

public class ThreadPool{

    private static int POOL_NUM = 10;     //线程池数量

    /**
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for(int i = 0; i<POOL_NUM; i++)
        {
            RunnableThread thread = new RunnableThread();

            //Thread.sleep(1000);
            executorService.execute(thread);
            executorService.execute(thread);
        }
        //关闭线程池
        executorService.shutdown();
    }

}

class RunnableThread implements Runnable
{
    @Override
    public void run()
    {
        System.out.println("通过线程池方式创建的线程:" + Thread.currentThread().getName() + " ");
    }
}

 

2.5 匿名内部类

适合创建启动线程次数较少的环境,书写方便

public class Demo3 {
    public static void main(String[] args) {
        //方式1:相当于继承了Thread类,作为子类重写run()实现
        new Thread() {
            public void run() {
                System.out.println("匿名内部类创建线程方式1...");
            };
        }.start();

        //方式2:实现Runnable,Runnable作为匿名内部类
        new Thread(new Runnable() {
            public void run() {
                System.out.println("匿名内部类创建线程方式2...");
            }
        }).start();
    }
}

 

2.6 定时器

file


import java.util.Timer;
import java.util.TimerTask;
/**
 * 方法6:创建启动线程之Timer定时任务
 * @author fatah
 */
public class Demo6 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时任务延迟0(即立刻执行),每隔1000ms执行一次");
            }
        }, 0, 1000);
    }

}

 

我们发现Timer有不可控的缺点,当任务未执行完毕或我们每次想执行不同任务时候,实现起来比较麻烦。

2.7 Lambda表达式的实现

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
 * 使用Lambda表达式并行计算
 * parallelStream
 * @author fatah
 */
public class Demo8 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5,6);
        Demo8 demo = new Demo8();
        int result = demo.add(list);
        System.out.println("计算后的结果为"+result);
    }
    public int add(List<Integer> list) {
        //若Lambda是串行执行,则应顺序打印
        list.parallelStream().forEach(System.out :: println);
        //Lambda有stream和parallelSteam(并行)
        return list.parallelStream().mapToInt(i -> i).sum();
    }
}

 

2.8 Spring实现多线程

1) 新建Maven工程导入Spring相关依赖

2) 新建一个Java配置类,注意需要开启@EnableAsync注解以支持异步任务

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class Config {
}

 

3) 创建异步执行的方法类(方法上需要有@Async注解支持异步方法调用)

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class AsyncService {
    @Async
    public void Async_A() {
        System.out.println("Async_A is running");
    }
    @Async
    public void Async_B() {
        System.out.println("Async_B is running");
    }
}

 

4) 创建运行类

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Run {
    public static void main(String[] args) {
        //构造方法传递Java配置类Config.class
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
        AsyncService bean = ac.getBean(AsyncService.class);
        bean.Async_A();
        bean.Async_B();
    }
}

 

三、部分创建方式的对比

上面已经介绍完了JAVA中创建线程的三种方法,通过对比我们可以知道,JAVA实现多线程可以分为两类:一类是继承Thread类实现多线程;另一类是:通过实现Runnable接口或者Callable接口实现多线程。

下面我们来分析一下这两类实现多线程的方式的优劣:

通过继承Thread类实现多线程:

优点:

1、实现起来简单,而且要获取当前线程,无需调用Thread.currentThread()方法,直接使用this即可获取当前线程;

缺点:

1、线程类已经继承Thread类了,就不能再继承其他类;

2、多个线程不能共享同一份资源(如前面分析的成员变量 i );

通过实现Runnable接口或者Callable接口实现多线程:

优点:

1、线程类只是实现了接口,还可以继承其他类;

2、多个线程可以使用同一个target对象,适合多个线程处理同一份资源的情况。

缺点:

1、通过这种方式实现多线程,相较于第一类方式,编程较复杂;

2、要访问当前线程,必须调用Thread.currentThread()方法。

综上:

一般采用第二类方式实现多线程。

四、Reference

  1. JAVA多线程的三种创建方式
  2. JAVA多线程实现的四种方式
  3. JAVA创建多线程的8种方式