Java多线程

文章目录


前言

我们在创建线程时有四种方法
1、继承Thread类创建线程
2、实现Runnable接口创建线程
3、使用Callable和Future创建线程
4、使用线程池创建(使用java.util.concurrent.Executor接口)


一、创建线程

1.继承Thread类

简单实现:

  • 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就是线程要完成的任务。run()方法称为执行体。

  • 创建Thread子类的实例,即创建了线程对象。

  • 调用线程对象的start()方法来启动该线程。

代码示例如下:

package thread;

public class ThreadModelOne extends Thread {
    @Override
    public void run() {
        System.out.println("我是继承Thread创建的子线程");
    }

    public static void main(String[] args) {
        //创建两个子线程
        for (int i = 0; i < 2; i++) {
            new ThreadModelOne().start();
        }
        craterByJava8();
    }

    // 使用java8的Lambda表达式创建
    public static void craterByJava8() {
        new Thread(){
            @Override
            public void run() {
                System.out.println("我是实现Runnable接口创建ByJava8");
            }
        }.start();
    }
}

2.实现Runnable接口

简单实现:

  • 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体
  • 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象
  • 调用线程对象的start()方法来启动该线程

代码示例如下:

package thread;

public class ThreadModelTwo implements Runnable {
    @Override
    public void run() {
        System.out.println("我是实现Runnable接口创建的子线程");
    }

    public static void main(String[] args) {
        Runnable runnable = new ThreadModelTwo();
        //同样是生成两个子线程
        for (int i = 0; i < 2; i++) {
            new Thread((runnable)).start();
        }
        craterByJava8();
    }

    // 使用java8的Lambda表达式创建
    public static void craterByJava8() {
        new Thread(() -> {
            System.out.println("我是实现Runnable接口创建ByJava8");
        }).start();
    }

}

3.使用Callable和Future

和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大,其表现在:

call方法可以有返回值
call()方法可以声明抛出异常

简单实现:

  • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值
  • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
  • 使用FutureTask对象作为Thread对象的target创建并启动新线程(因为FutureTask实现了Runnable接口)。
  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

代码示例如下:

package thread;

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

public class ThreadModelThree implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("我是实现Callable接口创建的子线程");
        Thread.sleep(3000);
        return Thread.currentThread().getName();
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadModelThree callable = new ThreadModelThree();
        FutureTask<String> futureTask = new FutureTask <>(callable);
        Thread thread = new Thread(futureTask,"Hello,Tom");
        Thread thread2 = new Thread(futureTask,"Hello,Jerry"); 
        thread.start();
        thread2.start();
        System.out.println(futureTask.get());
    }
}

二、使用线程池

1. 线程的生命周期

java中经常需要用到多线程来处理一些业务,创建过多的线程也可能引发资源耗尽的风险,不建议单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下文切换问题。

线程有五种基本状态,分别是:

1、NEW(初始化)状态
实例化一个Thread类对象出来(未执行start()方法前),Thread的状态为NEW。

2、RUNNABLE(可运行)状态
调用线程的start()方法,此时线程进入RUNNABLE状态,该状态的线程位于可运行线程池中,等待被操作系统调度,获取CPU的使用权。
当获得CPU的时间片时,线程进入运行状态,执行程序代码。

3、BLOCKED(阻塞)状态
当线程等待获取monitor锁(synchronized)时,线程就会进入BLOCKED状态。
-注意:
等待获取monitor锁,线程的状态是BLOCKED。
等待获取Lock锁(LockSupport.park()),线程的状态是WAITING

4.1、TIMED_WAITING(超时等待)状态
当执行 Thread.sleep(time)、Thread.join(time)、Object.wait(time)、LockSupport.parkNanos(time)、LockSupport.partUntil(time)等操作时,线程会从RUNNABLE状态进入TIMED_WAITING状态。
当执行Object.notify()/notifyAll()、Thread.join()程序执行完、LockSupport.unpark()、线程被中断等操作时,线程会从WAITING状态进入RUNNABLE状态
4.2 、WAITING(等待)状态
当执行Object.wait()、Thread.join()、LockSupport.park()等操作时,线程会从RUNNABLE状态进入WAITING状态。
当执行Object.notify()/notifyAll()、Thread.join()程序执行完、LockSupport.unpark()、线程被中断等操作时,线程会从WAITING状态进入RUNNABLE状态

5、TERMINATED(终止)状态
当线程执行完毕、Thread.stop()、内存溢出时,线程就会进入TERMINATED状态。
线程一旦死亡,就无法复活。
在一个死亡的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

Java多线程

2.线程池使用

这个时候引入线程池比较合理,方便线程任务的管理。

java中涉及到线程池的相关类均在jdk1.5开始的java.util.concurrent包中,涉及到的几个核心类及接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等

线程池可以自动创建也可以手动创建:

1、自动创建体现在Executors工具类中,常见的可以创建newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    
        public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
 
       public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
  • Executors.newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

  • Executors.newSingleeThreadPool()创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

  • Executors.newScheduledThreadPool ()创建一个定长线程池,支持定时及周期性任务执行。

  • Executors.newCacheThreadPool()创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收

2、手动创建主要用ThreadPoolExecutor类,体现在可以灵活设置线程池的各个参数,体现在代码中即ThreadPoolExecutor类构造器上各个实参的不同:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
             
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }

ThreadPoolExecutor中重要的几个参数详解

corePoolSize:核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务。

maximumPoolSize:最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)

keepAliveTime:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);

unit:keepAliveTime的时间单位

workQueue:用于保存任务的队列,可以为*、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中

threadFactory:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建。

handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy。

线程池中的线程创建流程(拒绝策略):
Java多线程


总结

实现Runnable和实现Callable接口的方式基本相同,不过是后者执行call()方法有返回值,后者线程执行体run()方法无返回值,并且如果使用FutureTask类的话,只执行一次Callable任务。

这种方式与继承Thread类的方法之间的差别如下:

1、线程只是实现Runnable或实现Callable接口,还可以继承其他类。

2、这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。

3.继承Thread类只需要this就能获取当前线程。不需要使用Thread.currentThread()方法

4、继承Thread类的线程类不能再继承其他父类(Java单继承决定)。

5、前三种的线程如果创建关闭频繁会消耗系统资源影响性能,而使用线程池可以不用线程的时候放回线程池,用的时候再从线程池取,项目开发中主要使用线程池的方式创建多个线程。

6.实现接口的创建线程的方式必须实现方法(run() call())。

上一篇:初始化线程四种使用方式


下一篇:自我学习要求:线程。对线程的理解和并发的深度挖掘