线程池基础

什么是线程池

可以用 new Thread( () -> { 线程执行的任务 }).start(); 这种形式开启一个线程。当 run() 方法运行结束,线程对象会被 GC 释放。
在真实的生产环境中,可能需要很多线程来支撑整个应用,当线程数量非常多时,反而会耗尽 CPU 资源。如果不对线程进行控制与管理,反而会影响程序的性能。线程开销主要包括:创建与启动线程的开销;线程销毁开销;线程调度的开销;线程数量受限 CPU 处理器数量。
线程池就是有效使用线程的一种常用方式。线程池内部可以预先创建一定数量的工作线程,客户端代码直接将任务作为一个对象提交给线程池,线程池将这些任务缓存在工作队列中,线程池中的工作线程不断地从队列中取出任务并执行。
线程池
JDK 提供了一套 Executor 框架,可以帮助开发人员有效的使用线程池
主要的继承关系
创建线程池的工具类 Executors
工具类

1、线程池的基本使用

public class Test01Executors {
    public static void main(String[] args) {
        // 创建有5个线程大小的线程池,Executors.newFixedThreadPool(5);
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);

        // 向线程池中提交18个任务,这18个任务存储到线程池的工作(阻塞)队列中,
        // 线程池中这5个线程就从工作(阻塞)队列中取任务执行
        for (int i = 0; i < 18; i++) {
            fixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getId() + " 编号的任务在执行任务,开始时间: " + System.currentTimeMillis());
                    try {
                        Thread.sleep(1000); // 模拟任务执行时长
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

2、线程池的计划任务
ScheduleExecutorService 接口中有四个重要的方法,其中 scheduleAtFixedRate 和 scheduleWithFixedDelay 在实现定时程序时比较方便。

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, // 执行线程
                                              long initialDelay, // 初始化延时
                                              long period, // 两次开始执行最小间隔时间
                                              TimeUnit unit); // 计时单位
  public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, // 执行线程
                                                 long initialDelay, // 初始化延时
                                                 long delay, // 前一次执行结束到下一次执行开始的间隔时间(间隔执行延迟时间)
                                                 TimeUnit unit); // 计时单位
public class Test02ScheduledExecutorService {
    public static void main(String[] args) {
        // 创建一个有调度功能的线程池
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);

        // 在延迟2秒后执行任务,schedule( Runnable任务, 延迟时长, 时间单位 )
        scheduledExecutorService.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId() + " -- " + System.currentTimeMillis());
            }
        }, 2, TimeUnit.SECONDS);

        // 以【固定的频率】执行任务,开启任务的时间是固定的,在3秒后执行任务,希望以后每隔2秒重新执行一次,具体是否如此要看任务的实际执行时间
        scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId() + "----在固定频率开启任务---" + System.currentTimeMillis());
                try {
                    // 睡眠模拟任务实际执行时间,如果任务执行时长超过了时间间隔2秒,则任务完成后立即开启下个任务,实际间隔变为了4秒
                    TimeUnit.SECONDS.sleep(4);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 3, 2, TimeUnit.SECONDS);

        // 在上次任务结束后,在【固定延迟】后再次执行该任务,
        // 不管执行任务耗时多长,总是在任务结束后的2秒再次开启新的任务
        scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId() + "----在固定频率开启任务---" + System.currentTimeMillis());
                try {
                    // 睡眠模拟任务执行时间 ,不管执行任务耗时多长,总是在上一个个任务结束后的2秒再次开启新的任务
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 3, 2, TimeUnit.SECONDS);
    }
}
  • 核心线程池的底层实现

查看 Executors 工具类中 newCachedThreadPool(),newFixedThreadPool(),newSingleThreadExcecutor(),newScheduledThreadPool(int corePoolSize) 源码:

1、newCachedThreadPool(),该方法创建的线程池,SynchronousQueue 的队列容量是0,它不保存任何元素,它的主要作用是在线程之间传递数据,keepAliveTime 是60秒,maximumPoolSize 是 Integer.MAX_VALUE = 2147483647,corePoolSize 是0,因为没有核心线程,所以任务来了就判断是否有空闲线程,如果有,就去取出空闲线程任务执行,如果没有空闲线程,就新建一个线程执行。若新建了大量线程且在60秒内没有GC 掉,最终会导致内存溢出 OutOfMemoryError。适用于并发执行大量短期的小任务。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

