Java多线程(下)——线程池、并发工具类、ThreadLocal

1 ThreadPoolExecutor

1.1 新建线程池

用法:

ThreadPoolExecutor(int corePoolSize, //核心线程数

int maximumPoolSize, //最大线程数

long keepAliveTime, //线程存活时间

TimeUnit unit, //时间单位

BlockingQueue workQueue, //阻塞队列

RejectedExecutionHandler handler) //饱和策略

 

先用一个小例子感性地认识下线程池:

public class ThreadPoolTest {
    private static AtomicInteger ai = new AtomicInteger();

    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 8,
                500, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(8),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 20; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(ai.getAndIncrement());
                }
            });
        }
    }
}

 

 

Java多线程(下)——线程池、并发工具类、ThreadLocal_第1张图片

理解下线程池基本原理:

Java多线程(下)——线程池、并发工具类、ThreadLocal_第2张图片

当提交一个新的任务到线程池时,处理流程如下:

1)判断核心线程池(corePool)里面还有没有空闲线程,如果还有空闲就创建新的工作线程来执行,如果没有就进入流程2;

2)判断阻塞队列(workQueue)是否已满,如果没满,就把新提交的任务放在阻塞队列里,如果满了就进入流程3;

3)判断线程池里忙碌的线程数是否达到最大(maximumPoolSize),如果还没有,就创建新的工作线程来执行,如果已经满了,就交给饱和策略处理。

 

1.阻塞队列有四种:

1)ArrayBlockingQueue:基于数组结构,必须指定容量capacity;

2)LinkedBlockingQueue:基于链表结构,可以指定容量capacity,也可以不指定,不指定就是Integer.MAX_VALUE;

3)SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入一直处于阻塞状态。

4)PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

 

2.饱和策略有四种:

1)AbortPolicy:直接抛出异常。注意了,这里虽然抛异常了,但是进程并没有结束。

继续使用上面示例的代码,我们知道最多能有8+8=16个线程,超过这个数字就拒绝执行并抛异常了,结果与预期符合,0-15都正常运行了,下一个线程不执行了:

Java多线程(下)——线程池、并发工具类、ThreadLocal_第3张图片

看看这种策略的源码,在拒绝执行(rejectedExecution)时抛出了异常:

Java多线程(下)——线程池、并发工具类、ThreadLocal_第4张图片

2)DiscardPolicy:直接丢弃掉任务,不执行也不报错。

看下示例代码,饱和策略换成DiscardPolicy后,丢失了打印16-19的任务。

Java多线程(下)——线程池、并发工具类、ThreadLocal_第5张图片

看看源码,拒绝执行时啥也没做:

Java多线程(下)——线程池、并发工具类、ThreadLocal_第6张图片

3)DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

正常执行打印0-15的任务,从16开始,丢弃最近的任务,所以16-19的打印被丢弃了。

Java多线程(下)——线程池、并发工具类、ThreadLocal_第7张图片

源码:

Java多线程(下)——线程池、并发工具类、ThreadLocal_第8张图片

4)CallerRunsPolicy:调用者执行

看看第一轮打印结果:打印了9个数字,但是线程池最大8,所以第9个数字是调用者线程(在这里例子中就是main线程执行的)。

Java多线程(下)——线程池、并发工具类、ThreadLocal_第9张图片

这样做,main线程就不能干别的了。我们给线程池后面加个打印任务:

public class ThreadPoolTest {
    private static AtomicInteger ai = new AtomicInteger();

    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 8,
                500, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(8),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 20; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(ai.getAndIncrement());
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        System.out.println("main线程执行");
    }
}

看到main线程被线程池征用了,第一轮打印0-8,第二轮打印9-17,第三轮就是main线程任务和打印18-20同时进行了。

Java多线程(下)——线程池、并发工具类、ThreadLocal_第10张图片

源码:

Java多线程(下)——线程池、并发工具类、ThreadLocal_第11张图片

1.2 向线程池提交任务

有两种方法:execute()和submit。区别在与,execute无返回值,而submit返回Future,这样就可以通过Future的get()阻塞住。

1.2.1 execute

只能提交Runnable任务,不能是Callable的:

1.2.2 submit

可以提交Runnable任务

Java多线程(下)——线程池、并发工具类、ThreadLocal_第12张图片

也可以提交Callable任务:

Java多线程(下)——线程池、并发工具类、ThreadLocal_第13张图片

当运行Callable任务的时候,可以返回值,还可以抛异常

public class ThreadPoolTest {
    private static AtomicInteger ai = new AtomicInteger();

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 8,
                500, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(20),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 20; i++) {
            Future future = threadPool.submit(new Callable() {
                @Override
                public String call() throws InterruptedException {
                    Thread.sleep(3000);
                    int val = ai.getAndIncrement();
                    return String.valueOf(val);
                }
            });
            String val = future.get();
            System.out.println(val);
        }
    }
}

