线程池和线程相关类

线程池概述
系统启用一个新线程的成本是比较高的,因为它涉及与操作系统交互。在这种情形下,使用线程池可以很好的提高性能。线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池会启动一个线程来执行它们的run()或call方法,当方法执行结束后,线程并不会死亡,而是再次返回到线程池成为空闲状态,等待执行下一个Runnable对象的方法。除此之外,线程池可以有效地控制系统中并发线程的数量。

Java8改进的线程池

在Java5之前,开发者需要手动实现自己的线程池,从Java5开始,Java内建支持线程池。提供了一个Executors工厂类来产生线程池,该工厂类包含如下几个静态工厂方法来创建线程池。

  • newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。
  • newFixedThreadExecutor(int nThreads):创建一个可重用的、具有固定线程数的线程池。
  • newSingleThreadExecutor():创建一个只有单线程的线程池;
  • newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。
  • newSingleThreadScheduledPool():创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。
  • ExcutorService newWorkStealingPool(int parallelism):创建持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争。
  • ExcutorService newWorkStealingPool():前一个版本的简化版,如果当前机器有4个CPU,则目标并行级别被设置为4.

前三个方法返回ExecutorService对象,中间两个方法返回它的子类;ScheduledExecutorService线程池,后来两个方法充分利用多CPU并行的能力;
ExecutorService代表尽快执行线程的线程池,它提供了如下三个方法:

  • Future<?> submit(Runnable task):将一个Runnable对象提交给指定的线程池,线程池将在有空闲线程时执行任务。其中Future代表Runnable任务的返回值,但run方法没有返回值,所以返回null,但是可以调用Future的isDone()、isCancelled()方法来获得Runnable对象的执行状态。
  • <T>Future<T>submit(Runnable task,T result):将一个Runnable对象提交给指定的线程池,线程池将在有空闲线程时执行任务。其中result显式指定线程执行结束后的返回值,所以Future对象将在方法执行完之后返回null。
  • <T>Future<T> submit(Callable<T> task):将一个Callable对象提交给指定的线程池,线程池将在有空闲线程时执行任务。其中Future代表Callable任务的返回值.

ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池。

  • ScheduledFuture<V>schedule(Callable<V>callable,long delay,TimeUnit unit):指定callable任务将在delay延迟后执行。
  • ScheduledFuture<?>schedule(Runnable command,long delay,TimeUnit unit):指定command任务将在delay延迟后执行。
  • ScheduledFuture<?>scheduleAtFixedRate(Runnable command,,long initialDelay,long period,TimeUnit unit):指定command任务将在delay延迟后执行,并且以设定频率重复执行;
  • ScheduledFuture<?>scheduleWithFixedRate(Runnable command,,long initialDelay,long delay,TimeUnit unit):创建并执行一个在给定初始延迟后首次启用的定期操作,随后在每一个执行终止和下一次执行开始之间都存在给定延迟,如果任务在任一次执行时遇到异常,就会取消后续执行,否则,只能通过程序来显式取消或终止该任务。

用完一个线程池之后,应该调用该线程池的shutdown方法,该方法将启动线程池的关闭序列,调用后的线程池将不再接受新任务,但会将以前所有已经提交的任务完成。当所有线程都完成的时候,线程池都的所有线程都会死亡;除此之外,也可以调用线程池的shutdownNow()方法来关闭线程池,该方法试图通知所有正在执行的活动任务,暂停正在等待的任务,并返回等待的任务列表。

使用线程池来执行线程任务的步骤:

  • 调用Executors类的静态工厂方法创建一个Executor Service对象,该对象代表一个线程池;
  • 创建Runnable实现类或Callable实现类的实例,作为线程执行任务;
  • 调用Executor Service对象的submit()方法来提交Runnable实例或者Callable实例;
  • 使用shutdown来关闭线程池;
package org.westos.demo8;

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

public class ThreadPoolTest {
    public static void main(String[] args) {
        //创建一个具有固定线程数的线程池
        ExecutorService pool = Executors.newFixedThreadPool(6);
        //使用Lambda表达式创建Runnable对象
        Runnable target=()->{
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+"的i值为:"+i);
            }
        };
        //向线程池中提交两个线程
        pool.submit(target);
        pool.submit(target);
        //关闭线程池
        pool.shutdown();

    }
}

Java8增强的ForkJoinPool

为了充分利用国多CPU、多核CPU的优势,Java7提供了ForkJoinPool来支持讲一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果。
ForkJoinPool是ExecutorService的实现类。
常用构造器:

  • ForkJoinPool(int parallelism):创建一个包含parallelism个并行线程的ForkJoinPool。
  • ForkJoinPool():以Runtime.availableProcessors()方法的返回值作为parallelism参数创建ForkJoinPool。