2、newFixedThreadPool(int nThreads),该方法创建的线程池,LinkedBlockingQueue 的默认大小是 Integer.MAX_VALUE = 2147483647,keepAliveTime 时间是0,因此适用于处理 CPU 密集型的任务,确保 CPU 在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。

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

3、newSingleThreadExecutor(),该方法创建的线程池,corePoolSize 和 maximumPoolSize 都是1,keepAliveTime 时间是0,适用于串行执行任务的场景,一个任务一个任务地执行。

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

4、newScheduledThreadPool(int corePoolSize),该方法创建的线程池,maximumPoolSize 是 Integer.MAX_VALUE = 2147483647,DEFAULT_KEEPALIVE_MILLIS = 10L,10毫秒,DelayedWorkQueue 的实现是基于 DelayQueue,DelayQueue 是一个无界阻塞队列,它只允许在延迟时间到达后才能取出元素。DelayedWorkQueue 继承了 DelayQueue,并在其基础上增加了任务的优先级属性,使得任务可以按照优先级进行排序。适用于周期性执行任务的场景,需要限制线程数量的场景。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}

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

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

Excutors 工具类中返回线程池的方法底层都使用了 ThreadPoolExecutor 线程池,这些方法都是 ThreadPoolExecutor 线程池的封装。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

各个参数含义:
  corePoolSize,指定线程池中核心线程的数量
  maxinumPoolSize,指定线程池中最大线程数量
  keepAliveTime,当线程池线程的数量超过 corePoolSize 时,多余的空闲线程的存活时长,即空闲线程在多长时长内销毁
  unit,是 keepAliveTime 时长单位
  workQueue,工作队列,把任务提交到该任务队列中等待执行
  threadFactory,线程工厂,用于创建线程
  handler,拒绝策略,当任务太多来不及处理时,如何拒绝
线程池机制

  • workQueue 工作队列

workQueue 工作队列是指提交未执行的任务队列,它是 BlockingQueue 接口的对象,仅用于存储 Runnable 任务。根据队列功能分类,在 ThreadPoolExecutor 构造方法中可以使用以下几种阻塞队列:
1)直接提交队列,由 SynchronousQueue 对象提供,该队列没有容量,提交给线程池的任务不会被真实的保存,总是将新的任务提交给线程执行,如果没有空闲线程,则尝试创建新的线程,如果线程数量已经达到 maxinumPoolSize 规定的最大值则执行拒绝策略。
2)有界任务队列,由 ArrayBlockingQueue 实现,在创建 ArrayBlockingQueue 对象时,可以指定一个容量。当有任务需要执行时,如果线程池中线程数小于 corePoolSize 核心线程数则创建新的线程;如果大于 corePoolSize 核心线程数则加入等待队列。如果队列已满则无法加入,在线程数小于 maxinumPoolSize 指定的最大线程数前提下 会创建新的线程来执行,如果线程数大于 maxinumPoolSize 最大线程数则执行拒绝策略。
3)无界任务队列(在未指明容量时,容量默认为 Integer.MAX_VALUE = 2147483647),由 LinkedBlockingQueue 对象实现,与有界队列相比,除非系统资源耗尽,否则无界队列不存在任务入队失败的情况。 当有新的任务时,在系统线程数小于 corePoolSize 核心线程数则创建新的线程来执行任务;当线程池中线程数量大于 corePoolSize 核心线程数则把任务加入阻塞队列。
4)优先任务队列是通过 PriorityBlockingQueue 实现的,是带有任务优先级的队列,是一个特殊的无界队列。不管是 ArrayBlockingQueue 队列还是 LinkedBlockingQueue 队列都是按照先进先出算法处理任务的。在 PriorityBlockingQueue 队列中可以根据任务优先级顺序先后执行。

  • 拒绝策略

ThreadPoolExecutor 构造方法的最后一个参数指定了拒绝策略。当提交给线程池的任务量超过实际承载能力时,如何处理?即线程池中的线程已经用完了,等待队列也满了,无法为新提交的任务服务,可以通过拒绝策略来处理这个问题。JDK 提供了四种拒绝策略:
  AbortPolicy 策略,会抛出异常。
  CallerRunsPolicy 策略,只要线程池没关闭,会在调用者线程中运行当前被丢弃的任务。
  DiscardOldestPolicy 策略,将任务队列中最老的任务丢弃,尝试再次提交新任务(队列是先进先出,所谓最老的任务就是队列头部的那个任务)。
  DiscardPolicy 策略,直接丢弃这个无法处理的任务。