观察输出结果可以看到数字是一个一个打印的,间隔3000ms,说明get()阻塞住了。

1.3 关闭线程池

shutdown、shutdownNow

区别:

1)shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有正在执行或暂停任务的线程,并返回等待执行任务的列表。

Java多线程(下)——线程池、并发工具类、ThreadLocal_第14张图片

Java多线程(下)——线程池、并发工具类、ThreadLocal_第15张图片

2)shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断没有在执行的任务。

Java多线程(下)——线程池、并发工具类、ThreadLocal_第16张图片

shutdown和shutdownNow无论执行哪个,isShutdown都会返回false,但是只有线程池真的关闭了,调用isTerminated才会返回true。

注意:

“中断”就是调用线程的interrupt方法,所以我们在线程池里起线程的时候不要捕获InterruptedException ,不然的话线程有可能停不下来,更蛋疼的是,不仅没停下来,而且还写了个死循环,鄙人之前搞出过大锅。举个例子吧:

public class ThreadPoolTest {
    private static AtomicInteger ai = new AtomicInteger();

    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 8,
                500, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(20),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 20; i++) {
            Future future = threadPool.submit(new Callable() {
                @Override
                public String call() {
                    while (true) {
                        try {
                            Thread.sleep(3000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        System.out.println("根本停不下来");
                    }
                }
            });
            threadPool.shutdown();
        }
    }
}

 

 

Java多线程(下)——线程池、并发工具类、ThreadLocal_第17张图片

future.get()可以设置超时机制:future.get(long timeout, TimeUnit unit),如果超时未执行完就会给线程抛一个中断标志,这个方法会有三个异常:

我们写个小例子:

public class ThreadPoolTest {
    private static AtomicInteger ai = new AtomicInteger();

    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 8,
                500, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(20),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 1; i++) {
            Future future = threadPool.submit(new Callable() {
                @Override
                public String call(){
                    throw new NullPointerException("我抛的");
                }
            });

            String val = null;

            try {
                val = future.get(2, TimeUnit.SECONDS);
            } catch (TimeoutException e) {
                System.out.println("超时异常");
            } catch (ExecutionException e) {
                System.out.println("内部任务异常");
                System.out.println("实际异常是:" + e.getCause());
            } catch (InterruptedException e){
                System.out.println("中断异常");
            }

            System.out.println("666");
        }
    }
}

可以看到,内部的异常都被封装成了ExecutionException,不过我们仍然可以通过e.getCause()获取实际的异常:

2 Executors框架

Executors给我们封装了一些实现,简化线程池配置,下面讲解该框架提供的4种线程池:

newFixedThreadPool

newCachedThreadPool

newSingleThreadExecutor

newScheduledThreadPool

2.1 newFixedThreadPool

ExecutorService executor = Executors.newFixedThreadPool(4);

核心线程数和最大线程数相同,阻塞队列无限大(Integer.MAX_VALUE),所以任务再多,也能hold住,用起来就是火力全开,起nThreads个线程(最多这么多)。

开箱即用,拎包入住,只要设置一个线程池的大小就能用起来了。缺点也很明显,任务数量变得不可控,队列里缓存太多任务,搞不好就把内存干溢出了,所以阿里的java开发手册建议还是用ThreadPoolExecutor。

2.2 newCachedThreadPool

ExecutorService executor = Executors.newCachedThreadPool();

Java多线程(下)——线程池、并发工具类、ThreadLocal_第18张图片

不用指定线程池大小,核心线程数0,最大线程数无穷大,阻塞队列用的SynchronousQueue,这种队列一个元素也不存,空闲线程竟然能存活60s之久,这种线程池简直就是性能小怪兽,吃内存和cpu不吐骨头。

执行新任务的时候也是先看看线程池里面有没有空闲的线程,有的话直接执行,没有就新建一个线程来执行。

2.3 newSingleThreadExecutor

ExecutorService executor = Executors.newSingleThreadExecutor();

如果说cachedThreadPool是小怪兽的话,那singleThreadExecutor简直就是个废物,因为它每次只执行一个任务。

Java多线程(下)——线程池、并发工具类、ThreadLocal_第19张图片

核心线程数是1,最大线程数也是1,唯一的好处是阻塞队列是无界的,可以暂存Integer.MAX_VALUE个任务。

这种线程池存在的价值在于可以顺序执行一堆任务,那我不能用for循环执行吗,可以,但是不能异步执行。那我不能在for循环里new一个Thread吗,嗯好像也行,但是你没法控制一次有多少个线程执行呀。当然你又说了,那我可以用信号量控制呀,好吧也行,但是那样太复杂了,人生苦短,我们简单点就好。

