《多线程之线程池》

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35781178/article/details/89193086

《八:Java 并发包中线程池ThreadPoolExecutor原理》

线程池主要解决两个问题:

一是当执行大量异步任务时线程池能够提供较好的性能。在不使用线程池时,每当需要执行异步任务时直接new 一个线程来运行,而线程的创建和销毁是需要开销的。线程池里面的线程是可复用的,不需要每次执行异步任务时都重新创建和销毁线程。

二是线程池体提供了一种资源限制和管理的手段,比如可以限制线程的个数,动态新增线程等。每个ThreadPoolExecutor 也保留了一些基本的统计数据,比如当前线程池完成的任务数目等。‘

线程池也提供了许多可调用参数和可扩展性接口,以满足不同情景的需要,程序员可以更方便的Executors的工厂方法,比如newCachedThreadPool(线程池线程个数最多可达Integer.Max_value,线程自动回收),newFixedThreadPool(固定大小的线程池),newSingleThreadExecutor(单个线程)等来创建线程池。

Executors 是个工具类,里面提供了好多静态方法,这些方法根据用户选择返回不同的线程池实例。ThreadPoolExecutor继承了AbstractExecutorService,成员变量ctl 是一个Integer 的原子变量,用来记录线程池状态和线程池中线程个数,类似于ReentrantReadWriteLock使用一个变量来保存两种信息。

线程池状态含义
running 接受新任务并且处理阻塞队列里的任务
shutdown 拒绝新任务但是处理阻塞队列里的任务
stop 拒绝新任务并且抛弃阻塞队列里的任务,同时会中断正在处理的任务
tidying 所有任务都执行完(包含阻塞队列里面的任务)后当前线程池活动线程数为0,将要调用terminated方法
terminated 终止状态。terminated方法调用完成以后的状态
线程池状态转换列举:
running--shutdown: 显示调用shutdown()方法,或者隐式调用了finalize()方法里面的shutdown()方法
running/shutdown--stop 显示调用shutdownNow()方法时
shutdown--tidying 当线程池和任务队列都为空时
stop--tidying 当线程池为空时
tidying--terminated 当terminated()hook方法执行完成时
线程池参数如下:
corePoolSize 线程池核心线程个数
workQueue 用于保存等待执行的任务的阻塞队列,比如基于数组的有界ArrayBlockingQueue,基于链表的无界LinkedBlockingQueue,最多只有一个元素的同步队列SynchronousQueue以及优先级队列PriorityBlockingQueue等。

maximunPoolSize

线程池最大线程数量
ThreadFactory 创建线程的工厂
RejectedExecutionHandle 饱和策略,当队列满并且线程个数达到最大后采取的策略,比如抛出异常/使用调用者所在线程来郧西你个任务/调用poll丢弃一个任务,执行当前任务/默默丢弃,不跑出异常
keeyAliveTime 存活实践。如果当前线程池中的线程数量比核心线程数量多,并且是闲置状态,则这些闲置状态的线程能存活的最大时间
TimeUnit 存活时间的时间单位

execute方法的作用是提交任务command 到线程池进行执行。ThreadPoolExecutor的实现实际上是一个生产消费模型,当用户添加任务到线程池时相当于生产者生产元素,workers 线程工作几种的线程直接执行任务或者从任务队列里面获取任务时则相当于消费者消费元素。

总结:线程池使用一个Integer 类型的原子变量来记录线程池状态和线程池中的线程个数。通过线程池状态来控制任务的执行,每个worker 线程可以处理多个任务。线程池通过线程的复用减少了线程的创建和销毁的开销。

《九:Java 并发包中ScheduledThreadPoolExecutor》

ScheduledThreadPoolExecutor 的实现,这是一个可以在指定一定延迟时间后或者定时进行任务调度执行的线程池。

Executors 是个工具类,它提供了很多静态方法,可根据用户的选择返回不同的线程池实例。ScheduledThreadPoolExecutor 继承了ThreadPoolExecutor 并实现了ScheduledExecutorService 接口。线程池队列时DelayedWordQueue,和DelayedQueue类似,是一个延迟队列。

ScheduledFutureTask 是具有返回值的任务,继承自FutureTask .FutureTask 的内部有一个变量state 用来标识任务的状态,一开始状态为new。

