java

JUC并发编程(二)——线程池

Nick · 5月28日 · 2025年 · 本文10792字 · 阅读27分钟104

线程池介绍

java——多线程

线程池添加线程的规则:

JUC并发编程(二)——线程池-Nickの博客

线程池构造器:

JUC并发编程(二)——线程池-Nickの博客

线程池添加:

  • 线程池:核心池大小为5,最大池大小为10,队列为100。
  • 因为线程中的请求最多会创建5个,然后任务将被添加到队列
    中,直到达到100。当队列已满时,将创建最新的线程
    maxPoolSize,,最多到10个线程,如果再来任务,就拒绝。

keepAliveTime:临时线程存活时间

如果线程池当前的线程数多于corePoolSize,那么如果多余的
线程空闲时间超过keepAliveTime,它们就会被终止

线程工厂:ThreadFactory 用来创建线程

  • 新的线程是由ThreadFactory创建的,默认使用
    Executors.defaultThreadFactory(,创建出来的线程都在同
    一个线程组,拥有同样的NORM_PRIORITY优先级并且都不
    是守护线程。如果自己指定ThreadFactory,那么就可以改变
    线程名、线程组、优先级、是否是守护线程等。
  • 通常我们用默认的ThreadFactory就可以了

工作队列:

有3种最常见的队列类型:
1) 直接交接:SynchronousQueue
2) 无界队列: LinkedBlockingQueue

3) 有界的队列:ArrayBlockingQueue

线程池里的线程数量设定为多少比较合适?

  • CPU密集型(加密、计算hash等):最佳线程数为CPU核心
    数的1-2倍左右。
  • 耗时IO型(读写数据库、文件、网络读写等):最佳线程数一
    般会大于cpu核心数很多倍,以JVM线程监控显示繁忙情况为
    依据,保证线程空闲可以衔接上,参考Brain Goetz推荐的计
    算方法:线程数=CPU核心数 x(1+平均等待时间/平均工作时间)

常用线程池:

JUC并发编程(二)——线程池-Nickの博客

以上4种线程池的构造函数的参数:
JUC并发编程(二)——线程池-Nickの博客

停止线程池的正确方法:

1. shutdown
线程池开始停止,并不会直接停止,而是发出停止的信号,当前正在执行和队列里面的线程执行完后线程池停止;当新线程进来的时候会抛线程池已经停止的异常。

2. isShutdown
判断线程池是否已经处于停止或者正在停止的阶段,返回布尔值

3. isTerminated
判断线程池任务完全执行完,线程池处于停止状态,返回布尔值

4. awaitTermination
判断在范围时间内,线程池有没有结束(所有任务执行完并且shutdown),返回布尔值
awaitTermination(7L, TimeUnit.SECONDS);
需要传两个参数表示范围时间

5. shutdownNow
比较暴力,立刻把线程池关闭掉,并中断未执行的任务,会返回被中断任务的任务列表,`List<Runnable> runnableList = executorService.shutdownNow();`
可以后续把未执行完的任务放新的线程池执行。

关闭线程池的五种方法案例:

package threadpool;

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 描述:     演示关闭线程池
 */
public class ShutDown {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 100; i++) {
            executorService.execute(new ShutDownTask());
        }
        Thread.sleep(1500);
        List<Runnable> runnableList = executorService.shutdownNow();

        executorService.shutdown();
//        executorService.execute(new ShutDownTask());
        boolean b = executorService.awaitTermination(7L, TimeUnit.SECONDS);
        System.out.println(b);
//        System.out.println(executorService.isShutdown());
//        executorService.shutdown();
//        System.out.println(executorService.isShutdown());
//        System.out.println(executorService.isTerminated());
//        Thread.sleep(10000);
//        System.out.println(executorService.isTerminated());

//        executorService.execute(new ShutDownTask());
    }
}

class ShutDownTask implements Runnable {

    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "被中断了");
        }
    }
}

四种拒绝策略&可暂停的线程池

拒绝时机:任务太多,怎么拒绝
1.当Executor关闭时,提交新任务会被拒绝。
2.以及当Executor对最大线程和工作队列容量使用有限边界并
且已经饱和时

四种拒绝策略
AbortPolicy:直接抛出异常
DiscardPolicy:拒绝啥也不管,任务丢弃
DiscardOldestPolicy:把等待时间最久的任务丢弃,新任务接受
CallerRunsPolicy:哪个线程提交的任务哪个人物执行

线程池钩子方法,给线程池加点料
场景:

  • 每个任务执行前后
  • 日志、统计
  • 代码演示

实现可暂停的线程池:

package threadpool;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     演示每个任务执行前后放钩子函数
 */
public class PauseableThreadPool extends ThreadPoolExecutor {

