并发编程实战(三)线程池、ThreadLocal使用注意事项

一、创建线程和线程池时要指定与业务相关的名称

    在日常开发中,当在一个应用中需要创建多个线程 或者 线程池时,最好给每个线程 或者 线程池 根据业务类型 设置具体名称,以便在出现问题时方便进行定位。

例1 👀 创建线程需要有线程名

public class ThreadTest1 {
    public static void main(String[] args) {
        // 订单模块
        Thread threadOne = new Thread(new Runnable() {
            public void run() {
                System.out.println("保存订单的线程");
                try{
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }throw new NullPointerException();
            }
        });

        // 发货模块
        Thread threadTwo = new Thread(new Runnable() {
            public void run() {
                System.out.println("保存收获地址的模块");
            }
        });

        threadOne.start();
        threadTwo.start();
    }
}

运行结果:
在这里插入图片描述
    从代码可以知道,是 threadOne 抛出了 NPE 异常,但是只看运行结果的日志,无法判断是订单模块的线程抛出的。运行结果显示的是 “Thread-0” ,创建线程的源码:

 public Thread(Runnable target) {
 		// (一)                      (二)
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

(一):

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

(二):

/* For autonumbering anonymous threads. */
    private static int threadInitNumber;
    private static synchronized int nextThreadNum() {
        return threadInitNumber++;
    }

    可以看到,如果调用没有指定线程名称的方法创建的线程,其内部会使用 “Thread-” + nextThreadNum() 作为线程的默认名称。 threadInitNumber 是 static 变量,nextThreadNum() 是 static 方法,所以线程的编号是唯一的并且是递增的,使用 synchronized 关键字进行同步保证线程安全。
    当一个系统中有多个业务模块 而 每个模块又都使用自己的线程时,除非抛出与业务相关的异常,否则,根本无法判断是哪一个模块出现了问题。修改代码:

public class ThreadTest1 {
    static final String THREAD_SAVE_ORDER ="THREAD_SAVE_ORDER";
    static final String THREAD_SAVE_ADDR ="THREAD_SAVE_ORDER";
    public static void main(String[] args) {
        // 订单模块
        Thread threadOne = new Thread(new Runnable() {
            public void run() {
                System.out.println("保存订单的线程");
                try{
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }throw new NullPointerException();
            }
        },THREAD_SAVE_ORDER);

        // 发货模块
        Thread threadTwo = new Thread(new Runnable() {
            public void run() {
                System.out.println("保存收获地址的模块");
            }
        },THREAD_SAVE_ADDR);

        threadOne.start();
        threadTwo.start();
    }
}

运行结果:
在这里插入图片描述
    从运行结果就可以定位到 是 保存订单 模块抛出了 NPE 异常,这样就可以找到问题所在。
    
例2 👀 创建线程池时也需要指定线程池的名称 :

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolTest {
    static ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5,5,1,TimeUnit.MINUTES,
            new LinkedBlockingQueue<Runnable>());
    static ThreadPoolExecutor executorTwo = new ThreadPoolExecutor(5,5,1,TimeUnit.MINUTES,
            new LinkedBlockingQueue<Runnable>());

    public static void main(String[] args) {
        // 接受用户链接模块
        executorOne.execute(new Runnable() {
            public void run() {
                System.out.println("接受用户链接线程");
                throw new NullPointerException();
            }
        });

        // 具体处理用户请求模块
        executorTwo.execute(new Runnable() {
            public void run() {
                System.out.println("具体处理业务线程");
            }
        });

        executorOne.shutdown();
        executorTwo.shutdown();
    }
}

运行结果:
在这里插入图片描述
    和上面线程的创建同理,这样的运行结果 不知道是哪个模块的线程池抛出了异常,创建线程池的源码:

  public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
  public static ThreadFactory defaultThreadFactory() {
        return new DefaultThreadFactory();
    }
  static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

    可以看到,poolNumber 是 static 的 原子变量,用来记录当前线程池的编号。“pool-1-thread-1” 中 “pool-1” 的 ”1“ 就是线程池编号;而 threadNumber 是线程池级别的,“thread-1” 的 ”1“ 就是线程池中线程的编号;namePrefix 是 线程池中线程名称的前缀。在执行线程池内任务时,调用 execute 方法 ,其内部会调用 addWorker 方法,addWorker 方法内部会 new 一个 Worker,Worker 的无参构造方法中会调用 newThread 方法,源码:

   public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }

    可以看到,线程的名称是使用namePrefix+threadNumber.getAndIncrement(), 拼接的。
    所以,只需要对 DefaultThreadFactory 的代码中的 初始化进行改动,即 当需要创建线程池时 传入与业务相关的 namePrefix 名称就可以了。

