开启大量线程会有什么问题,如何优化?

这道题想考察什么?

  • 是否了解线程开启的方式?
  • 开启大量线程会引起什么问题?为什么?怎么优化?

考察的知识点

  • 线程的开启方式
  • 开启大量线程的问题
  • 线程池

考生应该如何回答

1、首先,关于如何开启一个线程,大多数人可能都会说3种,Thread、Runnable、Callable嘛!但事实却不是这样的。看JDK里怎么说的。

/**
 * ...
 * There are two ways to create a new thread of execution. One is to
 * declare a class to be a subclass of <code>Thread</code>. 
 * The other way to create a thread is to declare a class that
 * implements the <code>Runnable</code> interface.
 * ....
 */
public class Thread implements Runnable{
      
}

Thread源码的类描述中有这样一段,翻译一下,只有两种方法去创建一个执行线程,一种是声明一个Thread的子类,另一种是创建一个类去实现Runnable接口。惊不惊讶,并没有提到Callable。

继承Thread类

public class ThreadUnitTest {

    @Test
    public void testThread() {
        //创建MyThread实例
        MyThread myThread = new MyThread();
        //调用线程start的方法,进入可执行状态
        myThread.start();
    }

    //继承Thread类,重写内部run方法
    static class MyThread extends Thread {

        @Override
        public void run() {
            System.out.println("test MyThread run");
        }
    }
}

实现Runnable接口

public class ThreadUnitTest {

    @Test
    public void testRunnable() {
        //创建MyRunnable实例,这其实只是一个任务,并不是线程
        MyRunnable myRunnable = new MyRunnable();
        //交给线程去执行
        new Thread(myRunnable).start();
    }

    //实现Runnable接口,并实现内部run方法
    static class MyRunnable implements Runnable {

        @Override
        public void run() {
            System.out.println("test MyRunnable run");
        }
    }
}

Callable

public class ThreadUnitTest {