    private final ReentrantLock lock = new ReentrantLock();
    private Condition unpaused = lock.newCondition();
    private boolean isPaused;

    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
            TimeUnit unit,
            BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
            TimeUnit unit, BlockingQueue<Runnable> workQueue,
            ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
            TimeUnit unit, BlockingQueue<Runnable> workQueue,
            RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
            TimeUnit unit, BlockingQueue<Runnable> workQueue,
            ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory,
                handler);
    }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        lock.lock();
        try {
            while (isPaused) {
                unpaused.await();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    private void pause() {
        lock.lock();
        try {
            isPaused = true;
        } finally {
            lock.unlock();
        }
    }

    public void resume() {
        lock.lock();
        try {
            isPaused = false;
            unpaused.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        PauseableThreadPool pauseableThreadPool = new PauseableThreadPool(10, 20, 10l,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>());
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("我被执行");
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 10000; i++) {
            pauseableThreadPool.execute(runnable);
        }
        Thread.sleep(1500);
        pauseableThreadPool.pause();
        System.out.println("线程池被暂停了");
        Thread.sleep(1500);
        pauseableThreadPool.resume();
        System.out.println("线程池被恢复了");

    }
}

执行结果:
JUC并发编程(二)——线程池-Nickの博客

实现自定义线程池的钩子方法,可暂停的线程池
主要实现逻辑:在任务执行前,利用暂停字段isPaused来阻塞任务,暂停字段isPaused使用锁锁住保证线程安全。

线程的生命周期
线程的生命周期

线程池关闭状态:

RUNNING:接受新任务并处理排队任务
SHUTDOWN:不接受新任务,但处理排队任务
STOP:不接受新任务,也不处理排队任务,并中断正在进行的任务
TDYING,中文是整洁,理解了中文就容易理解这个状态了:所有任务都已终止,workerCount为零时,线程会转换到
TIDYING状态,并将运行terminate()钩子方法。
TERMINATED: terminate()运行完成

使用线程池的注意点:

  • 避免任务堆积
  • 避免线程数过度增加
  • 排查线程泄漏

ThreadLocal

两大使用场景:
典型场景1:每个线程需要一个独享的对象(通常是工具类,典
型需要使用的类有SimpleDateFormat和Random)

典型场景2:每个线程内需要保存全局变量(例如在拦截器中获
取用户信息),可以让不同方法直接使用 避免参数传递的麻烦

案例:场景一:SimpleDateFormat的进化之路

  1. 2个线程分别用自己的SimpleDateFormat,这没问题
  2. 后来延伸出10个,那就有10个线程和10个SimpleDateFormat, 这虽然写法不优雅(应该复用对象),但勉强可以接受
  3. 但是当需求变成了1000个,那么必然要用线程池(否则消耗内存太多)
  4. 使用synchronized锁住当前类或者simpleDateFormat对象,所有的线程都共用同一个simpleDateFormat对象(解决了线程安全的问题,但是1000个任务需要一个个排队,simpleDateFormat对象同时只能一个线程使用)
  5. 更好的解决方案是使用ThreadLocal(不会发生线程安全问题,并且县城并行)
  6. lambda表达式写法:
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

/**
    Java 8之后的lambda表达式,和上面的等效
*/
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

代码案例:

package threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 描述:     利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
 */
public class ThreadLocalNormalUsage05 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage05().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
//        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
        return dateFormat.format(date);
    }
}

class ThreadSafeFormatter {

    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

案例:场景二:线程内需要保存全局变量

  1. 用ThreadLocal保存一些业务内容(用户权限信息、从用户系统
    获取到的用户名、user ID等)
  2. 这些信息在同一个线程内相同,但是不同的线程使用的业务内
    容是不相同的
  3. 在线程生命周期內,都通过这个静态ThreadLocal实例的get0
    方法取得自己set过的那个对象,避免了将这个对象(例如user
    对象)作为参数传递的麻烦

代码案例:

package threadlocal;

/**
 * 描述:     演示ThreadLocal用法2:避免传递参数的麻烦
 */
public class ThreadLocalNormalUsage06 {

    public static void main(String[] args) {
        new Service1().process("");

    }
}

class Service1 {

    public void process(String name) {
        User user = new User("超哥");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {

    public void process() {
        User user = UserContextHolder.holder.get();
        ThreadSafeFormatter.dateFormatThreadLocal.get();
        System.out.println("Service2拿到用户名:" + user.name);
        new Service3().process();
    }
}

class Service3 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用户名:" + user.name);
        UserContextHolder.holder.remove();
    }
}

class UserContextHolder {

    public static ThreadLocal<User> holder = new ThreadLocal<>();

}

class User {

    String name;

    public User(String name) {
        this.name = name;
    }
}

使用ThreadLocal带来的好处

  1. 达到线程安全
  2. 不需要加锁,提高执行效率
  3. 更高效地利用内存、节省开销:相比于每个任务都新建一个
    SimpleDateFormat,显然用ThreadLocal可以节省内存和开
  4. 免去传参的繁琐:无论是场景一的工具类,还是场景二的用户
    名,都可以在任何地方直接通过ThreadLocal拿到,再也不需
    要每次都传同样的参数。ThreadLocal使得代码耦合度更低,
    更优雅

ThreadLocal原理