Executors 工具类提供的静态方法返回的线程池默认的拒绝策略是 AbortPolicy 抛出异常,如果内置的拒绝策略无法满足实际需求,可以扩展 RejectedExecutionHandler 接口

1、自定义拒绝策略

public class Test03RejectedExecutionHandler {
    public static void main(String[] args) {
        // 定义任务
        Runnable r = new Runnable() {
            @Override
            public void run() {
                // random.nextInt(100),该方法的作用是生成一个随机的 int 值,该值介于[0,n)的区间,
                // 也就是0到n之间的随机int值,包含0而不包含n。
                int num = new Random().nextInt(5);
                System.out.println(Thread.currentThread().getId() + "--" + System.currentTimeMillis() + "开始睡眠" + num + "秒");
                try {
                    TimeUnit.SECONDS.sleep(num);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // 创建线程池,自定义拒绝策略
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), Executors.defaultThreadFactory(), new RejectedExecutionHandler(){
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                // r 就是请求的任务,executor就是当前线程池
                System.out.println(r + " is discarding..");
            }
        });

        // 向线程池提交若干任务
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            threadPoolExecutor.submit(r);
        }
    }
}
  • 线程池工厂:ThreadFactory

线程池中的线程从哪儿来的? 答案就是 ThreadFactory。

public interface ThreadFactory {
    Thread newThread(Runnable r);
}

ThreadFactory 是一个接口,只有一个用来创建线程的方法:Thread newThread(Runnable r); 当线程池中需要创建线程时就会调用该方法

1、自定义线程池工厂

public class Test04ThreadFactory {
    public static void main(String[] args) throws InterruptedException {
        // 定义任务
        Runnable r = new Runnable() {
            @Override
            public void run() {
                int num = new Random().nextInt(10);
                System.out.println(Thread.currentThread().getId() + "--" + System.currentTimeMillis() + "开始睡眠:" + num + "秒");
                try {
                    TimeUnit.SECONDS.sleep(num);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // 创建线程池,使用自定义线程工厂,采用默认的拒绝策略是抛出异常
        ExecutorService executorService = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new SynchronousQueue<>(), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                // 根据参数r接收的任务,创建一个线程
                Thread t = new Thread( r );
                t.setDaemon(true);  // 设置为守护线程,当主线程运行结束,线程池中的线程会自动退出
                System.out.println("创建了线程: " + t);
                return t ;
            }
        });

        // 提交5个任务,当给当前线程池提交的任务超过5个时,线程池默认抛出异常
        for (int i = 0; i < 5; i++) {
            executorService.submit(r);
        }
   
        Thread.sleep(10000); // 主线程睡眠
        // 主线程睡眠超时,主线程结束,线程池中的线程会自动退出
    }
}

2、监控线程池

ThreadPoolExecutor 提供了一组方法用于监控线程池:
  int getActiveCount() 获得线程池中当前活动线程的数量
  long getCompletedTaskCount() 返回线程池完成任务的数量
  int getCorePoolSize() 线程池中核心线程的数量
  int getLargestPoolSize() 返回线程池曾经达到的线程的最大数
  int getMaximumPoolSize() 返回线程池的最大容量
  int getPoolSize() 当前线程池的大小
  BlockingQueue<Runnable> getQueue() 返回阻塞队列
  long getTaskCount() 返回线程池收到的任务总数

public class Test05_Montior_ThreadPool {
    public static void main(String[] args) throws InterruptedException {
        // 先定义任务
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId() + " 编号 的线程开始执行:" + System.currentTimeMillis());
                try {
                    Thread.sleep(10000);    // 线程睡眠10秒,模拟任务执行时长
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // 定义线程池:线程工厂是使用默认的线程工厂,有界队列5个
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2, 5,
                0, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardPolicy());

        // 向线程池提交30个任务
        for (int i = 0; i < 30; i++) {
            poolExecutor.submit(r);
            System.out.println("当前线程池核心线程数量:" + poolExecutor.getCorePoolSize());
            System.out.println("最大线程数:" + poolExecutor.getMaximumPoolSize());
            System.out.println("当前线程池大小:" + poolExecutor.getPoolSize());
            System.out.println("活动线程数量:" + poolExecutor.getActiveCount());
            System.out.println("收到任务数量:" + poolExecutor.getTaskCount());
            System.out.println("完成任务数:" + poolExecutor.getCompletedTaskCount());
            System.out.println("等待任务数:" + poolExecutor.getQueue().size());
            TimeUnit.MILLISECONDS.sleep(500);
        }