ScheduledFutureTask内部还有一个变量period 用来标识任务的类型,任务类型如下:

1.period=0,说明当前任务是一次性的,执行完毕后就退出了

2.period为负数,说明当前任务为fixed-delay任务,是固定延迟的定时可重复执行任务。

3.period 为正数,说明当前任务为fixed-rate任务,是固定频率的定时可重复执行任务。

ScheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)方法:该方法相对起始时间点以固定频率调用指定的任务。当把任务提交到线程池并延迟initialDelay 时间后开始执行任务command。

CountDownLatch :在日常开发中经常会遇到需要在主线程中开启多个线程去并行执行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。在CountDownLatch出现之前一般都使用线程的jion 方法来是实现,但join 不够灵活,不能满足不同场景的需要,所以便产生了CountDownLatch。

public class JoinCountDownLatch2 {
    //创建一个CountDownLatch 实例
    private static CountDownLatch countDownLatch = new CountDownLatch( 2 );
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        //将线程A添加到线程池
        executorService.submit( new Runnable() {
            @Override
            public void run() {
              try{
               Thread.sleep( 1000 );
              }catch (InterruptedException e){
                  e.printStackTrace();
              }finally {
                  countDownLatch.countDown();
              }
                System.out.println("child threadOne over");
            }
        } );
        //将线程B添加到线程池
        executorService.submit( new Runnable() {
            @Override
            public void run() {
              try{
                  Thread.sleep( 1000 );
              }catch (InterruptedException e){
                  e.printStackTrace();
              }finally {
                  countDownLatch.countDown();
              }
                System.out.println("child threadTwo over");
            }
        } );
        System.out.println("wait all child thread over");
        //等待子线程执行完毕,返回
        countDownLatch.await();
        System.out.println("all child thread over");
        executorService.shutdown();
    }
}

总结:countDownLatch 与 join 方法 的区别。一个区别是,调用一个子线程的join()方法后,该线程会一直被阻塞直到线程先运行完毕,而countDownLatch则使用计数器来允许子线程运行完毕或者在运行中递归计数,也就是CountDownLatch可以线程池运行的任何时候让await方法返回而不一定必须等到线程结束。另外,使用线程池来管理线程时一般都是直接添加Runable 到线程池,这时候就没有办法再调用线程的join 了,就是说countDownLatch 相比join 方法让我们对线程同步有更灵活的控制。

boolean await(long timeout ,TimeUnit unit)方法: 当线程调用了countDownLatch 对象的该方法后,当前线程会被阻塞,直到:

1.当所有线程都调用了countDownLatch对象的countDown方法后,也就是计数器为0时,这时候会返回true;

2.设置的timeout 时间到了,因为超时而返回false;

3.其他线程调用了当前线程的interrupt()方法中断了当前线程,当前线程会抛出InterruptedException异常,返回。

回环屏障CyclicBarrier : CountDownLatch 在解决多个线程同步方面相对于调用线程的join 方法作了优化,但是CountDownLatch 的计数器是一次性的,也就是等到计数器值变为0后,再调用CountDownLatch 的await 和countdown 方法都会立刻返回,这就起不到线程同步的效果了。所以满足计数器可以重置的需要,CyclicBarrier 应运而出,它可以让一组线程全部达到一个状态后再全部同时执行。

总结:CycleBarrier 与 CountDownLatch的不同在于,前者可以复用,并且前者特别适合分段任务有序执行的场景。然后它是通过独占锁ReentrantLock 实现计数器原子性更新,并使用条件变量队列来实现线程同步。CycleBarrier 的某个线程运行到某个点上以后,该线程即停止运行,直到所有的线程都到达这个点,所有的线程才重新运行;CycleBarrier只能唤起一个任务,CountDownLatch 可以唤起多个任务。

信号量Semaphore:信号量Semaphore 也是Java 中的一个同步器,与CountDownLatch 和 CycleBarrier 不同的是,它内部的计数器是递增的,并且在一开始初始化semaphore 时可以指定一个初始值,但是并不需要知道需要同步的线程的个数,而是在需要同步的地方调用acquire 方法时执行需要同步的线程个数。

