Java线程池初步了解

线程池

线程池初始化时即创建一些空闲的线程。当程序将一个任务提交给线程池时,线程池就会选择一个空闲的线程来执行该任务。在任务结束以后,该线程并不会死亡,而是由线程池回收成为空闲状态,等待执行下一个任务。

线程池状态

线程池状态有如下几种(来源于网上):
RUNNING:运行时状态,可以接受新的任务,并处理等待队列中的任务
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务
STOP:不接受新的任务提交,也不再处理等待队列中的任务,中断正在执行任务的线程。
TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
TERMINATED:terminated()方法结束后,线程池的状态就会变成这个

工作机制

线程池工作时,任务是提交给线程池。线程池接到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程;如果没有,则查看是否需要创建新的线程去处理。如果已达到最大线程数,则被暂存在一个任务队列中;如果是有限队列且满了,则执行拒绝策略。

整个流程源码在java.util.concurrent.ThreadPoolExecutor#execute.execute(Runnable command) 方法中:

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

过程

线程池的构建

(图从网上找的)


创建线程池时有7个重要的参数:

int corePoolSize :初时线程池大小
int maximumPoolSize :最大线程数
long keepAliveTime:空闲多久回收线程
TimeUnit unit :时间单位
threadFactory :用来创建线程的工厂
BlockingQueue<Runnable> workQueue :任务队列
RejectedExecutionHandler handler:队列满了后,拒绝的callback

 

原生线程池

Java自带了7种线程池:

方法

最大线程数

队列

队列满了后

适用场景

newSingleThreadExecutor()

1

无界

/

 

newCachedThreadPool()

Integer.MAX_VALUE

无界

/

缓存线程,适合大量短时间任务

newFixedThreadPool(int nThreads)

参数指定

无界

/

空闲则销毁,需要则补足

newSingleThreadScheduledExecutor()

1

DelayedWorkQueue

 

返回ScheduledExecutorService,定时或周期性的工作调度

newScheduledThreadPool(int corePoolSize)

参数指定

DelayedWorkQueue

 

同上,只是线程数不通而已

newWorkStealingPool(int parallelism)

 

 

 

Java8新加,Work-Stealing算法,并行地处理任务,不保证处理顺序

new ThreadPoolExecutor()

参数指定

参数指定

参数指定

最基本的

 

队列

线程池队列分两种:有界队列、无界队列。也可以分阻塞队列与非阻塞队列:阻塞队列有SynchronousQueue、LinkedBlockingQueue、ArrayBlockingQueue与PriorityBlockingQueue,非阻塞队列有ConcurrentLinkedQueue与LinkedTransferQueue。

阻塞队列:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。生产者-消费者设计模式就是用的阻塞队列,这样可以简化编码。内部实现都是用锁实现。
非阻塞队列:一般使用CAS替代锁。
SynchronousQueue :即同步队列,是BlockingQueue 接口的实现。从名字可知,每次往队列里put 一个元素后,就会阻塞,直到有人task 该元素。
只是它是内部只能包含一个元素的队列。拥有公平(FIFO)和非公平(LIFO)策略,对应的就是两个内部类TransferQueue、TransferStack。

LinkedBlockingQueue :是一个无界队列,入对用的是putLock 锁,出对用的是takeLock 锁。
ArrayBlockingQueue :利用数组实现的一个队列,初始化时指定队列大小。

PriorityBlockingQueue:即优先级队列,意思就是可以控制任务的执行先后顺序。它是一个无界队列。因为有优先级,所以队列里的元素必须要实现comparable 接口。

ConcurrentLinkedQueue是使用CAS来实现非阻塞入队出队。

这里简单说下优先级队列的例子:

首先,需要缓存的对象需要实现Comparable接口

public class Event implements Comparable<Event> {
    private int priority;

    private String name;

    public Event(String name, int priority) {
        this.name = name;
        this.priority = priority;
    }

    @Override
    public int compareTo(Event event) {
        return this.priority - event.priority;
    }

    public int getPriority() {
        return priority;
    }

    public String getName() {
        return name;
    }
}

然后把元素加入队列,并用线程池去消费

public class TryPriorityBlockingQueue {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        PriorityBlockingQueue queue = new PriorityBlockingQueue<Event>();

        queue.put(new Event("Third", 3));
        queue.put(new Event("Fourth", 4));
        queue.put(new Event("First", 1));
        queue.put(new Event("Second", 2));

        ExecutorService executor = Executors.newCachedThreadPool();
        executor.submit(new Runnable() {
            @Override
            public void run() {
                while (!queue.isEmpty()) {
                    try {
                        Event event = (Event) queue.take();
                        System.out.println("event " + event.getName());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                countDownLatch.countDown();
            }
        });
        countDownLatch.await();
        executor.shutdown();
    }
}

从输出结果看,确实是根据优先级进行消费的。

其实优先级队列本身是用堆实现的。

大顶堆:二叉树的父节点大于左右子节点

小顶堆:二叉树的父节点小于左右子节点

 

 

 

策略

线程池创建时,需要告诉线程池:当线程池处理不了新的任务时,怎么处理。Java默认提供了4种策略可供选择。

4个策略。分别是:
CallerRunsPolicy。提交任务的线程自己负责执行这个任务。
AbortPolicy。使Executor抛出异常,通过异常做处理。
DiscardPolicy。丢弃提交的任务。
DiscardOldestPolicy。丢弃掉队列中最早加入的任务

 

ForkJoin

ForkJoin 是Java 提供的并发处理框架。基本做法是分而治之,即把一个负责任务拆成多个小任务,然后把所有小任务的结果汇总。

核心类主要就是ForkJoinTask 抽象类与 ForkJoinPool。使用ForkJoin 时,只需要继承它的子类 RecursiveAction 或者 RecursiveTask 即可,并实现compute 方法。

复写实现 compute 方法一般需要遵循这个规则:

if(任务足够小){
  直接执行任务;
  如果有结果,return结果;
} else {
  拆分为2个子任务;
  分别执行子任务的fork方法;
  分别执行子任务的join方法;
  如果有结果,return合并结果;
}

例如:

public class MyForkJoinTask extends RecursiveTask<Integer> {
    private List<Integer> needProcess;

