高并发编程线程池篇

使用线程池的目的
    降低资源消耗
        通过重复利用已创建的线程降低线程创建和销毁造成的消耗
    提高响应效率
        当任务到达时,任务可以不需要等到线程创建就能立即执行
    方便管理
        线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用。

提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

通过Executors(JDK1.5并发包)四种创建方式
    newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
    newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
    newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。
        schedule(Runable command, long delay, TimeUnit unit):定时开始执行
        scheduleAtFixedRate(Runable command, long initialDelay, long period, TimeUnit unit): 第一次定时开始执行之后,周期性的继续执行(周期频率为:上一次任务开始执行时间为起点,过了period时间,调度下一次任务,当然如果上一次任务执行还没结束,就算到了period时间也不会执行下一次任务)。
        scheduleWithFixedFixedDelay(Runable command, long initialDelay, long delay, TimeUnit unit): 第一次定时开始执行之后,周期性的继续执行。(周期频率为:上一个任务结束后,经过delay时间再进行下一次任务调度)。
    newSingleThreadExecutor创建一个单线程话的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。
    ThreadPoolExecutor上述4中方法实际上底层实现是ThreadPoolExecutor的4个构造器。
    Executors的submit方法
        以submit(Callable task)方法开起的线程可以获得线程返回值,通过get()获得当前线程的返回值,当获取返回值得时候如果这个线程的写操作还没有完成则会等待(通常建立一个新的子线程来获取,这样不会阻塞主线程)。在获取该线程的返回值之前,是不会影响其他线程的执行的。
            底层实现:future模式(以后说)
    shutdown()
        关闭线程池,不会暴力关闭,而是等待所有任务执行完成后再关闭。只是发送了一个关闭信号,shutdown()方法执行后,这个线程池不会再接收新的任务。

配置线程池
    CPU密集
        该任务执行的时候,不会产生大量IO阻塞,CPU运行速度快
            配置最大线程数=CPU核数
    IO密集
        该任务需要大量I/O操作,产生阻塞,如果是在单线程中执行,可以使用多线程技术,CPU运算能力不会浪费等待资源
            配置最大线程数:2*CPU核数
    CPU的数量*CPU使用率*(1+等待时间/计算时间)
        CPU数量可以通过java中的Runtime.getRuntime().availableProcessors()方法获取。

并发包
    stop(弃用)
        原因:暴力终止线程,并释放所持有的锁,可能会造成保存的数据不一致,而导致数据永久地破坏和丢失
    interrupt()
        中断线程
    isInterrupted()
        判断是否被中断
    interrupted()
        判断是否被中断,并清除当前中断状态
    sleep(xx)
        阻塞(xx毫秒后重新进入就绪状态),会由于中断而抛出异常,此时,它会清除中断标记。所以我们可以在catch块中通过interrupt()再次设置中断标记为,就可以防止下次循环无法捕获这个中断的问题
    锁对象.wait()
        让线程等待,释放锁的资源
    锁对象.notify()
        唤醒当前对象锁池等待的线程
    join(x)
        让目标程序先执行,执行结束之后再执行自己(x可选,填的话就是设置最大等待时长,超过这个时长将会回到自己继续执行)
        本质是让调用线程wait()再当前线程对象实例上
    yield()
        让出CPU让别人先执行,之后再重新争抢CUP。(Thread.yeild())
    CountDownLatch
        倒计时器:new CountDownLatch(x); x表示计数个数。CountDownLatch,await*(方法,说明x个任务全部完成后,才能继续执行
    CyclicBarrier    
        循环栅栏:可反复使用的计数器,当计数任务完成后,进行下一次计数
        特有的异常类:BrokenBarrierException表示CyclicBarrier已经破损,无需继续等待
    信号量
        允许几个线程同时访问
            Semaphore类
                通过new Semaphore(x)创建对象,“x"表示允许几个线程同时访问
                acquire()申请信号量,无法获得会等待,直到有线程释放信号量
                acquireUniterruptibly()和acquire()类似,但是不响应中断
                tryAcquire()尝试获得一个许可,成功返回true失败返回false,不会进行等待
                release()释放信号量
    阻塞工具类:LockSupport
        park():挂起线程(类似suspend())
        unpark():重启线程(类似resume())
    拒绝策略:均实现了RejectedExecutionHandler接口,若这些策略扔无法满足应用,则可以自己扩展RejectedExecutionHandler接口(如重写beforeExecute()(任务开始前),afterExecute()(任务结束后)、terminiated()(整个线程池退出)等)
        AbortPolicy
            直接抛出异常,阻止系统正常工作
        GallerRumsPolicy
            只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降
        DiscardOledestPolicy
            丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务
        DiscardPolicy    
            丢弃无法处理的任务,不予任何处理。(任务丢失)
    一定要在synchronized里执行,持有同一把锁。

线程池的优化
    提交的任务和线程数量并不是一对一的关系。绝大多数情况下,一个物理线程实际上是需要处理多个逻辑任务的。因此每个线程必然需要拥有一个任务队列。在实际执行过程中,可能会出现这么一种情况:线程A已经把自己的任务都执行完成了,而线程B还有一堆任务等着处理,此时线程A就会“帮助”线程B,从线程B的队列中拿一个任务过来处理,尽可能地达到平衡。一个值得注意的地方是,当线程试图帮助别人时,总是从任务队列的底部开始拿数据,而线程试图执行自己的任务时,则是从相反的顶部开始拿。因此这种行为也十分有利于避免数据竞争。

异常处理,帮助我们定位到抛异常的地方:

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

    @Override
    public void execute(Runnable task) {
        super.execute(wrap(task, clientTrace(), Thread.currentThread().getName()));
    }

    @Override
    public Future<?> submit(Runnable task) {
        return super.submit(wrap(task, clientTrace(), Thread.currentThread().getName()));
    }
    
    private Exception clientTrace(){
        return new Exception("Client stack trace");
    }

    private Runnable wrap(final Runnable task, final Exception clientStack, String clientTreadName){
        return new Runnable() {
            @Override
            public void run() {
                try{
                    task.run();
                }catch (Exception e){
                    clientStack.printStackTrace();
                    throw e;
                }
            }
        };
    }
}

扫描二维码关注公众号,回复: 8811549 查看本文章

threadlocal
    给每个线程提供一个局部变量,但是不是通过threadLocal来完成的,而是需要在应用层面上来保证的,如果在应用上为每一个线程分配了相同的对象实例,那么ThreadLocal也不能保证线程安全。
     当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
    4个方法
        void set(Object value)
        public Object get()
        public void remove()
        protected Object initialValue()

创建threadLocal:

    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        protected Integer initialValue() {

            return 0;
        };

    };

    底层就是一个ThreadLocalMap可以理解为一个map集合,key是当前线程
    内存泄露
        当我们使用线程池,意味着当前线程未必会退出。我们将一些对象设置到ThreadLocal中(它实际保存在线程持有的ThreadLocalMap内),可能会使系统出现内存泄露的问题(用完未清理),使用完了再也不用了,但是却无法被回收。ThreadLocalMap更加类似WeakHashMap,它的实现使用了弱引用,java虚拟机在垃圾回收时,如果发现弱引用,就会立即回收
        解决办法:使用ThreadLocal.remove()方法将变量移除,或者直接赋值null,那么这个对象会更容易被垃圾回收器发现,从而加速回收。(如:t = null,那么这个ThreadLocal对应的所有线程的局部变量都有可能被回收)。

参考书籍《JAVA高并发程序设计》

发布了18 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_32285039/article/details/103489316
今日推荐