        System.out.println("-----------------------------------------------");
        while ( poolExecutor.getActiveCount() >= 0 ){
            System.out.println("当前线程池核心线程数量:" + poolExecutor.getCorePoolSize());
            System.out.println("最大线程数:" + poolExecutor.getMaximumPoolSize());
            System.out.println("当前线程池大小:" + poolExecutor.getPoolSize());
            System.out.println("活动线程数量:" + poolExecutor.getActiveCount());
            System.out.println("收到任务数量:" + poolExecutor.getTaskCount());
            System.out.println("完成任务数:" + poolExecutor.getCompletedTaskCount());
            System.out.println("等待任务数:" + poolExecutor.getQueue().size());
            Thread.sleep(1000);
        }
    }
}

3、扩展线程池

有时需要对线程池进行扩展,如在监控每个任务的开始和结束时间,或者自定义一些其他增强的功能。ThreadPoolExecutor 线程池提供了两个方法:
  protected void afterExecute(Runnable r, Throwable t) { }
  protected void beforeExecute(Thread t, Runnable r) { }
在线程池执行某个任务前会调用 beforeExecute() 方法,在任务结束后(任务异常退出)会执行 afterExecute() 方法。
查看 ThreadPoolExecutor 源码,在该类中定义了一个内部类 Worker,ThreadPoolExecutor 线程池中的工作线程就是 Worker 类的实例,Worker 实例在执行时会调用 beforeExecute() 与 afterExecute()方法。

public class Test06KuoZhan {
    // 定义任务类
    private static class  MyTask implements  Runnable{
         String name;

        public MyTask(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(name + "任务正在被线程 " + Thread.currentThread().getId() + " 执行");
            try {
                Thread.sleep(1000);     // 模拟任务执行时长
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 定义扩展线程池,可以定义线程池类继承 ThreadPoolExecutor
        // 在子类中重写 beforeExecute() / afterExecute()方法
        // 也可以直接使用 ThreadPoolExecutor 的内部类

        // 后面参数不写,就是默认,匿名内部类
        ExecutorService executorService = new ThreadPoolExecutor(5, 5,
                0, TimeUnit.SECONDS, new LinkedBlockingQueue<>() ){
				
            // 在匿名内部类中重写任务开始方法
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
                System.out.println(t.getId() + "线程准备执行任务: "
                        + ((MyTask)r).name);
            }

            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                System.out.println( ((MyTask)r).name + "任务执行完毕");
            }

            @Override
            protected void terminated() {
                System.out.println("线程池退出");
            }
        };

        // 向线程池中添加任务
        for (int i = 0; i < 5; i++) {
            MyTask task = new MyTask("task-" + i);
            executorService.execute(task);
        }

        // 关闭线程池
        executorService.shutdown();
        // 关闭线程池仅仅是说线程池不再接收新的任务,线程池中已接收的任务正常执行完毕
    }
}

4、优化线程池大小

线程池大小对系统性能是有一定影响的,过大或者过小都会无法发挥最优的系统性能,线程池大小不需要非常精确,只要避免极大或者极小的情况即可,一般来说,线程池大小需要考虑 CPU 数量,内存大小等因素。在《Java Concurrency in Practice》书中给出一个估算线程池大小的公式:
线程池大小 = CPU 的数量 * 目标 CPU 的使用率 * ( 1 + 等待时间与计算时间的比 )

5、线程池死锁

如果在线程池中执行的 任务A 在执行过程中又向线程池提交了 任务B,任务B 添加到了线程池的等待队列中,如果 任务A 的结束需要等待 任务B 的执行结果。就有可能会出现这种情况:线程池中所有的工作线程都处于等待任务处理结果,而这些任务在阻塞队列中等待执行,线程池中没有可以对阻塞队列中的任务进行处理的线程,这种等待会一直持续下去,从而造成死锁。
适合给线程池提交相互独立的任务,而不是彼此依赖的任务。对于彼此依赖的任务,可以考虑分别提交给不同的线程池来执行。

6、线程池中的异常处理

在使用 ThreadPoolExecutor 进行 submit 提交任务时,有的任务抛出了异常,但是线程池并没有进行提示,即线程池把任务中的异常给吃掉了,可以把 submit 提交改为 execute 执行,也可以对 ThreadPoolExecutor 线程池进行扩展。对提交的任务进行包装。
演示线程池可能会吃掉程序中的异常