总结:CountDownLatch 通过计数器提供了更灵活的控制,只要检测到计数器值为0,就可以往下执行,相比使用join 必须等待线程执行完毕后主线程才会继续向下运行更灵活。另外 CycleBarrier也可以达到CountDownLatch 的效果,但是后者在计数器值为0后,就不能被复用,而前者则可以使用reset 方法重置后复用,前者对同一个算法但是输入参数不同的类似场景适合。SemapHore采用了信号量递增的策略,一开始并不需要关心同步的线程个数,等调用aquire 方法时再指定需要同步的个数,并且提供了获取信号量的公平性策略。

《Java 并发编程》

在高并发,高流量并且响应时间要求比较小的系统中同步打印日志已经满足不了需求了,因为打印日志本身需要写入磁盘的,写磁盘的操作会暂时阻塞调用打印日志的业务线程,这会造成线程的rt 增加。

log back 使用的是有界队列ArrayBlockingQueue,之所以使用有界队列是考虑内存溢出问题。在高并发下写日志的QPS会很高,如果设置为无界队列,队列本身会占用很大的内存,很可能会造成OOM。tomcat 使用队列把接受请求与处理请求操作进行解耦,实现异步处理。其实tomcat 中nioendPoint中的每个poller 里面都维护一个ConcurrentLinkedQueue,用来缓存请求任务,用来缓存请求任务,其本身也是一个多生产-单消费者模型。

生产者--Acceptor 线程:Acceptor 线程的作用是接受客户端发来的连接请求并将其放入Poller 的事件队列。

//创建map,key为topic,value为设备列表
static ConcurrentHashMap<String,List<String>> map = new ConcurrentHashMap<String, List<String>>(  );

public static void main(String[] args) {
    //进入直播topic1,线程one
    Thread threadOne = new Thread( new Runnable() {
        @Override
        public void run() {
          List<String> list1 = new ArrayList<>( );
          list1.add( "de1" );
          list1.add( "de2" );

          map.put( "topic1",list1 );
            System.out.println(JSON.toJSONString(map));
        }
    } );
    //进入直播间topic1,线程two
    Thread threadTwo = new Thread( new Runnable() {
        @Override
        public void run() {
            List<String> list1 = new ArrayList<>( );
            list1.add( "de11" );
            list1.add( "de22" );
            map.put( "topic1",list1 );
            System.out.println(JSON.toJSONString(map));
        }
    } );
    //进入直播间topic2,线程three
    Thread threadThree = new Thread( new Runnable() {
        @Override
        public void run() {
            List<String> list1 = new ArrayList<>( );
            list1.add( "de111" );
            list1.add( "de222" );
            map.put( "topic2",list1 );
            System.out.println(JSON.toJSONString(map));
        }
    } );
    //启动线程
    threadOne.start();
    threadTwo.start();
    threadThree.start();
}

SimpleDateFormat 是线程不安全的:SimpleDateFormat 是Java提供的一个格式化和解析日期的工具类。SimpleDateFormat 实例里面都有一个Calendar 对象,它之所以线程不安全的,就是因为Calendar 是线程不安全的,后者之所以是线程不安全的,是因为其中存放日期数据的变量都是线程不安全的,比如fields,time等。

bug代码复现:

SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" );
//创建多个线程,并启动
for (int i =0;i<10;++i){
    Thread thread = new Thread( new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(sdf.parse( "2017-12-13 15:15:15" ));
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    } );
    //启动线程
    thread.start();
}

解决办法:

1.每次使用时new 一个SimpleDateFormat的实例,这样可以保证每个由于使用自己的Calendar实例,但是每次使用都需要new一个对象,并且使用后由于没有其他引用,又需要回收,开销会很大。

2.出错的根本原因就是因为多线程下未原子性操作,可以用synchronized进行同步。进行同步意味着多个线程要竞争锁,在高并发场景下会导致系统响应性能下降。

 SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" );