import java.util.Random;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

// 命名线程工厂
public class NamedThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;
    
    NamedThreadFactory(String name){
        SecurityManager s = System.getSecurityManager();
        group = (s != null)? s.getThreadGroup():Thread.currentThread().getThreadGroup();
        if(null==name||name.isEmpty()){
            name = "pool";
        }
        namePrefix = name +"-" +poolNumber.getAndIncrement()+"-thread-";
    }
    public Thread newThread(Runnable r) {
      Thread t = new Thread(group,r,namePrefix + threadNumber.getAndIncrement(),0);
      if(t.isDaemon()) {
          t.setDaemon(false);
      }
      if(t.getPriority()!= Thread.NORM_PRIORITY){
          t.setPriority(Thread.NORM_PRIORITY);
      }
        return t;
    }
}

修改创建线程池的代码为:

static ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5,5,1,TimeUnit.MINUTES,
            new LinkedBlockingQueue<Runnable>(),new NamedThreadFactory("ASY-ACCEPT-POOL"));
    static ThreadPoolExecutor executorTwo = new ThreadPoolExecutor(5,5,1,TimeUnit.MINUTES,
            new LinkedBlockingQueue<Runnable>(),new NamedThreadFactory("ASYN-PROCESS-POOL"));

运行代码:
在这里插入图片描述
    综上,如果不为线程 或者 线程池 起名字会给问题排查带来麻烦,然后通过源码分析了线程 和 线程池 名称 以及 默认名称是如何来的,以及 如何定义线程池的名字,另外,在 run 方法里 使用 try-catch 块,避免将异常抛到 run 方法以外,同时打印日志也是一个最佳实践。
    

二、使用线程池的情况下当程序结束时记得调用 shutdown 关闭线程池

    在日常开发中为了便于线程的有效复用,经常会用到线程池,然而 使用完线程池后 如果不调用 shutdown 关闭线程池,则会导致线程池资源一直不被释放。

  • 问题复现
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ShutDownTest {
    static void asynExecuteOne(){
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(new Runnable() {
            public void run() {
                System.out.println("--async execute one---");
            }
        });
    }

    static void asynExecuteTwo(){
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(new Runnable() {
            public void run() {
                System.out.println("--async execute two---");
            }
        });
    }
    public static void main(String[] args) {
    // 同步执行
    System.out.println("---sync execute");

       // 异步执行操作 one
        asynExecuteOne();

        // 异步执行操作 two
        asynExecuteTwo();

        // 执行完毕
        System.out.println("---execute over---");
    }
}

    可以看到,代码先是同步执行,然后使用线程池的线程进行异步操作。我们期望当主线程与两个异步任务执行完,整个 JVM 就会退出。
运行结果:
在这里插入图片描述
    可以看到,进程还存在的。在代码中添加线程池的 shutdown 方法:

 executor.shutdown();

运行结果:
在这里插入图片描述
    可以看到,JVM 已经退出了,说明 只有调用了线程池的 shutdown 方法后,线程池任务执行完毕,线程池资源才会被释放。

  • 问题分析
        JVM 退出的条件是 当前不存在用户线程,而 线程池默认的 ThreadFactory 创建的线程是用户线程,源码:
/**
 * The default thread factory
 */
static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

    可以看到,线程池默认的 ThreadFactory 创建的都是用户线程,而 线程池里的核心线程是一直存在的,如果没有任务 则会被阻塞,所以 线程池里的用户线程 一直存在。而 shutdown 的作用 就是 让这些核心线程终止,shutdown 源码:

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
		
		// 设置线程池状态为 SHUTDOWN
        advanceRunState(SHUTDOWN);

		// 中断所有的空闲工作线程 Worker(阻塞到队列的 take() 方法的线程)
        interruptIdleWorkers();
        
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

    可以看到,shutdown 方法中设置了线程池的状态为 SHUTDOWN,并且设置了所有 Worker 空闲线程的中断标志。

Worker 类的 run 方法源码:

  public void run() {
  			// (一)
            runWorker(this);
        }

