带大家学习一下线程池的基本操作及ThreadLocal的使用

一、线程池的概念

​ 首先,为什么要使用线程池呢?之前我们使用线程是怎么去使用创建的,可能对这方面有所了解的人都会想到,创建线程的过程中是很损性能这回事。为什么会导致这样,主要是线程的频繁的创建、销毁等引起的。后面在JDK1.5中加入了线程池这种观念,线程池会帮我们提前创建好你所需要的线程数量或者能够极大的提高线程之间的复用性,进而提升本身程序的性能。

二、讲一下线程池的几种创建方式
1、new ThreadPoolExecutor(…)
2、new ScheduledThreadPoolExecutor(…)
3、new ForkJoinPool()
 /**
         * 创建线程池的3种方式
         * 1、new ThreadPoolExecutor(...)
         * 2、new ScheduledThreadPoolExecutor(...)
         * 3、new ForkJoinPool()
         */
        // 第一种方式(一般也是最常用的一种方式 ThreadPoolExecutor())
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(5), new ThreadFactoryBuilder().setPriority(8).setDaemon(true).build(), new ThreadPoolExecutor.DiscardOldestPolicy());

        // 第二种方式(默认最大线程池大小Integer.MAX_VALUE,采用延迟队列缓冲,线程池空闲时间线程存活为0)
        ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(5, new ThreadPoolExecutor.DiscardOldestPolicy());

        // 第三种方式
        ForkJoinPool forkJoinPool = new ForkJoinPool();
在这3种创建方式中,我主要讲第一种方式原理及使用!!!
三、ThreadPoolExecutor()的原理及使用
1、在使用之前我们看一下Executors的静态方法创建几种线程池的方式
// 创建固定线程池 (默认采用LinkedBlockingQueue队列,有界队列)
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 创建定时执行线程池 (默认采用DelayedWorkQueue()队列,有界队列)
ExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
// 创建动态池 (默认采用SynchronousQueue()同步队列,属于无界队列,不做缓冲层)
ExecutorService executorService = Executors.newCachedThreadPool();
// 单列线程池,也就是只创建一个线程池 (默认采用LinkedBlockingQueue队列,有界队列)
ExecutorService executorService = Executors.newSingleThreadExecutor();

/**
	在VM option中加入-Xms10M -Xmx10M参数,可以发现使用Executors静态方式创建容易
	造成OOM异常
*/
while (true) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    User user = new User();		// 循环创建对象
                    System.out.println("user:" + user);
                }
            });
        }

通过上面测试可以发现,使用静态方式创建会很容易造成OOM异常,所以在使用线程池时,一般不推荐此方式创建线程池。

接下来为了解决上面的问题,我们可以通过自己去创建合适的线程池去跑程序。

ThreadPoolExecutor类中提供了4种构造器让我们去创建线程池,如何创建?源码如下:

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) {
        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);
    }

 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.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
参数说明:

corePoolSize:核心线程池数量

maximumPoolSize:最大线程池数量

keepAliveTime:空闲线程存活时间

unit:时间单位/SECONDS等

workQueue:线程池所使用的缓冲队列 (有好多种队列方式)

threadFactory:线程池创建线程使用的工厂

handler:线程拒绝处理器

  • RejectedExecutionHandler来处理;ThreadPoolExecutor内部有实现4个拒绝策略,默认为AbortPolicy策略:

    • CallerRunsPolicy:由调用execute方法提交任务的线程来执行这个任务
    • AbortPolicy:抛出异常RejectedExecutionException拒绝提交任务
    • DiscardPolicy:直接抛弃任务,不做任何处理
    • DiscardOldestPolicy:去除任务队列中的第一个任务,重新提交
