Java并发JUC(五)

77 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情

本文主要讲解JUC中的阻塞队列、同步队列和线程池,线程池内容包含三大方法、七大参数、四大拒绝策略。

10. 阻塞队列

写入:如果队列满,就必须阻塞等待;取出:如果队列空,必须阻塞等待生产。

继承关系简略结构:

image.png

队列添加、移除、检测队列首元素方法:

会抛出异常的:add(添加)、remove(移除)、element(检测队首元素)

有返回值,不抛出异常:offer(添加)、poll(移除)、peek(检测队首)

阻塞等待:put、take

超时等待:offer和poll

  • put和take是无返回值的
  • offer和poll是可以含有多个参数的
  • * offer有三个参数,第一个参数是添加的东西,第二个是等待的时间,第三个是时间的单位
    * poll有两个参数,第一个是等待的时间,第二个是时间的单位
    * 满了等待,不抛异常不返回布尔;等待,阻塞(等待超时,超时就不等了)
    
public static void test4() throws InterruptedException {
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(blockingQueue.offer("a",2, TimeUnit.SECONDS));
    System.out.println(blockingQueue.offer("b",2,TimeUnit.SECONDS));
    System.out.println(blockingQueue.offer("c",2,TimeUnit.SECONDS));
    System.out.println(blockingQueue.offer("d",2,TimeUnit.SECONDS));
    System.out.println("=================");
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));
}

同步队列SynchronousQueue

没有容量,进去一个元素就要等待这个元素被取出,才能再往里面放一个元素。

用的是put和take方法。