(一)runWorker:

 final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {

										 // (二)
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

(二)getTask():

    private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
	
			// (四)
            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {

				// (三)
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

    可以看到,如果队列中没有任务,则 工作队列被阻塞到 (三)处,等待从工作队列里获取一个任务。这时 如果调用线程池的 shutdown 命令,shutdown 会中断所有工作线程,则 (三)之后的 catch 就会捕获到 InterruptedException 异常而返回。然后继续 for 循环,因为 shutdown 方法会把线程池状态变为 SHUTDOWN,所以 getTask 方法 返回了 null,那么 runWorker 方法退出循环,该工作线程就退出了
    

三、线程池使用 FutureTask 时需要注意的事情

    线程池使用 FutureTask 时 如果把拒绝策略设置为 DiscardPolicy 和 DiscardOldestPolicy,并且在被拒绝的任务的 Future 对象上调用了 无参 get 方法,那么调用的线程会一直被阻塞。

  • 问题复现
import java.util.concurrent.*;

public class FutureTest {

    // 线程池单个线程,线程池队列元素个数为 1
    private final static ThreadPoolExecutor executorService = new ThreadPoolExecutor(1,1,
            1L,TimeUnit.MINUTES,new ArrayBlockingQueue<Runnable>(1),
            new ThreadPoolExecutor.DiscardPolicy());

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 添加任务 one
        Future futureOne = executorService.submit(new Runnable() {
            public void run() {
              System.out.println("start runnable one");
            try{
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } }
        });

        // 添加任务 two
        Future futureTwo = executorService.submit(new Runnable() {
            public void run() {
                System.out.println("start runable two");
            }
        });

        // 添加任务 three
        Future futureThree = null;
        try{
            futureThree = executorService.submit(new Runnable() {
                public void run() {
                    System.out.println("start runnable three");
                }
            });
        }catch (Exception e){
            System.out.println(e.getLocalizedMessage());
        }

		// (一)
        System.out.println("task one " + futureOne.get());
		// (二)
        System.out.println("task two " + futureTwo.get());
        // (三)
        System.out.println("task three" + futureThree==null ? null : futureThree.get());

        executorService.shutdown();
    }
}

    可以看到,以上代码创建了一个单线程 和 一个队列元素个数为 1 的线程池,并且把拒绝策略设置为 DiscardPolicy 。然后向线程池提交了一个任务 one,并且这个任务会由唯一的线程来执行,任务在打印 start runnable one 后阻塞该线程 5 s;再线程池提交了一个任务 two,这时会把任务 two 放入阻塞队列;最后向线程池提交任务 three,由于队列已满,所以 触发拒绝策略 丢弃任务 three。

运行结果:
在这里插入图片描述
    可以看到,在任务 one 阻塞的 5s 内,主线程执行到 (一)处,并等待任务 one 执行完毕,任务 one 执行完毕后 (一) 返回,主线程打印 task one null。任务 one 执行完毕后 线程池里 唯一的线程 会去队列里取出任务 two 并执行,所以输出 start runable two,然后 (二)处返回,这时 主线程输出 task two null。然后执行到(三)处,等待任务 three 执行完毕,从运行结果看,(三)处会一直阻塞而不会返回。
    如果把拒绝策略改为 DiscardOldestPolicy,也会存在 有一个任务的 get 方法一直阻塞。但是如果把拒绝策略改为 AbortPolicy 则会正常返回,并且会输出以下结果:
在这里插入图片描述
    

  • 问题分析

线程池的 submit 方法源码:

public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();

	// 装饰 Runnable 为 Future 对象(一)
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    
    // (二)
    execute(ftask);

	// 返回 Future 对象
    return ftask;
}

(一)newTaskFor :

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
	// (三)
    return new FutureTask<T>(runnable, value);
}

(三)FutureTask 的构造方法:

 public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

    可以看到,使用 newTaskFor 方法 将 Runnable 任务转换为 FutureTask ,而在 FutureTask 的构造方法中,将状态值设置为 NEW

    而拒绝策略 rejectedException 并没有对状态值进行修改:

 public DiscardPolicy() { }

(二)execute:

 public void execute(Runnable command) {
        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);
    }

接下来看看 FutureTask 的 get() 方法源码:

public V get() throws InterruptedException, ExecutionException {
        int s = state;

		// 当状态值 <= COMPLETING 时需要等待,否则 调用 report 方法
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
	
			   // (四)
        return report(s);
    }

(四)report:

private V report(int s) throws ExecutionException {
        Object x = outcome;

		// 状态值为 NORMAL 正常返回
        if (s == NORMAL)
            return (V)x;

		// 状态值大于等于 CANCELLED 则抛出异常
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }

    可以看到,当 Future 状态 > COMPLETING 时 调用 get 方法才会返回,而 DiscardPolicy 策略在拒绝元素时 并没有设置该 Future 的状态,所以 Future 状态一直是 NEW,也就一直不会返回。同理 ,DiscardOldestPolicy 策略也存在这样的问题。
    那么 默认的 AbortPolicy 策略 为什么没问题呢? 其实在执行 AbortPolicy 策略 时,代码会抛出 RejectExeception 异常,也就是 submit 方法 并没有返回 Future 对象,这时候 futureThree 是 null。
    所以 当使用 Future 时,尽量使用带超时时间的 get 方法,这样即使使用了 DiscardPolicy 策略,也不至于一直等待,超时时间到了就会自动返回 。 如果非要使用不带参数的 get 方法则可以重写 DiscardPolicy 的拒绝策略,在执行策略时 设置该 FutureTask 提供的方法,会发现只有 cancel 方法是 public 的,并且可以设置 FutureTask 的状态大于 COMPLETING,则 重写拒绝策略:(只能这样啦,其实如果能把 FutureTask 的状态设置成 NORMAL 最好了,但是 FutureTask 并没有提供接口)

import java.util.concurrent.FutureTask;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