    @Test
    public void testCallable() {
        //创建MyCallable实例,需要与FutureTask结合使用
        MyCallable myCallable = new MyCallable();
        //创建FutureTask,与Runnable一样,也只能算是个任务
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        //交给线程去执行
        new Thread(futureTask).start();

        try {
            //get方法获取任务返回值,该方法是阻塞的
            String result = futureTask.get();
            System.out.println(result);
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //实现Callable接口,并实现call方法,不同之处是该方法有返回值
    static class MyCallable implements Callable<String> {

        @Override
        public String call() throws Exception {
            Thread.sleep(10000);
            return "test MyCallable run";
        }
    }
}

Callable的方式必须与FutureTask结合使用,我们看看FutureTask的继承关系

//FutureTask实现了RunnableFuture接口
public class FutureTask<V> implements RunnableFuture<V> {

}

//RunnableFuture接口继承Runnable和Future接口
public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

真相大白了,其实实现Callback接口创建线程的方式,归根到底就是Runnable方式,只不过它是在Runnable的基础上又增加了一些能力,例如取消任务执行等。

线程开启后CPU调度会发生什么?

计算机的世界里,CPU会分为若干时间片,通过各种算法分配时间片来执行任务,有耳熟能详时间片轮转调度算法、短进程优先算法、优先级算法等。当一个任务的时间片用完,就会切换到另一个任务。在切换之前会保存上一个任务的状态,当下次再切换到该任务,就会加载这个状态, 这就是所谓的线程的上下文切换。

很明显,上下文的切换是有开销的,包括很多方面,操作系统保存和恢复上下文的开销、线程调度器调度线程的开销和高速缓存重新加载的开销等。
在这里插入图片描述
经过上面两个理论基础的回顾,开启大量线程引起的问题,总结起来,就两个字——开销。

  • 消耗时间 线程的创建和销毁都需要时间,当数量太大的时候,会影响效率。
  • 消耗内存 创建更多的线程会消耗更多的内存,这是毋庸置疑的。线程频繁创建与销毁,还有可能引起内存抖动,频繁触发GC,最直接的表现就是卡顿。长而久之,内存资源占用过多或者内存碎片过多,系统甚至会出现OOM。
  • 消耗CPU 在操作系统中,CPU都是遵循时间片轮转机制进行处理任务,线程数过多,必然会引起CPU频繁的进行线程上下文切换。这个代价是昂贵的,某些场景下甚至超过任务本身的消耗。

针对上面提及到的问题,我们自然需要进行优化。线程的本质是为了执行任务,在计算机的世界里,任务分大致分为两类,CPU密集型任务IO密集型任务

  • CPU密集型任务比如公式计算、资源解码等。这类任务要进行大量的计算,全都依赖CPU的运算能力,持久消耗CPU资源。所以针对这类任务,其实不应该开启大量线程。因为线程越多,花在线程切换的时间就越多,CPU执行效率就越低,一般CPU密集型任务同时进行的数量等于CPU的核心数,最多再加个1。
  • IO密集型任务比如网络读写、文件读写等。这类任务不需要消耗太多的CPU资源,绝大部分时间是在IO操作上。所以针对这类任务,可以开启大量线程去提高CPU的执行效率,一般IO密集型任务同时进行的数量等于CPU的核心数的两倍。

另外,在无法避免,必须要开启大量线程的情况下,我们也可以使用线程池代替直接创建线程的做法进行优化。线程池的基本作用就是复用已有的线程,从而减少线程的创建,降低开销。

在Java中,线程池的使用还是非常方便的,JDK中提供了现成的ThreadPoolExecutor类,我们只需要按照自己的需求进行相应的参数配置即可,这里提供一个示例。

/**
 * 线程池使用
 */
public class ThreadPoolService {

    /**
     * 线程池变量
     */
    private ThreadPoolExecutor mThreadPoolExecutor;

    private static volatile ThreadPoolService sInstance = null;

    /**
     * 线程池中的核心线程数,默认情况下,核心线程一直存活在线程池中,即便他们在线程池中处于闲置状态。
     * 除非我们将ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这时候处于闲置的核心         * 线程在等待新任务到来时会有超时策略,这个超时时间由keepAliveTime来指定。一旦超过所设置的超时时间,闲     * 置的核心线程就会被终止。
     * CPU密集型任务  N+1   IO密集型任务   2*N
     */
    private final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() + 1;
    /**
     * 线程池中所容纳的最大线程数,如果活动的线程达到这个数值以后,后续的新任务将会被阻塞。包含核心线程数+非*      * 核心线程数。
     */
    private final int MAXIMUM_POOL_SIZE = Math.max(CORE_POOL_SIZE, 10);
    /**
     * 非核心线程闲置时的超时时长,对于非核心线程,闲置时间超过这个时间,非核心线程就会被回收。
     * 只有对ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这个超时时间才会对核心线       * 程产生效果。
     */
    private final long KEEP_ALIVE_TIME = 2;
    /**
     * 用于指定keepAliveTime参数的时间单位。
     */
    private final TimeUnit UNIT = TimeUnit.SECONDS;
    /**
     * 线程池中保存等待执行的任务的阻塞队列
     * ArrayBlockingQueue  基于数组实现的有界的阻塞队列
     * LinkedBlockingQueue  基于链表实现的阻塞队列
     * SynchronousQueue   内部没有任何容量的阻塞队列。在它内部没有任何的缓存空间
     * PriorityBlockingQueue   具有优先级的无限阻塞队列。
     */
    private final BlockingQueue<Runnable> WORK_QUEUE = new LinkedBlockingDeque<>();
    /**
     * 线程工厂,为线程池提供新线程的创建。ThreadFactory是一个接口,里面只有一个newThread方法。 默认为DefaultThreadFactory类。
     */
    private final ThreadFactory THREAD_FACTORY = Executors.defaultThreadFactory();
    /**
     * 拒绝策略,当任务队列已满并且线程池中的活动线程已经达到所限定的最大值或者是无法成功执行任务,这时候       * ThreadPoolExecutor会调用RejectedExecutionHandler中的rejectedExecution方法。
     * CallerRunsPolicy  只用调用者所在线程来运行任务。
     * AbortPolicy  直接抛出RejectedExecutionException异常。
     * DiscardPolicy  丢弃掉该任务,不进行处理。
     * DiscardOldestPolicy   丢弃队列里最近的一个任务,并执行当前任务。
     */
    private final RejectedExecutionHandler REJECTED_HANDLER = new ThreadPoolExecutor.AbortPolicy();

    private ThreadPoolService() {
    }

    /**
     * 单例
     * @return
     */
    public static ThreadPoolService getInstance() {
        if (sInstance == null) {
            synchronized (ThreadPoolService.class) {
                if (sInstance == null) {
                    sInstance = new ThreadPoolService();
                    sInstance.initThreadPool();
                }
            }
        }
        return sInstance;
    }

    /**
     * 初始化线程池
     */
    private void initThreadPool() {
        try {
            mThreadPoolExecutor = new ThreadPoolExecutor(
                    CORE_POOL_SIZE,
                    MAXIMUM_POOL_SIZE,
                    KEEP_ALIVE_TIME,
                    UNIT,
                    WORK_QUEUE,
                    THREAD_FACTORY,
                    REJECTED_HANDLER);
        } catch (Exception e) {
            LogUtil.printStackTrace(e);
        }
    }

    /**
     * 向线程池提交任务,无返回值
     *
     * @param runnable
     */
    public void post(Runnable runnable) {
        mThreadPoolExecutor.execute(runnable);
    }

    /**
     * 向线程池提交任务,有返回值
     *
     * @param callable
     */
    public <T> Future<T> post(Callable<T> callable) {
        RunnableFuture<T> task = new FutureTask<T>(callable);
        mThreadPoolExecutor.execute(task);
        return task;
    }
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值