//创建多个线程,并启动
for (int i =0;i<10;++i){
    Thread thread = new Thread( new Runnable() {
        @Override
        public void run() {
            try {
               synchronized (sdf){
                   System.out.println(sdf.parse( "2017-12-13 15:15:15" ));
               }
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    } );
    //启动线程
    thread.start();
}

3.使用ThreadLocal ,这样每个线程只需要使用一个SimpleDateFormat 实例,这就大大节省了对象的创建销毁开销,并且不需要多个线程同步。

static ThreadLocal<DateFormat> sdf = new ThreadLocal<DateFormat>(){
    @Override
    protected SimpleDateFormat initialValue(){
        return new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" );
    }
};
//创建多个线程,并启动
for (int i =0;i<10;++i){
    Thread thread = new Thread( new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(sdf.get().parse( "2017-12-13 15:15:15" ));
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    } );
    //启动线程
    thread.start();
}

使用Timer时需要注意的事情:当一个Timer运行多个TimerTask时,只要其中一个TimerTask在执行中向run方法外抛出了异常,则其他任务也会自动终止。创建线程和线程池时要指定与业务相关的名称:在日常开发中,当在一个应用中需要创建多个线程或者线程池时最好给每个线程或者线程池根据业务类型设置具体的迷你歌词,以便在出现问题时方便进行定位。

//难排查问题:

public static void main(String[] args) {
    //订单模块
    Thread threadOne = new Thread( new Runnable() {
        @Override
        public void run() {
            System.out.println("保存订单的线程");
            try{
                Thread.sleep( 500 );
            }catch (Exception e){
                e.printStackTrace();
            }
            throw new NullPointerException(  );
        }
    } );
    //发货模块
    Thread threadTwo = new Thread( new Runnable() {
        @Override
        public void run() {
            System.out.println("保存收获地址的线程");
        }
    } );
    //启动xianc
    threadOne.start();
    threadTwo.start();
}

//优化代码:

static final String THREAD_SAVE_ORDER = "HREAD_SAVE_ORDER";
static final String THREAD_SAVE_ADDR = "THREAD_SAVE_ADDR";
public static void main(String[] args) {
    //订单模块
    Thread threadOne = new Thread( new Runnable() {
        @Override
        public void run() {
            System.out.println("保存订单的线程");
            try{
                Thread.sleep( 500 );
            }catch (Exception e){
                e.printStackTrace();
            }
            throw new NullPointerException(  );
        }
    },THREAD_SAVE_ORDER);
    //发货模块
    Thread threadTwo = new Thread( new Runnable() {
        @Override
        public void run() {
            System.out.println("保存收获地址的线程");
        }
    },THREAD_SAVE_ADDR );
    //启动xianc
    threadOne.start();
    threadTwo.start();
}线程池也需要名字:

出现问题难以排查的问题的代码:

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

    public static void main(String[] args) {
        //接受用户连接模块
          executorOne.execute( new Runnable() {
              @Override
              public void run() {
                  System.out.println("接受用户连接线程");
                  throw new NullPointerException(  );
              }
          } );
       //具体处理用户请求模块
       executorTwo.execute( new Runnable() {
           @Override
           public void run() {
               System.out.println("具体处理用户请求模块");
           }
       } );
       executorOne.shutdown();
       executorTwo.shutdown();
    }
}

优化代码:

static 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-";
    }
    @Override
    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<>(  )
,new NamedThreadFactory( "ASYN-ACCEPT-POOL" ));
static ThreadPoolExecutor executorTwo = new ThreadPoolExecutor( 5,5,1,TimeUnit.MINUTES,new LinkedBlockingQueue<>(  )
,new NamedThreadFactory( "ASYN-PROCESS-POOL" ));

public static void main(String[] args) {
    //接受用户连接模块
      executorOne.execute( new Runnable() {
          @Override
          public void run() {
              System.out.println("接受用户连接线程");
              throw new NullPointerException(  );
          }
      } );
   //具体处理用户请求模块
   executorTwo.execute( new Runnable() {
       @Override
       public void run() {
           System.out.println("具体处理用户请求模块");
       }
   } );
   executorOne.shutdown();
   executorTwo.shutdown();
}

总结:线程池有利于线程的复用,但请记得关闭shutdown ,否则会导致线程池资源一直不被释放。线程池使用FutureTask 时如果把拒绝策略设置成DiscardPolicy 和 DiscardOldestPolicy,并且在被拒绝的任务的Future 对象上调用了无参get()方法,那么调用线程会一直被阻塞。

如果提交任务时,线程池已满,会发生:

如果是使用的无界队列linkedBlockingQueue ,没关系,继续添加任务到阻塞队列中等待执行,可以无线存放任务。

如果使用有界队列ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,如果它满了,则会使用拒绝策略,处理满了的任务,默认时AbrotPolicy。

猜你喜欢

转载自blog.csdn.net/qq_35781178/article/details/89193086