代码编织梦想

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

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

例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 变量后,要记得及时清除掉。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接: https://blog.csdn.net/weixin_41750142/article/details/110451569

Java并发编程_线程池相关-爱代码爱编程

1.什么是线程池? 通俗理解就是一个容器,里面放了一些线程,需要用时就取出来用,用完了就放回去等待下一次用。 线程池内部维护一个任务队列,从池里取出线程去执行队列里的任务。 2.为什么要使用线程池 在 Java 诞生之初是没有线程池的概念的,而是先有线程,随着线程数的不断增加,人们发现需要一个专门的类来管理它们,于是才诞生了线程池。没有线程池的时候

Java线程池线程异常且异常处理器失效的“诡异”现象-爱代码爱编程

  写此文档目的主要是我在开发程序过程中设置了未捕获异常处理器,用ExecutorService接口的submit方法提交子线程任务,而子线程运行发生异常,但是并未回调异常处理器,就出现了子线程未运行成功也没有任何Log的”诡异“现象。   在Java层,我们创建子线程的常用方式有:   1.继承Thread,重写run();   2.实现Runnabl

Java线程池学习总结-爱代码爱编程

自定义线程池 ThreadPoolExecutor是jdk给我们提供的可以快速开启管理一个线程池的方法。它的构造方法中提供了多个可配置参数,开发者可根据需求自定义参数。 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,

Android之线程池的使用场景理解-爱代码爱编程

线程池概念 源于JDK1.5版本之后的Executor接口,通过ThreadPoolExceutor进行实现,而ThreadPoolExecutor继承于AbstractExecutorService,AbstractExecutorService 是ExecutorService的实现,ExecutorService继承了Executor接口. 线程

OkHttp之线程池的使用-爱代码爱编程

Java从1.5之后提供的线程池,提高线程的复用,因为线程的开启回收很消耗内存 线程池的基本创建 第一个参数,核心线程第二个参数,非核心线程,线程池的最大容量第三个参数/第四个参数,为线程的闲置时间,如果小于当前设置时间,则会复用,否则当前Runnable回被回收第五参数,为任务队列,把超出任务放到缓存队列中 public ThreadPoolExec

juc-12-线程池2-线程池的使用-爱代码爱编程

这是线程池第二篇文章,上一篇文章写了如何创建线程池,这篇文章用代码来演示通过 ThreadPoolExecutor 和 Executors的静态工厂方法创建线程池,以及这些线程池的基本使用,然后重点讲解 ThreadPoolExecutor 的实现接口 ExecutorService 中的常用API,如何通过这些API来管理线程池。 1、ThreadPo

ThreadLocal原理、源码、内存泄露分析-爱代码爱编程

ThreadLocal ThreadLocal类和Thread类都位于java.lang包下面,关系紧密。Thread类里面有一个成员变量 ThreadLocal.ThreadLocalMap threadLocals = null; 也就是说,每个线程都有自己独立的ThreadLocalMap(容器)。ThreadLocal就是通过与线程的这个T

ThreadLocal与InheritableThreadLocal区别-爱代码爱编程

特点 ThreadLocal声明的变量是线程私有的成员变量,每个线程都有该变量的副本,线程对变量的修改对其他线程不可见。 InheritableThreadLocal声明的变量同样是线程私有的,但是子线程可以从父线程继承InheritableThreadLocal声明的变量。 子线程对InheritableThreadLocal变量的修改对

Java线程池基础&ThreadLocal传递线程池&任务监控-爱代码爱编程

目录 1、Jdk 线程池介绍 1.1 固定大小线程池(FixedThreadPool) 1.2 单线程线程池(SingleThreadPool) 1.3 可缓存的线程池(CachedThreadPool) 1.4 可调度线程池(ScheduledThreadPool) 1.5 工作窃取线程池(WorkStealingThreadPool) 1

一文让你彻底明白ThreadLocal-爱代码爱编程

前言: ThreadLocal在JDK中是一个非常重要的工具类,通过阅读源码,可以在各大框架都能发现它的踪影。它最经典的应用就是 事务管理 ,同时它也是面试中的常客。 今天就来聊聊这个ThreadLocal;本文主线: ①、ThreadLocal 介绍 ②、ThreadLocal 实现原理 ③、ThreadLocal 内存泄漏分析

InheritableThreadLocal 的作用?-爱代码爱编程

直接看总结: InheritableThreadLocal主要用于子线程创建时,需要自动继承父线程的ThreadLocal变量,方便必要信息的进一步传递。 线程间传递实现原理: 说到InheritableThreadLocal,还要从Thread类说起: public class Thread implements Runnable { ..

聊一聊我眼中的ThreadLocal(面试题形式总结)-爱代码爱编程

这篇总结一下 ThreadLocal,主要的议题有: ThreadLocal 介绍ThreadLocal 实现原理ThreadLocal 内存泄漏分析ThreadLocal 应用场景及示例最早听说 ThreadLocal 是18年还在实习的时候,那时候有一个要用到线程池的任务,有人说并发的问题也可以通过 ThreadLocal 来解决。但当时没有用到这玩