public class Test07eat_exception {
    // 定义类实现Runnable接口,用于计算两个数相除
    private static class  DivideTask implements  Runnable{
        private  int x;
        private  int y;

        public DivideTask(int x, int y) {
            this.x = x;
            this.y = y;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "计算:" + x + " / " + y + " = " + (x/y));
        }
    }
    public static void main(String[] args) {
        // 创建线程池,直接提交队列
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(0,
                Integer.MAX_VALUE, 0, TimeUnit.SECONDS,
                new SynchronousQueue<>());

        // 向线程池中添加计算两个数相除的任务
        for (int i = 0; i < 5; i++) {
            poolExecutor.submit(new DivideTask(10, i));
//            poolExecutor.execute(new DivideTask(10, i));
        }
        /*
            运行程序,只有四条计算结果,我们实际上向线程池提交了5个计算任务,
            分析结果发现当 i == 0 时,提交的任务会产生算术异常,
            线程池把该异常给吃掉了,导致我们对该异常一无所知
            解决方法:
                一是把 submit() 提交方法改为 execute()
                二是对线程池进行扩展,对submit()方法进行包装
         */
    }
}
  • ForkJoinPool 线程池

“分而治之”是一个有效的处理大数据的方法,著名的 MapReduce 就是采用这种分而治之的思路。简单点说,如果要处理1000 个数据,但是我们不具备处理1000个数据的能力,可以只处理10个数据,可以把这1000个数据分阶段处理100次,每次处理10 个,把100次的处理结果进行合成,形成最后这1000个数据的处理结果。
把一个大任务调用 fork() 方法分解为若干小的任务,把小任务的处理结果进行 join() 合并为大任务的结果。
ForkJoinPool
系统对 ForkJoinPool 线程池进行了优化,提交的任务数量与线程的数量不一定是一对一关系。在多数情况下,一个物理线程实际上需要处理多个逻辑任务。
系统对 ForkJoinPool 线程池进行了优化
ForkJoinPool 线程池中最常用的方法是:

public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task) {
    return externalSubmit(task);
}

<T> ForkJoinTask<T> submit(ForkJoinTask<T> task)向线程池提交一个 ForkJoinTask 任务。ForkJoinTask 任务支持 fork() 分解与 join() 等待的任务。ForkJoinTask 有两个重要的子类:RecursiveAction 和 RecursiveTask,它们的区别在于 RecursiveAction 任务没有返回值,RecursiveTask 任务可以带有返回值。

public abstract class RecursiveAction extends ForkJoinTask<Void> {
	protected abstract void compute();
}
public abstract class RecursiveTask<V> extends ForkJoinTask<V> {
	V result;
	protected abstract V compute();
}

1、演示ForkJoinPool线程池的使用,使用该线程池模拟数列求和

public class Test09ForkJoinPool {
    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool(); // 创建 ForkJoinPool 线程池
        CountTask task = new CountTask(0L, 200000L); // 创建一个大的任务
        ForkJoinTask<Long> result = forkJoinPool.submit(task); // 把大任务提交给线程池
        try {
            Long res = result.get(); // 调用任务的 get() 方法返回结果
            System.out.println("计算数列结果为:" + res);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        // 验证
        long s = 0L;
        for (long i = 0; i <= 200000 ; i++) {
            s += i;
        }
        System.out.println("验证数列结果为:" + s);
    }

    // 计算数列的和,需要返回结果,可以定义任务继承【RecursiveTask】、RecursiveAction 无返回值,在此不合适
    private static class CountTask extends RecursiveTask<Long>{ // 这里的泛型,就是返回值类型
        // 定义数据规模的阈值,允许计算10000个数内的和,超过该阈值的数列就需要分解
        private static final int THRESHOLD = 10000;
        private static final int TASKNUM = 100; // 定义每次把大任务分解为100个小任务
        private long start; // 计算数列的起始值
        private long end; // 计算数列的结束值

        public CountTask(long start, long end) {
            this.start = start;
            this.end = end;
        }