    private String start;

    public MyForkJoinTask(String start, List<Integer> needProcess) {
        this.start = start;
        this.needProcess = needProcess;
    }

    @Override
    protected Integer compute() {
        try {
            if (needProcess.size() < 5) {
                return needProcess.stream().max((i, j) -> i - j).orElse(0);
            }
            int meddle = needProcess.size() / 2;
            MyForkJoinTask left = new MyForkJoinTask(start + ".0", needProcess.subList(0, meddle));
            MyForkJoinTask right = new MyForkJoinTask(start + ".1", needProcess.subList(meddle + 1, needProcess.size()));
            left.fork();
            right.fork();
            Integer leftResult = left.get();
            System.out.println("left " + left.start + " get " + leftResult);
            Integer rightResult = right.get();
            System.out.println("right " + right.start + " get " + rightResult);

            return Math.max(leftResult, rightResult);
        } catch (Exception e) {
            System.out.println("failed");
        }
        return -1;
    }
}

这里我利用task 分治取得最大值。对应的还要新建一个Pool

public class TestForkJoinTask {
    public static void main(String[] args) {
        List<Integer> input = Lists.newArrayList(14, 4, 69, 23, 90, 45, 78, 12, 37, 49, 48, 100, 24, 38, 49, 58, 29, 344, 1, 231, 124, 90, 38, 200, 125, 178, 158, 16, 159, 150, 25, 27, 49, 410, 387, 49, 37, 49, 151, 172, 25, 59, 289, 20);

        ForkJoinPool forkJoinPool = new ForkJoinPool();
        Integer max = forkJoinPool.invoke(new MyForkJoinTask("0", input));

        System.out.println("Result is " + max);
    }
}

此时就可以把比较路径输出了。

它使用了一个无限队列来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。因此线程数是可控的。

fork()方法类似于线程的Thread.start()方法,但是它不是真的启动一个线程,而是将任务放入到工作队列中。
join()方法类似于线程的Thread.join()方法,但是它不是简单地阻塞线程,而是利用工作线程运行其它任务。当一个工作线程中调用了join()方法,它将处理其它任务,直到注意到目标子任务已经完成了。

工作窃取
1.每个工作线程都有自己的工作队列WorkQueue;
2.这是一个双端队列,它是线程私有的;
3.ForkJoinTask中fork的子任务,将放入运行该任务的工作线程的队头,工作线程将以LIFO的顺序来处理工作队列中的任务;
4.为了最大化地利用CPU,空闲的线程将从其它线程的队列中“窃取”任务来执行;
5.从工作队列的尾部窃取任务,以减少竞争;
6.双端队列的操作:push()/pop()仅在其所有者工作线程中调用,poll()是由其它线程窃取任务时调用的;
7.当只剩下最后一个任务时,还是会存在竞争,是通过CAS来实现的;

说到这,就不得不提下Java 8的新特性parallelStream与parallel。它俩实现原理正是ForJoinPool。

如下代码:

    @Test
    public void should_print_sequence_1() {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
        //  顺序输出
        numbers.stream().forEach(System.out::println);
    }

这里因为元素有序的,经过流依旧是顺序打印输出。改成下边就会出现乱序,即并行流:

// 乱序输出
numbers.parallelStream().forEach(System.out::println);

为了看清,我能把线程id输出:

        numbers.parallelStream().forEach(s -> {
            System.out.println("print " + s + " from " + Thread.currentThread().getId());
        });

从输出发现线程ID确实不一致,也就是parallelStream 利用了多线程。

另外stream().parallel() 等于 parallelStream。即:

numbers.stream().parallel().forEach(System.out::println);

接下来,尝试加上 forEachOrdered

        // 顺序输出
        numbers.parallelStream().forEachOrdered(s -> {
            System.out.println("print " + s + " from " + Thread.currentThread().getId());
        });

从输出看,不再是并行。

再看下直接利用ForJoinPool:

        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);

        ForkJoinPool forkJoinPool = new ForkJoinPool();// 默认创建jvm处理器核数的并行度
        forkJoinPool.submit(() -> {
            numbers.parallelStream().forEach(s -> {
                System.out.println("print " + s + " from " + Thread.currentThread().getId());
            });
        }).join();

注意:任务之间最好是状态无关的,因为parallelStream默认是非线程安全的。
另外,如果在lambda 表达式操作集合最好用 Collections.synchronizedList(new ArrayList<>()); 转成线程安全的。

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值