  1. 先搞清楚ThreadThreadLocal以及ThreadLocalMap三者
    之间的关系
  2. 每个Thread对象中都持有一个ThreadLocalMap成员变量
  3. ThreadLocalMap里面可以包含很多个ThreadLocal

JUC并发编程(二)——线程池-Nickの博客

ThreadLocalMap 类:

ThreadLocalMap 类,也就是Thread.threadLocals

ThreadLocalMap 类是每个线程Thread类里面的变量,里面
最重要的是一个键值对数组Entryll table,可以认为是一个
map,键值对:
键:这个ThreadLocal
值:实际需要的成员变量,比如user或者
simpleDateFormat对象

主要方法介绍

1. T initialValue()初始化(new的时候需要重写该方法)
2. void set(T t):为这个线程设置一个新值
3. T get():得到这个线程对应的value。如果是首次调用
3. T get(),则会调用initialize来得到这个值
4. void remove():删除对应这个线程的值

ThreadLocal注意点

  1. 内存泄漏
    什么是内存泄漏:某个对象不再有用,但是占用的内存却不能被回收
    什么是弱引用:弱引用
    弱引用的特点是,如果这个对象只被弱引用关联(没有任
    何强引用关联),那么这个对象就可以被回收
    所以弱引用不会阻止GC,因此这个弱引用的机制

Value的泄漏:

* ThreadLocalMap 的每个 Entry 都是一个对key的弱引用,同时,每个 Entry 都包含了一个对value的强引用
* 正常情况下,当线程终止,保存在ThreadLocal里的value会被垃
圾回收,因为没有任何强引用了
* 但是,如果线程不终止(比如线程需要保持很久),那么key对应
的value就不能被回收,因为有以下的调用链:
Thread -> ThreadLocalMap —> Entry(key为null)—>Value
* 因为value和Thread之间还存在这个强引用链路,所以导致value
无法回收,就可能会出现OOM
* JDK已经考虑到了这个问题,所以在set, remove, rehash方法中
会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收
* 但是如果一个ThreadLocal不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时线程了不停止,那么调用链就一直存在,那么就导致了value的内

如何避免内存泄露(阿里规约)
调用remove方法,就会删除对应的Entry对象,可以避免内存
泄漏,所以使用完ThreadLocal之后,应该调用remove方法

  1. 空指针异常
    在进行get之前,必须先set,否则可能会报空指针异常?
    原因是返回的是是一个装箱的类型,不能使用基本数据类型,应该使用装箱类型进行接收,否则装箱拆箱过程null会空指针异常。

  2. 共享对象
    如果在每个线程中ThreadLocal.set(进去的东西本来就是
    多线程共享的同一个对象,比如static对象,那么多个线程
    的ThreadLocal.get(取得的还是这个共享对象本身,还是
    有并发访问问题

  3. 如果可以不使用ThreadLocal就解决问题,那么不要强行使用
    例如在任务数很少的时候,在局部变量中可以新建对象就可以
    解决问题,那么就不需要使用到ThreadLocal

  4. 优先使用框架的支持,而不是自己创造
    例如在Spring中,如果可以使用RequestContextHolder,那
    么就不需要自己维护ThreadLocal,因为自己可能会忘记调用
    remove0方法等,造成内存泄漏

RequestContext的生命周期
Context的生命周期与HTTP请求的生命周期绑定,具体如下:
(1)请求开始时
当一个HTTP请求到达服务器时,RequestContextFilter或RequestContextInterceptor会被触发。在preHandle方法(拦截器)或doFilter方法(过滤器)中,会创建一个ServletRequestContext实例,并将其绑定到RequestContextHolder中。
此时,RequestContextHolder.getRequestContext()就可以获取到当前请求的上下文信息。
(2)请求处理过程中
在控制器方法、视图渲染或其他业务逻辑中,可以通过RequestContextHolder.getRequestContext()访问当前请求的上下文信息。
例如,获取请求参数、国际化信息等。
(3)请求结束时
在postHandle方法(拦截器)或doFilter方法的finally块(过滤器)中,会调用RequestContextHolder.removeRequestContext()方法,清理当前线程的RequestContext。这一步非常重要,可以防止ThreadLocal的内存泄漏问题。

注意⚠️:
RequestContext是通过RequestContextHolder中的ThreadLocal实现的,因此它是线程安全的。每个线程都有自己的RequestContext实例,互不干扰。
如果不清理Context,ThreadLocal中的引用可能会导致内存泄漏。因此,在请求处理完成后,必须清理RequestContext。Spring的默认实现(如RequestContextFilter和RequestContextInterceptor)会自动处理这一点。

0 条回应
在线人数:1人 来访统计
新时代
Ado
隐藏