public class MyRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable runnable, ThreadPoolExecutor e){
    if(!e.isShutdown()){
        if(null!=runnable && runnable instanceof FutureTask)
        {
            ((FutureTask) runnable).cancel(true);
        }
    }
    }
}

    使用这样的策略, cancel 方法的源码:

  public boolean cancel(boolean mayInterruptIfRunning) {
        if (!(state == NEW &&
              UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                  mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
            return false;
      ... ...
    }
 

    可以看到,它是把状态值通过 CAS 设置成 INTERRUPTING,那么 调用 get 方法,就会去调用 report 方法,抛出 CancellationException 异常,所以,我们的代码也需要使用 try-catch 捕获异常,将代码修改为:

 try{
        System.out.println("task three " + (futureThree == null ? null : futureThree.get()));}
        catch (Exception e){
            System.out.println(e.getLocalizedMessage());
        }

运行结果:
在这里插入图片描述
    

四、使用 ThreadLocal 不当可能会导致内存泄漏

1、为何会出现内存泄漏

    ThreadLocal 只是一个工具类,具体存放变量的是线程的 threadLocals 变量。threadLocals 是一个 ThreadLocalMap 类型的变量。
在这里插入图片描述
    可以看到,ThreadLocalMap 内部是一个 Entry 数组,Entry 继承自 WeakReference ,Entry 内部的 value 用来存放 通过 ThreadLocal 的 set 方法传递的值。
    
Entry 的构造方法:

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

    Entry(ThreadLocal<?> k, Object v) {
    
    	// (一)
        super(k);
        value = v;
    }
}

(一)父类是 WeakReference:

   public WeakReference(T referent) {
   
   		// (二)
        super(referent);
    }

(二)父类是 Reference:

Reference(T referent) {

		// (三)
        this(referent, null);
    }

(三):

 Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

    可以看到,在 Entry 的构造方法中,k 作为 WeakReference 的 构造方法的参数传入,也就是说,ThreadLocalMap 里的 key 是 ThreadLocal 对象的弱引用,referent 变量引用了 ThreadLocal 对象,value 是具体调用 ThreadLocal 的 set 方法时传递的值。
    当一个线程调用 ThreadLocal 的 set 方法设置变量时,当前线程的 ThreadLocalMap 里会存放一个记录,这个记录的 key 是 ThreadLocal 的弱引用,value 则为设置的值。如果当前线程一直存在 且 没有调用 ThreadLocal 的 remove 方法,并且 这时候 在其他地方还有对 ThreadLocal 的引用,则 当前线程的 ThreadLocalMap 变量里 会存在对 ThreadLocal 变量的引用 和 对 value 对象的引用,它们是不会被释放的,这就会造成 内存泄漏。
    其实 在 ThreadLocal 的 set、get 和 remove 方法中可以找到一些时机 对这些 key 为 null 的 entry 进行清理,但是这些清理不是必须发生的。

比如 remove 方法中的清理,源码:

private void remove(ThreadLocal<?> key) {

	// 计算当前 ThreadLocal 变量所在的 table 数组的位置,尝试使用快速定位方法
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

	// 这里使用循环是防止快速定位失效后,遍历 table 数组
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {

		// 找到
        if (e.get() == key) {
        	// 调用 WeakReference 的 clear 方法清除对 ThreadLocal 的弱引用
            e.clear();

			// (四)清除 key 为 null 的元素
            expungeStaleEntry(i);
            
            return;
        }
    }
}

(四) expungeStaleEntry:

 private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;

        // 去掉对 value 的引用
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;

        // Rehash until we encounter null
        Entry e;
        int i;
        for (i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();

			// (五)从当前元素的下标开始查看 table 数组里是否有 key 为 null 的其他元素,有 则 清理
            if (k == null) {
                e.value = null;
                tab[i] = null;
                size--;
            } 

				else {
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    tab[i] = null;

                    // Unlike Knuth 6.4 Algorithm R, we must scan until
                    // null because multiple entries could have been stale.
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }

    可以看到,(五)处,从当前元素的下标开始查看 table 数组中 是否有 key 为 null 的其他元素,有则清理。循环退出的条件是遇到 table 里 有 null 元素,而 null 元素 后面 的 Entry 里面 key 为 null 的元素 是不会被清理的。
    

其实 remove() 方法的内部就是调用了 remove(ThreadLocal<?> key) 方法,源码:

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }


    
🎭 总结:
    ThreadLocalMap 的 Entry 中的 key 使用的是对 ThreadLocal 对象的弱引用,这在避免内存泄漏方面是一个进步,因为 如果是强引用,ThreadLocalMap 中的 ThreadLocal 对象还是不会被回收的,而 如果是弱引用 则 ThreadLocal 是会被回收掉的,但是对应的 value 还是不能被回收,这样 ThreadLocalMap 里就会存在 key 为 null 但是 value 不为 null 的 entry 项,虽然 ThreadLocalMap 提供了 set、get 和 remove 方法,可以在一些时机下 对这些 Entry 项进行清理,但是这时不及时的,也不是每次都会执行,所以 在一些情况下还是会发生内存泄漏,因此 在使用完毕后 及时调用 remove() 方法,才是解决内存泄漏问题的王道。
    

2、在线程池中使用 ThreadLocal 导致的内存泄漏

例 👀

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolTest {
    
    static class LocalVariable{
        private Long[] a = new Long[1024*1024];
    }
    
    // 核心线程数 和 最大线程数 都为 5
    final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,5,1,
            TimeUnit.MINUTES,new LinkedBlockingQueue<Runnable>());
    
    final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();
    
    public static void main(String[] args) throws InterruptedException {
        
        // 向线程池中放入 50 个任务
        for(int i=0;i<50;++i){
            poolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    localVariable.set(new LocalVariable());
                    
                    System.out.println("use local varaible");
                    localVariable.remove();
                }
            });
            
            Thread.sleep(1000);
        }
        
        System.out.println("pool execute over");
    }
}

    由于没有调用线程池的 shutdown 或者 shutdownNow 方法,所以 线程池里的用户线程不会退出,进而 JVM 进程也不会退出。
    运行代码,使用 jconsole 监控堆内存变化。(在 jdk 目录的 bin 下可以找到 jconsole.exe 运行并选择以上类相应进程即可)
在这里插入图片描述

然后将 localVariable.remove(); 注释放开,再运行,观察堆内变化:
在这里插入图片描述

     可以看到 ,当主线程处于休眠时,进程占用了大概 53MB内存(书上的例子大概是 77 MB 的样子),运行 有 localVariable.remove(); 的代码,显示占用了大概 18MB 内存(书上的例子大概是 25 MB 的样子)。由此可知 运行第一种 发生了内存泄漏,下面分析内存泄漏的原因。
     第一次运行代码时,在设置线程的 localVariable 变量后 没有调用 localVariable 的 remove 方法,这导致 线程池里 5 个核心线程的 threadLocals 变量里的 new LocalVariable() 实例没有被释放。虽然线程池里的任务执行完了,但是 线程池里的 5 个线程会一直存在,直到 JVM 进程被杀死。这里要注意的是,由于 localVariable 被声明成了 static 变量,虽然在 线程的 ThreadLocalMap 里 对 localVariable 进行了弱引用,但是 localVariable 不会被回收。第二次运行代码时,由于线程在设置 localVariable 变量后 及时调用了 remove 方法进行了清理,所以不会存在内存泄漏问题。
    