2、在上面基本了解了4种创建方式后,下面就开始基本使用:
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(1),
                new ThreadFactoryBuilder().build(),new ThreadPoolExecutor.AbortPolicy());
        try {
            for (int i = 1; i <= 5; i++) {
                executor.execute(new Runnable() {       // 循环执行任务1、2、3、4、5
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(3 * 1000);
                            System.out.println("threadName:" + Thread.currentThread().getName());
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            executor.shutdown();    // 释放资源
        }

当执行任务1、2时:i<=1或i<=2,此时线程会在核心池中执行;

当执行任务3时,i<=3此时当前线程会进入到缓冲队列中,由于当前缓冲队列大小为1;

当执行任务4时,i<=4此时线程池达到最大容量;

当执行线程任务5时,i<=5此时核心池、最大池、缓冲队列都已满,会执行拒绝策略对当前线程是否执行,如上面采用默认策略AbortPolicy()会抛出异常。

3、前面使用的execute()方法执行,我们下面看看,采用submit()方法执行,两者有什么区别?
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(1),
                new ThreadFactoryBuilder().build(),new ThreadPoolExecutor.DiscardOldestPolicy());
        try {
            Future<String> result = executor.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    System.out.println("threadName:" + Thread.currentThread().getName());
                    return "返回执行结果...";
                }
            });
            // 获取返回结果
            System.out.println("result:" + result.get(5L, TimeUnit.SECONDS));   // 设置为5秒
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            executor.shutdown();        // 记得用完释放资源
        }

从上面可以看出,execute()与submit()方法的主要区别在于是否有返回值。

源码如下:

// execute()由void修饰,故无返回值
public void execute(Runnable command) {		// 提供了1种
        if (command == null)
            throw new NullPointerException();
        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);
    }

// submit()返回值是Future<T>接口,故有返回值	
public <T> Future<T> submit(Callable<T> task) {		// 提供了3种
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

 public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }

 public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }
4、shutdown()与shutdownNow()关闭线程区别

shutdown(); //平缓关闭,不允许新的线程加入,正在运行的都跑完即可关闭。
shutdownNow(); //暴力关闭。不允许新的线程加入,且直接停到正在进行的线程。

四、ThreadPoolExecutor结合ThreadLocal的使用
1、ThreadLocal实现

ThreadLocal是本地线程的局部变量,多用于对当前线程与其他线程私有化。能够保证在线程中安全性。

操作不当,但也容易造成内存泄漏。

/**
         * ThreadLocal本地线程,子线程的变量操作,并不影响主线程的值。
         * 通过这一点能够说明,主线程与子线程之间的相互操作,并不影响各自的实现
         */
        final ThreadLocal<Object> threadLocal = new ThreadLocal<>();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(1),
                new ThreadFactoryBuilder().build(),new ThreadPoolExecutor.DiscardOldestPolicy());
        threadLocal.set("currentID");       // 当前设置值
        try {
            Future<String> result = executor.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    threadLocal.set("currentID");       // 先执行set,然后再执行remove,是否还能获取到值?答案:否
                    threadLocal.remove();
                    System.out.println("threadName:" + Thread.currentThread().getName() + "--threadLocal:" + threadLocal.get());
                    return "返回执行结果...";
                }
            });
            // 获取返回结果
            System.out.println("threadName:" + Thread.currentThread().getName() + "--result:" + result.get(5L, TimeUnit.SECONDS) + "threadLocal:" + threadLocal.get());   // 设置为5秒
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            executor.shutdown();        // 记得用完释放资源
        }

ThreadLocal的remove()方法是移除掉当前线程中所修饰的本地线程值,而不是整个应用程序;

set()方法,若是当前线程ThreadLocalMap为空,则会重新创建ThreadLocalMap,存储ThreadLocal变量值,

ThreadLocalMap是entry数组存储,并不是entry对象,由于你在类中可以创建多个ThreadLocal变量,

但始终只有一个ThreadLocalMap在存储,其实底层还是使用的Map集合去存。

2、ThreadLocal造成的内存泄漏

通过ThreadLocal类的set()方法,但是你在操作set之后,没有及时调用remove()方法就容易造成很多key为空的,而值是不为空,相当于当前变量都是无效的,一直累积下去,会造成内存泄漏严重问题。

对于没了解过ThreadLocal源码的可能会不知道有这回事,源码如下:

 static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);	// key使用父类方法存储 由于父类是弱引用,每次垃圾回收都会执行,不会造成内存泄漏
                value = v;	// value属于强引用,并不会被回收,导致内存泄漏问题
            }
        }

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);		// 调用本类的私有方法remove()
     }

private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);	// 当key为空时,会将该key清除,解决内存泄漏
                    return;
                }
            }
        }	

在以上可以看出造成内存泄漏的原因是由于大量的key会自动回收,key都为空,但value并不会被回收,此时相当于该变量是不可用的,最后导致内存泄漏严重。

3、解决ThreadLocal造成的内存泄漏

(1)、每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

(2)、在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

参考资料 《Java高并发程序设计》

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

架构潜行之路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值