Java8开始ForkJoinPool中增加了通用池功能:

  • ForkJoinPool commonPool():该方法返回一个通用池,通用池的运行状态不会受shutdown或shutdownNow方法的影响。除非使用System.exit(0)来终止虚拟机;
  • int getCommonPoolParallelism():该方法返回通用池的并行级别;

创建了ForkJoinPool实例后,就可以调用ForkJoinPool的submit(ForkJoinTask task)或invoke(ForkJoinTask task)方法来执行指定任务。其中ForkJoinTask代表一个可以并行、合并的任务。

案例:以执行没有返回值的“大任务”(打印0~300的数值)为例,程序将一个“大任务”差分成多个“小任务”,并将任务交给ForkJoinPool来执行。

package org.westos.demo8;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.TimeUnit;

class PrintTask extends RecursiveAction{
    //每个“小任务”最多只能打印50个数
    private static final int THRESROLD=50;
    private int start;
    private int end;

    public PrintTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected void compute() {
        //当end和start之间的数小于THRESROLD时开始打印
        if(end-start<THRESROLD){
            for(int i=start;i<end;i++){
                System.out.println(Thread.currentThread().getName()
                +"的i值:"+i);
            }
        }else {
            //当任务大于50时,分解大任务
            int middle=(end+start)/2;
            PrintTask left = new PrintTask(start, middle);
            PrintTask right = new PrintTask(middle, end);
            //并行执行两个小任务
            left.fork();
            right.fork();
        }
    }
}

public class ForkJoinPoolTest {
    public static void main(String[] args) throws InterruptedException {
        ForkJoinPool pool = new ForkJoinPool();
        pool.submit(new PrintTask(0,300));
        pool.awaitTermination(2,TimeUnit.SECONDS);
        //关闭县城池
        pool.shutdown();
    }
}

在这里插入图片描述

从运行结果可以看到,线程池启动了四个线程来打印,这是因为运行计算机是四核的。还可以看到打印并不是按顺序打印,这是因为四个线程是并行的。

上面案例是一个没有返回值的打印任务,如果是有返回值的打印任务,则可以让任务继承RecuriveTask,其中泛型参数T代表的就是返回值的类型。
案例2:对一个长度为100的数组值进行叠加:

在这里插入代码片

线程相关类

Java还为线程安全提供了一些工具类,如ThreadLocal类,它代表一个线程局部变量,通过把数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本,从而避免并发访问的线程安全问题。

ThreadLocal

Thread的功能是为每一个使用该变量的线程都提供变量值副本,使每个线程可以独立地改变自己的副本,而不会和其他线程副本冲突。

ThreadLocal类的用法很简单,它只提供了如下三个方法:

  • T get():返回此线程局部变量中当前线程副本的值;
  • void remove():删除此线程局部变量中当前线程的值;
  • void set(T value):设置次线程局部变量中当前线程副本的值;
package org.westos.demo4;

class Account{
    //定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量,每个线程都会保留该变量的一个副本
    private ThreadLocal<String> name=new ThreadLocal<>();
    //定义一个初始化name成员变量的构造器
    public Account(String str){
        this.name.set(str);
        //用于访问当前线程的name副本的值
        System.out.println("---"+this.name.get());
    }
    public String getName() {
        return this.name.get();
    }
    public void setName(String name) {
        this.name.set(name);
    }
}

class MyTest extends Thread {
    private Account account;

    public MyTest(Account account, String name) {
        super(name);
        this.account = account;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (i == 6) {
                //当i=6时,将账户名改为当前线程名
                account.setName(getName());
            }
            System.out.println(account.getName() + "账户的值:" + "i");
        }
    }

}
public class ThreadLocalTest {
    public static void main(String[] args) {
        Account at = new Account("初始名");
        new MyTest(at, "线程甲").start();
        new MyTest(at, "线程乙").start();

    }
}

在这里插入图片描述

可以看到两个线程都会在i=6时将账户名改为与线程名相同,所以两个线程拥有两个账户名的情形。

实际上账户有三个副本,主线程一个,另外两个启动线程各一个,它们的值互不干扰。

Thread和其他所有同步机制一样,都是为了解决多线程中对同一变量的访问冲突。ThreadLocal并不能代替同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是为了隔离多个线程的数据共享,从而避免多个线程之间对共享资源的竞争,也就不需要对多个线程进行同步了。

猜你喜欢

转载自blog.csdn.net/mashaokang1314/article/details/84327480