        // 重写 RecursiveTask 类的 compute() 方法,计算数列的结果
        @Override
        protected Long compute() {
            long sum = 0 ; // 保存计算的结果
            // 判断任务是否需要继续分解,如果当前数列 end 与 start 范围的数超过阈值 THRESHOLD,就需要继续分解
            if ( end - start < THRESHOLD){
                // 小于阈值可以直接计算
                for (long i = start ; i <= end; i++){
                    sum += i;
                }
            } else { // 数列范围超过阈值,需要继续分解
                // 约定每次分解成100个小任务,计算每个任务的计算量
                long step = (start + end ) / TASKNUM;
                // start = 0,end = 200000,step = 2000,如果计算[0, 200000]范围内数列的和
                // 把该范围的数列分解为100个小任务,每个任务计算2000个数即可
                //注意,如果任务划分的层次很深,即 THRESHOLD 阈值太小,每个任务的计算量很小
                // 层次划分就会很深,可能出现两种情况:
                // 一是系统内的线程数量会越积越多,导致性能下降严重;
                // 二是分解次数过多,方法调用过多可能会导致栈溢出

                // 创建一个存储任务的集合
                ArrayList<CountTask> subTaskList = new ArrayList<>();
                long pos = start; // 每个任务的起始位置
                for (int i = 0; i < TASKNUM; i++) {
                    long lastOne = pos + step; // 每个任务的结束位置
                    // 调整最后一个任务的结束位置
                    if ( lastOne > end ){
                        lastOne = end;
                    }
                    // 创建子任务
                    CountTask task = new CountTask(pos, lastOne);
                    // 把任务添加到集合中
                    subTaskList.add(task);
                    // 调用 fork() 提交子任务
                    task.fork();
                    // 调整下个任务的起始位置
                    pos += step + 1;
                }

                // 等待所有的子任务结束后,合并计算结果
                for (CountTask task : subTaskList) {
                    sum += task.join(); // join() 会一直等待子任务执行完毕返回执行结果
                }
            }
            return sum;
        }
    }
}

保障线程安全的设计技术

从面向对象设计的角度出发介绍几种保障线程安全的设计技术,这些技术可以使得我们在不必借助锁的情况下保障线程安全,避免锁可能导致的问题及开销。

  • Java 运行时存储空间

Java运行时(Java runtime)空间可以分为栈区,堆区与方法区(非堆空间)。

栈空间(Stack Space)是为线程的执行准备的一段固定大小的存储空间, 每个线程都有独立的线程栈空间,创建线程时就为线程分配栈空间。在线程栈中每调用一个方法就给方法分配一个栈帧,栈帧用于存储方法的局部变量,返回值等私有数据,即局部变量存储在栈空间中,基本类型变量也是存储在栈空间中, 引用类型变量值也是存储在栈空间中,引用的对象存储在堆中。由于线程栈是相互独立的,一个线程不能访问另外一个线程的栈空间,因此线程对局部变量以及只能通过当前线程的局部变量才能访问的对象,进行的操作具有固定的线程安全性。

堆空间(Heap Space)用于存储对象,是在 JVM 启动时分配的一段可以动态扩容的内存空间。创建对象时,在堆空间中给对象分配存储空间,实例变量就是存储在堆空间中的,堆空间是多个线程之间可以共享的空间,因此实例变量可以被多个线程共享。多个线程同时操作实例变量可能存在线程安全问题。

非堆空间(Non-Heap Space)用于存储常量,类的元数据等,非堆空间也是在 JVM 启动时分配的一段可以动态扩容的存储空间。类的元数据包括静态变量,类有哪些方法及这些方法的元数据(方法名,参数,返回值等)。非堆空间也是多个线程可以共享的,因此访问非堆空间中的静态变量也可能存在线程安全问题。

堆空间和非堆空间是线程可以共享的空间,即实例变量与静态变量是线程可以共享的,可能存在线程安全问题。栈空间是线程私有的存储空间,局部变量存储在栈空间中,局部变量具有固定的线程安全性。

  • 无状态对象

对象就是数据及对数据操作的封装,对象所包含的数据称为对象的状态(State),实例变量与静态变量称为状态变量。如果一个类的同一个实例被多个线程共享并不会使这些线程存在共享的状态,那么该类的实例就称为无状态对象(Stateless Object)。 反之如果一个类的实例被多个线程共享会使这些线程存在共享状态,那么该类的实例称为有状态对象。实际上无状态对象就是不包含任何实例变量也不包含任何静态变量的对象。线程安全问题的前提是多个线程存在共享的数据,实现线程安全的一种办法就是避免在多个线程之间共享数据,使用无状态对象就是这种方法。

不可变对象