🎭 总结:
    如果在线程池里设置了 ThreadLocal 变量,则一定要记得及时 清理,因为线程池 里的核心线程是一直存在的,如果不清理,线程池的核心线程的 threadLocals 变量会一直持有 ThreadLocal 变量。
    

3、在 Tomcat 的 Servlet 中使用 ThreadLocal 导致内存泄漏

例 👀 :

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class HelloWorldExample extends HttpServlet {
    private static final long serialVersionUID =1L;
    static class LocalVariable {
        private Long[] a = new Long[1024 * 1024 * 100];
    }

        final static ThreadLocal<LocalVariable> localVariable = new
                ThreadLocal<LocalVariable>();

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        localVariable.set(new LocalVariable());

        response.setContentType("text/html");
        PrintWriter out = response.getWriter();

        out.println("<html>");
        out.println("<head>");

        out.println("title" +"title"+ "</title>");
        out.println("</head>");

        out.println("<body bgcolor=\"white\">");

        out.println(this.toString());

        out.println(Thread.currentThread().toString());

        out.println("</body>");
        out.println("</html>");
    }
}

修改 Tomcat 的 conf 下的 server.xml 配置为:

<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
        maxThreads="10" minSpareThreads="5"/>
<Connector executor="tomcatThreadPool"
               port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

    设置了 Tomcat 的处理线程池的最大线程数为 10,最小线程数为 5。回顾一下 Tomcat 的容器结构:
在这里插入图片描述
     Tomcat 中的 Connector 组件负责接受 并 处理请求,其中 Connector 中有 Socket acceptor thread ,负责接受用户的访问请求 ,然后把接受到的请求交给 Worker threads pool 线程池进行具体处理,这个线程池就是我们在 server.xml 中配置的线程池。Worker threads pool 里的线程负责把具体请求分发到具体的应用的 Servlet 上进行处理。
在这里插入图片描述
在 WEB-INF 下新增 classes 和 lib 包,classes用来存放编译后输出的classes文件,lib用于存放第三方的jar包。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
配置 Tomcat:
在这里插入图片描述
在 web.xml 中配置 servlet 节点 和 对应的 mapping 节点:

 <servlet>
        <servlet-name>HelloWorldExam</servlet-name>
        <servlet-class>HelloWorldExample</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>HelloWorldExam</servlet-name>
        <url-pattern>/servlet/hello</url-pattern>
    </servlet-mapping>

    接下来,启动 Tomcat 访问该 Servlet 多次,会发现输出结果:

HelloWorldExample@3da2b55e Thread[http-nio-8080-exec-5,5,main]
HelloWorldExample@3da2b55e Thread[http-nio-8080-exec-4,5,main]

    前半部分是 Servlet 实例,是一样的,说明 多次访问的是同一个 Servlet 实例,后半部分不同,则说明 Connector 中的线程池中的不同线程来执行 Servlet。
    在访问该 Servlet 的同时打开 jconsole 观察堆内存,会发现内存飙升,因为 工作线程在调用 Servlet 的 doGet 方法时,工作线程的 threadLocals 变量里添加了 LocalVariable 实例,但是后来没有清除 。另外,多次访问该 Servlet 可能使用的不是工作线程池里的同一个线程,这会导致 工作线程池里 多个线程都会存在内存泄漏问题。
    而且,在 Tomcat 6.0,应用 reload 操作后 会导致 加载该应用的 webappClassLoader 释放不了,因为 在 Servlet 的 doGet 方法里创建 LocalVariable 时使用的是 webappClassLoader ,所以 LocalVariable.class 里 持有 webappclass 的引用。由于 LocalVariable 实例没有被释放,所以 LocalVariable.class 对象也没有释放,所以 webappClassLoader 加载的所有类也没有被释放。因为在应用 reload 时,Connector 组件里的工作线程池里的线程还是一直存在的,并且 线程里的 threadLocals 变量并没有被清理。而在 Tomcat 7.0 这个问题被修复了,应用在加载时 ,会清理工作线程池中线程的 threadLocals 变量。

🎭 总结:
    Java 提供的 ThreadLocal 给我们编程提供了方便,到那时如果使用不当,也会带来麻烦,所以要养成良好的编程习惯,在线程中使用完 ThreadLocal 变量后,要记得及时清除掉。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值