2.4 newScheduledThreadPool

ScheduledExecutorService executor = Executors.newScheduledThreadPool(8);

(1)executor.schedule(Runnable command, long delay, TimeUnit unit)

延迟delay时间单位之后执行:

public class ThreadPoolTest {
    private static AtomicInteger ai = new AtomicInteger();

    public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(8);
        for (int i = 0; i < 20; i++) {
            executor.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println(ai.getAndIncrement());
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, 5, TimeUnit.SECONDS);
        }
    }
}

(2)executor.scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)

先等待initialDelay单位时间,然后每delay单位时间执行一次任务。

public class ThreadPoolTest {
    private static AtomicInteger ai = new AtomicInteger();

    public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(8);
        for (int i = 0; i < 20; i++) {
            executor.scheduleWithFixedDelay(new Runnable() {
                @Override
                public void run() {
                    System.out.println(ai.getAndIncrement());
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, 10, 3, TimeUnit.SECONDS);
        }
    }
}

3 并发工具类

CountDownLatch、CyclicBarrier、Semophore

3.1 CountDownLatch

先来个英语八级词汇:Latch,是啥?哈哈,有道查了下,是门闩的意思,很形象,想象一下,门上有两把门闩,俩都拉开的时候门就能开了。

public class ThreadPoolTest {
    private static AtomicInteger ai = new AtomicInteger();
    private static CountDownLatch count = new CountDownLatch(4);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 4; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(ai.getAndIncrement());
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count.countDown();
                }
            }).start();
        }
        count.await();
        System.out.println("门闩开啦,炒房团进来啦");
    }
}

这个就跟前面介绍的Thread.join()类似,就是当前线程等待别的线程执行完了再执行。

原理就是await时加共享锁,countDown时释放共享锁。

3.2 CyclicBarrier

又是一个八级词汇,论英语的重要性:循环使用的屏障

public class ThreadPoolTest {
    private static AtomicInteger ai = new AtomicInteger();
    private static CyclicBarrier barrier = new CyclicBarrier(4, new Runnable() {
        @Override
        public void run() {
            System.out.println("你们都执行完了吧?该轮到我了吧?");
        }
    });

    public static void main(String[] args) {
        for (int i = 0; i < 4; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(ai.getAndIncrement());
                    try {
                        Thread.sleep(3000);
                        barrier.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

Java多线程(下)——线程池、并发工具类、ThreadLocal_第20张图片

可以在屏障里指定如果所有线程都到达屏障了,后面应该执行哪个线程。

3.3 Semophore

信号量,控制并发量,分布式系统也有相似概念,称为“限流”。限流是有必要的,比如我们的系统里有一个导出汇总excel的功能,不限制并发量,系统立马就OOM了。

public class ThreadPoolTest {
    private static AtomicInteger ai = new AtomicInteger();
    //我们新建一条三车道的高速公路
    private static Semaphore semaphore = new Semaphore(3);

    public static void main(String[] args) {
        //高速入口堵了20辆车
        ExecutorService executors = Executors.newFixedThreadPool(20);
        for (int i = 0; i < 20; i++) {
            executors.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        //放行一辆
                        semaphore.acquire();
                        Thread.sleep(3000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        //车道空出来了
                        semaphore.release();
                    }
                    System.out.println(ai.getAndIncrement());
                }
            });
        }
    }
}

4 ThreadLocal有时候不安全

问:我们知道ThreadLocal可以用set、get存取线程私有变量,那如果是在线程池里使用ThreadLocal还能线程安全吗?

第一反应就是问题有坑,因为我们知道线程池中的线程是可以复用的,这个私有变量如果是上一个线程set的不就乱了吗,好我们来验证一下。

public class ThreadPoolTest {
    private static AtomicInteger ai = new AtomicInteger();

    private static final ThreadLocal threadLocal = new ThreadLocal(){
        @Override
        protected Integer initialValue() {
            return -1;
        }
    };

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 8,
                30, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
        //空闲线程可以存活30s,我们趁热使用上一个线程
        for (int i = 0; i < 4; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    int tmp = ai.getAndIncrement();
                    threadLocal.set(tmp);
                    System.out.println(Thread.currentThread().getId() + "#" + Thread.currentThread().getName() + "::"
                            + threadLocal.get() + ", set::" + tmp);
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        for (int i = 0; i < 8; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getId() + "#" + Thread.currentThread().getName() + "::"
                            + threadLocal.get());
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

    }
}

 

Java多线程(下)——线程池、并发工具类、ThreadLocal_第21张图片

所以线程池里的线程只要还存活着,那么设置的ThreadLocal变量值就还在。

 

你可能感兴趣的:(多线程)