不可变对象是指一经创建它的状态就保持不变的对象,不可变对象具有固有的线程安全性。当不可变对象现实实体的状态发生变化时,系统会创建一个新的不可变对象,就如 String 字符串对象。一个不可变对象需要满足以下条件:
  1)类本身使用 final 修饰,防止通过创建子类来改变它的定义
  2)所有的字段都是 final 修饰的,final 字段在创建对象时必须显示初始化,不能被修改
  3)如果字段引用了其他状态可变的对象(集合,数组),则这些字段必须是 private 私有的
不可变对象主要的应用场景:
  1)被建模对象的状态变化不频繁
  2)同时对一组相关数据进行写操作,可以应用不可变对象,既可以保障原子性也可以避免锁的使用
  3)使用不可变对象作为安全可靠的 Map 键,HashMap 键值对的存储位置与键的 hashCode() 有关,如果键的内部状态发生了变化会导致键的哈希码不同,可能会影响键值对的存储位置。如果HashMap的键是一个不可变对象,则 hashCode() 方法的返回值恒定,存储位置是固定的。

  • 线程特有对象

我们可以选择不共享非线程安全的对象,对于非线程安全的对象,每个线程都创建一个该对象的实例,各个线程访问各自创建的实例,一个线程不能访问另外一个线程创建的实例。这种各个线程创建各自的实例,一个实例只能被一个线程访问的对象就称为线程特有对象。线程特有对象既保障了对非线程安全对象的访问的线程安全,又避免了锁的开销。线程特有对象也具有固有的线程安全性。ThreadLocal<T> 类相当于线程访问其特有对象的代理,即各个线程通过 ThreadLocal 对象可以创建并访问各自的线程特有对象,泛型 T 指定了线程特有对象的类型。一个线程可以使用不同的 ThreadLocal 实例来创建并访问不同的线程特有对象。
ThreadLocal 对象
ThreadLocal 实例为每个访问它的线程都关联了一个该线程特有的对象,ThreadLocal 实例都有当前线程与特有实例之间的一个关联。

  • 装饰器模式

装饰器模式可以用来实现线程安全,基本思想是为非线程安全的对象创建一个相应的线程安全的外包装对象,客户端代码不直接访问非线程安全的对象而是访问它的外包装对象。
外包装对象与非线程安全的对象具有相同的接口,即外包装对象的使用方式与非线程安全对象的使用方式相同,而外包装对象内部通常会借助锁,以线程安全的方式调用相应的非线程安全对象的方法。
在 java.util.Collections 工具类中提供了一组 synchronizedXXX(xxx) 可以把不是线程安全的 xxx 集合转换为线程安全的集合,它就是采用了这种装饰器模式。这个方法返回值就是指定集合的外包装对象。这类集合又称为同步集合。

static class SynchronizedCollection<E> implements Collection<E>, Serializable {...}

static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {...}

private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {...}

static class SynchronizedSet<E> extends SynchronizedCollection<E> implements Set<E> {...}

使用装饰器模式的一个好处就是实现关注点分离,在这种设计中,实现同一组功能的对象的两个版本:
非线程安全的对象与线程安全的对象。
对于非线程安全的在设计时只关注要实现的功能,对于线程安全的版本只关注线程安全性。

锁的优化及注意事项

  • 有助于提高锁性能的几点建议

1、减少锁持有时间
对于使用锁进行并发控制的应用程序来说,如果单个线程特有锁的时间过长,会导致锁的竞争更加激烈,会影响系统的性能。在程序中需要尽可能减少线程对锁的持有时间,如下面代码:

public synchronized void syncMethod(){
	othercode1();
	mutexMethod();
	othercode();
}

在 syncMethod 同步方法中,假设只有 mutexMethod() 方法是需要同步的,othercode1() 方法与 othercode2() 方法不需要进行同步。如果othercode1() 与 othercode2() 这两个方法需要花费较长的CPU时间,在并发量较大的情况下,这种同步方案会导致等待线程的大量增加。一个较好的优化方案是,只在必要时进行同步,可以减少锁的持有时间,提高系统的吞吐量,如把上面的代码改为:

public void syncMethod(){
	othercode1();
	synchronized (this) {
		mutexMethod();
	}
	othercode();
}

只对 mutexMethod()方法进行同步,这种减少锁持有时间有助于降低锁冲突的可能性,提升系统的并发能力。