public static void main(String[] args) {
    BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
​
    //两个线程一个放一个取
    new Thread(()->{
​
        try {
            System.out.println(Thread.currentThread().getName() + " put 1");
            blockingQueue.put("1");
            System.out.println(Thread.currentThread().getName() + " put 2");
            blockingQueue.put("2");
            System.out.println(Thread.currentThread().getName() + " put 3");
            blockingQueue.put("3");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    },"T1").start();
​
    new Thread(()->{
        try {
            TimeUnit.SECONDS.sleep(3);
            System.out.println(Thread.currentThread().getName() +"="+ blockingQueue.take());
            TimeUnit.SECONDS.sleep(3);
            System.out.println(Thread.currentThread().getName() +"="+ blockingQueue.take());
            TimeUnit.SECONDS.sleep(3);
            System.out.println(Thread.currentThread().getName() +"="+ blockingQueue.take());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    },"T2").start();
}

运行结果:

T1 put 1
T2=1
T1 put 2
T2=2
T1 put 3
T2=3

11.线程池

内容:三大方法、七大参数、四种拒绝策略。

池化技术:程序的运行本质是占用系统的资源,而池化技术则可以优化资源的使用。

池化技术,实现准备好一些资源,有人要用,就直接拿,拿完还回来。

线程池的好处:降低资源消耗;提高响应速度;方便管理。线程复用、可以控制最大并发数、管理线程。

三大方法

ExecutorService threadPool = Executors.newSingleThreadExecutor();//线程池中只有单个线程。任务都是由这一个线程执行
Executors.newFixedThreadPool(5);//创建一个固定的线程池的大小,任务有固定的这几个线程执行
Executors.newCachedThreadPool();//可伸缩的,遇强则强、遇弱则弱
  • newSingleThreadExecutor()这个方法使得线程池中只有单个线程,任务都是由该线程去完成的。
  • newFixedThreadPool(5);这种方式创建的线程池中线程数量是固定的。
  • newCachedThreadPool();这种方式创建的线程池中线程数量不固定。

【注】:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式创建,这样更能明确线程池的运行规则以及规避资源耗尽的风险。

Executors.newSingleThreadExecutor();Executors.newFixedThreadPool(5);这两种方式创建的线程允许的最大长度是Integer.MAX_VALUE,大约21亿个,可能会堆积大量的请求,从而导致OOM。newCachedThreadPool()允许创建的线程数Integer.MAX_VALUE,也不要用。

线程池基本使用方式【以Exector举例】

创建线程池,使用线程池来创建线程,最后用完线程要关闭线程池。

 public static void main(String[] args) {
        //三大方法
        ExecutorService threadPool = Executors.newSingleThreadExecutor();//线程池中只有单个线程。任
        try {
            for (int i = 0; i < 10; i++) {
                //使用线程池之后要使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + "ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //线程池用完,程序结束要关闭线程池。
            threadPool.shutdown();
        }
    }

七大参数

上面的三种方式本质是ThreadPoolExecutor()。下面是ThreadPoolExecutor的源码

public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小
  int maximumPoolSize, // 最大核心线程池大小
  long keepAliveTime, // 超时了没有人调用就会释放
  TimeUnit unit, // 超时单位
  BlockingQueue<Runnable> workQueue, // 阻塞队列
  ThreadFactory threadFactory, // 线程工厂:创建线程的,一般不用动
  RejectedExecutionHandler handle // 拒绝策略) {
  if (corePoolSize < 0 ||
    maximumPoolSize <= 0 ||
    maximumPoolSize < corePoolSize ||
    keepAliveTime < 0)
    throw new IllegalArgumentException();
  if (workQueue == null || threadFactory == null || handler == null)
    throw new NullPointerException();
  this.acc = System.getSecurityManager() == null ?
    null :
  AccessController.getContext();
  this.corePoolSize = corePoolSize;
  this.maximumPoolSize = maximumPoolSize;
  this.workQueue = workQueue;
  this.keepAliveTime = unit.toNanos(keepAliveTime);
  this.threadFactory = threadFactory;
  this.handler = handler;
}

七大参数:

int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大核心线程池大小
long keepAliveTime, // 超时了没有人调用就会释放
TimeUnit unit, // 超时单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂:创建线程的,一般不用动
RejectedExecutionHandler handle // 拒绝策略

线程池核心大小指一般任务只要用核心数量的线程,不会全部线程都用。当队列满了,就会唤醒其他线程。拒绝策略就是队列满了,再来新的就会采用拒绝策略。七大参数用银行例子来讲解就是:银行有五个窗口办事(最大线程数),一般情况下只有两个窗口开着应对办事(核心线程池数),有需要到银行办事的来了,就先去窗口办事,开的两个窗口人满了就需要再候客区等待(阻塞队列),候客区假设有三个座位。今天来银行办事的人有点多,候客区也满了,然后银行那边就打电话让剩下三个窗口的工作人员回来工作,然后五个窗口都开了(阻塞唤醒)。那如果这个时候又有人来了,那他只能站着等了,或者离开(拒绝策略)。(保持时间)和(超时单位)都好理解,线程工厂一般不需要管。看下面的程序,相信很好理解。

public class Demo01 {
    public static void main(String[] args) {
        // 自定义线程池!工作 ThreadPoolExecutor
        ExecutorService threadPool = new ThreadPoolExecutor(
                  2,
                  5,
                  3,
                  TimeUnit.SECONDS,
                  new LinkedBlockingDeque<>(3),
                  Executors.defaultThreadFactory(),
                  new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试去和
        最早的竞争,也不会抛出异常!
        try {
        // 最大承载:Deque + max
        // 超过 RejectedExecutionException
            for (int i = 1; i <= 9; i++) {
              // 使用了线程池之后,使用线程池来创建线程
              threadPool.execute(()->{
                System.out.println(Thread.currentThread().getName()+" ok");
              });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}

四种拒绝策略

利用上述银行例子来解释四种策略

new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会抛出异常!

线程池的最大的大小如何去设置?

两种方式:CPU密集型;IO密集型

CPU核数获取:Runtime.getRuntime().availableProcessors();

CPU密集型:指的指CPU是几核,最大线程就设置几,可以保持CPU效率最高。

IO密集型:判断程序中十分耗IO的线程数量,最大线程设置为其数量的二倍。