2、减小锁的粒度
一个锁保护的共享数据的数量大小称为锁的粒度。如果一个锁保护的共享数据的数量大就称该锁的粒度粗,否则称该锁的粒度细。锁的粒度过粗会导致线程在申请锁时需要进行不必要的等待。减少锁粒度是一种削弱多线程锁竞争的一种手段,可以提高系统的并发性。
在 JDK7 前,java.util.concurrent.ConcurrentHashMap 类采用分段锁协议,可以提高程序的并发性。

3、使用读写分离锁代替独占锁
使用 ReadWriteLock 读写分离锁可以提高系统性能,使用读写分离锁也是减小锁粒度的一种特殊情况。第二条建议是能分割数据结构实现减小锁的粒度,那么读写锁是对系统功能点的分割。
在多数情况下都允许多个线程同时读,在写的使用采用独占锁,在读多写少的情况下,使用读写锁可以大大提高系统的并发能力。

4、锁分离
将读写锁的思想进一步延伸就是锁分离。读写锁是根据读写操作功能上的不同,进行了锁分离。根据应用程序功能的特点,也可以对独占锁进行分离。如 java.util.concurrent.LinkedBlockingQueue 类中 take() 与 put() 方法分别从队头取数据,把数据添加到队尾。虽然这两个方法都是对队列进行修改操作,由于操作的主体是链表,take() 操作的是链表的头部,put()操作的是链表的尾部,两者并不冲突。如果采用独占锁的话,这两个操作不能同时并发,在该类中就采用锁分离,take() 取数据时有取锁,put() 添加数据时有自己的添加锁,这样 take() 与 put() 相互独立实现了并发。

5、粗锁化
为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短。但是凡事都有一个度,如果对同一个锁不断的进行请求,同步和释放,也会消耗系统资源。如:

public void method1(){
	synchronized( lock ){
		同步代码块 1
	}
	synchronized( lock ){
		同步代码块 2
	}
}

JVM 在遇到一连串不断对同一个锁进行请求和释放操作时,会把所有的锁整合成对锁的一次请求,从而减少对锁的请求次数,这个操作叫锁的粗化,如上一段代码会整合为:

public void method1(){
	synchronized( lock ){
		同步代码块 1
		同步代码块 2
	}
}

在开发过程中,也应该有意识的在合理的场合进行锁的粗化,尤其在循环体内请求锁时,如:

for(int i = 0 ; i< 100; i++){
	synchronized(lock){}
}

这种情况下,意味着每次循环都需要申请锁和释放锁,所以一种更合理的做法就是在循环外请求一次锁,如:

synchronized( lock ){
	for(int i = 0 ; i< 100; i++){}
}
  • JVM 对锁的优化

1、锁偏向

锁偏向是一种针对加锁操作的优化,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无须再做任何同步操作,这样可以节省有关锁申请的时间,提高了程序的性能。
锁偏向在没有锁竞争的场合可以有较好的优化效果,对于锁竞争比较激烈的场景,效果不佳,锁竞争激烈的情况下可能是每次都是不同的线程来请求锁,这时偏向模式失效。

2、轻量级锁

如果锁偏向失败,JVM 不会立即挂起线程,还会使用一种称为轻量级锁的优化手段。会将对象的头部作为指针,指向持有锁的线程堆栈内部,来判断一个线程是否持有对象锁。如果线程获得轻量级锁成功,就进入临界区。如果获得轻量级锁失败,表示其他线程抢到了锁,那么当前线程的锁的请求就膨胀为重量级锁。当前线程就转到阻塞队列中变为阻塞状态。
偏向锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化时,没有任何线程访问它,它是可偏向的,即它认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,偏向第一个线程,这个线程在修改对象头成为偏向锁时使用 CAS 操作,将对象头中 ThreadId 改成自己的ID,之后再访问这个对象时,只需要对比 ID 即可。 一旦有第二个线程访问该对象,因为偏向锁不会主动释放,所以第二个线程可以查看对象的偏向状态,当第二个线程访问对象时,表示在这个对象上已经存在竞争了,检查原来持有对象锁的线程是否存活,如果挂了则将对象变为无锁状态,然后重新偏向新的线程;如果原来的线程依然存活,则马上执行原来线程的栈,检查该对象的使用情况,如果仍然需要偏向锁,则偏向锁升级为轻量级锁。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对同一个锁的操作会错开,或者稍微等待一下(自旋)另外一个线程就会释放锁。当自旋超过一定次数,或者一个线程持有锁,一个线程在自旋,又来第三个线程访问时,轻量级锁会膨胀为重量级锁,重量级锁除了持有锁的线程外,其他的线程都阻塞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值