22-08-30 西安JUC(03) Callable接口、BlockingQueue阻塞队列、ThreadPool线程池

Callable接口

1、为什么使用Callable接口

Thread和Runnable

都有的缺点:启动子线程的线程 不能获取子线程的执行结果,也不能捕获子线程的异常

从java5开始,提供了Callable接口,是Runable接口的增强版。用Call()方法作为线程的执行体,增强了之前的run()方法。因为call方法可以有返回值,也可以声明抛出异常。

1.Runnable方式创建:在主线程里捕获子线程的异常? 不可以。

try {
    //Runnable方式:主线程无法捕获子线程执行的异常
    new Thread(()->{
            int i = 1/0;
            String result = "jieguo";
    }).start();
} catch (Exception e) {
    System.out.println("主线程获取到了子线程异常:"+ e.getMessage());
}

根据控制台打印,在main线程里并没有捕获到子线程出现的异常


2、使用Callable创建线程并运行

Callable接口中注意点: 可以使用泛型<V>指定返回值类型,并且call方法可以抛出异常

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

使用Callable创建的子线程需要借助FutureTask对象来执行它的call方法

无论哪种方式创建多线程 都必须借助Thread对象的start方法启动线程,Thread只能接受Runnable对象: thread.run()-> runnable.run()

public static void main(String[] args) {
    Callable<String> callable = ()->{
        System.out.println("callable的call方法执行了.....");
        return "hehe....";
    };
    //juc包中提供了FutureTask  间接实现了Runnable接口,并实现了run方法
    new Thread(new FutureTask<String>(callable)).start();
}

1.Thread 调用了start方法后,系统CPU调度执行线程的run方法,run方法中判断 传入的runnable对象如果不为空则调用它的run方法。


2.我们传入的runnable对象是FutureTask的对象,所以调用的是FutureTask的run方法


3.FutureTask的run方法执行时,调用了我们传入的Callable对象的call方法执行 并接受返回的结果

总的来说:就是移花接木,FutureTask间接实现了Runnable接口 并实现了run方法:run方法中调用了Callable的call方法

java.util.concurrent.FutureTask

1.juc包中提供了FutureTask  间接实现了Runnable接口,并实现了run方法

public class FutureTask<V> implements RunnableFuture<V> 

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

2.FutureTask的run方法执行时的结果 和异常 会通过FutureTask的成员属性接收,并通过一个布尔类型的标记记录执行是否有异常

//结果和异常使用同一个变量接收
private Object outcome;

3.FutureTask中会在run方法执行结束时  将线程执行的状态从0(线程还未执行完毕)改为1(线程执行结束)

private volatile int state;
private static final int NEW          = 0;
private static final int COMPLETING   = 1;

3、FutureTask的get()方法

在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。

callable可以返回方法的执行结果:通过 futureTask的get方法 阻塞获取返回结果

public static void main(String[] args) {
    //Callable:获取执行结果+捕获异常
    Callable<String> callable = ()->{
        System.out.println(Thread.currentThread().getName()+"执行了call方法..");
        return "haha....";
    };
    //FutureTask它的泛型跟Callable的泛型要一样,因为都是代表该子线程的执行结果类型
    FutureTask<String> futureTask = new FutureTask<String>(callable);
    new Thread(futureTask,"AA").start();
    try {
        //获取子线程执行的结果   调用get方法会阻塞主线程,所以一定要将获取子线程结果的操作写在方法的最后
        String result = futureTask.get();
        System.out.println("主线程获取到的子线程结果:"+futureTask.get());
    } catch (Exception e) {//捕获子线程执行的异常
        System.out.println(Thread.currentThread().getName()+"  获取到子线程的异常:"+e.getMessage());
    }
}

在使用futureTask的get方法时,要去捕获异常,可以获取子线程的异常

    public static void main(String[] args) {
        //Callable:获取执行结果+捕获异常
        Callable<String> callable = ()->{
            int i = 1/0;
            System.out.println(Thread.currentThread().getName()+"执行了call方法..");
            return "haha....";
        };

        //FutureTask它的泛型跟Callable的泛型要一样,因为都是代表该子线程的执行结果类型
        FutureTask<String> futureTask = new FutureTask<String>(callable);

        new Thread(futureTask,"AA").start();

        try {
            //获取子线程执行的结果   调用get方法会阻塞主线程,所以一定要将获取子线程结果的操作写在方法的最后
            String result = futureTask.get();
            System.out.println("主线程获取到的子线程结果:"+futureTask.get());

        } catch (Exception e) {//捕获子线程执行的异常
            System.out.println(Thread.currentThread().getName()+"  获取到子线程的异常:"+e.getMessage());
        }
    }

控制台打印:

为什么说get方法会阻塞主线程呢?

//以下为FutureTask的源码
public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}

开发时:一定要把获取子线程结果的位置放在方法的最后


 4、FutureTask的复用

只计算一次,FutureTask会复用之前计算过得结果

 public static void main(String[] args) {
     //FutureTask复用问题:  执行异步任务时 为了提高效率它会缓存执行结果
     FutureTask<String> futureTask = new FutureTask<>(() -> {
         System.out.println(Thread.currentThread().getName()+"...");
         return "haha..";
     });
     new Thread(futureTask,"AA").start();
     new Thread(futureTask,"BB").start();
 }
执行异步任务时 为了提高效率它会缓存执行结果。。所以BB线程是从缓存拿的,并没有去走call方法

不想复用之前的计算结果。怎么办?再创建一个FutureTask对象即可

public static void main(String[] args) {
    //FutureTask复用问题:  执行异步任务时 为了提高效率它会缓存执行结果
    FutureTask<String> futureTask = new FutureTask<>(() -> {
        System.out.println(Thread.currentThread().getName()+"...");
        return "haha..";
    });
    FutureTask<String> futureTask2 = new FutureTask<>(() -> {
        System.out.println(Thread.currentThread().getName()+"...");
        return "haha..";
    });
    
    new Thread(futureTask,"AA").start();
    new Thread(futureTask2,"BB").start();
}


5、Callable接口与Runnable接口的区别

老师版:

1、callable有返回值   可以抛出异常
2、runnable可以直接通过Thread启动
3、callable需要通过FutureTask来接收,再由Thread启动
4、callable的任务方法时call(),runnable的是run()

笔记版:

相同点:都是接口,都可以编写多线程程序,都采用Thread.start()启动线程

不同点:

  1. 具体方法不同:一个是run,一个是call

  2. Runnable没有返回值;Callable可以返回执行结果,是个泛型

  3. Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能在内部消化,不能往上继续抛

  4. 它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。


阻塞队列

1、队列 queue 和 栈 stack

queue: 先进先出,后进后出。怎么容易理解呢:左进右出

线程池使用、mq也使用了

---------------------

Stack: 特点 先进后出 后进先出

public class Stack<E> extends Vector<E>{// Stack就是一个集合类  是一个线程安全的集合类
	//1、入栈方法
    public E push(E item) {
        addElement(item);
        return item;
    }
    // 数组:连续的一块内存,按照添加的索引先后顺序有序
    // Vector中的方法:添加元素到 elementData元素数组中
    public synchronized void addElement(E obj) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;//将元素添加到elementData数组的最后一个位置
    }
    //2、出栈方法
    public synchronized E pop() {
        E       obj;
        int     len = size();
        obj = peek();
        removeElementAt(len - 1);//将获取到的最后一个位置元素删除

        return obj;
    }
    // 获取数组最后一个索引的元素 返回
    public synchronized E peek() {
        int     len = size();//获取元素个数

        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);//数组长度-1  也就是数组最后一个位置的元素
    }
}

扩展

方法栈:java代码执行时,每个线程jvm会为他创建一个栈来存储线程调用方法的执行过程数据

线程方法栈。每个方法都是一个栈帧


2、阻塞队列 BlockingQueue

线程池用来存不能及时处理的任务的数据结构

BlockingQueue:阻塞队列

在开发中我们不用关心向队列中添加元素的线程  如果队列满了 它如何阻塞等待

 获取队列中元素的线程 如果队列空了 它如何阻塞等待  以及阻塞的线程如何被唤醒

BlockingQueue是一个接口,继承Queue接口,Queue接口继承 Collection接口

BlockingQueue接口主要有以下7个实现类

  1. ArrayBlockingQueue:由数组结构组成的有界阻塞队列。

  2. LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。

  3. PriorityBlockingQueue:支持优先级排序的无界阻塞队列。

  4. DelayQueue:使用优先级队列实现的延迟无界阻塞队列。

  5. SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。

  6. LinkedTransferQueue:由链表组成的无界阻塞队列。

  7. LinkedBlockingDeque:由链表组成的双向阻塞队列。

线程池使用阻塞队列

  1. ArrayBlockingQueue: 数组阻塞队列
  2. LinkedBlockingQueue:链表阻塞队列
  3. SynchronousQueue:同步阻塞队列(不存储元素)

ArrayBlockingQueue创建时手动指定的长度就是该队列的最大的长度

LinkedBlockingQueue:
        默认长度:Integer.MAX_VALUE   最多存储21亿左右的元素

阻塞队列:添加元素  获取元素 移除元素的方法有4套,根据方法是否返回结果 是否抛出异常 是否可以阻塞 是否可以阻塞超时划分

抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time, unit)
获取 element() peek() 不可用 不可用

要是看着懵,是因为没继续看下去,看完再回来看就一目了然了


3、抛出异常的方法

add正常执行返回true,element(不删除)和remove正常执行会返回阻塞队列中的第一个元素 ​

  • 当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full ​
  • 当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException ​
  • 当阻塞队列空时,再调用element检查元素会抛出NoSuchElementException

add():添加元素失败抛出异常

public static void main(String[] args) {
    //数组阻塞队列初始化时需要传入  初始化数组的长度
    ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
    System.out.println("1:"+queue.add("a"));
    System.out.println("2:"+queue.add("b"));
    System.out.println("3:"+queue.add("c"));
    System.out.println("4:"+queue.add("d"));
}

----------------------

remove():移除成功返回移除的数据 移除失败抛出异常

public static void main(String[] args) {
    //数组阻塞队列初始化时需要传入  初始化数组的长度
    ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
    System.out.println("1:"+queue.add("a"));
    System.out.println("2:"+queue.add("b"));
    System.out.println("3:"+queue.add("c"));
    //移除成功返回移除的数据
    System.out.println(queue.remove());
    System.out.println(queue.remove());
    System.out.println(queue.remove());
    //移除失败抛出异常
    System.out.println(queue.remove());
}


--------------------------

element():获取最先添加的元素 获取不到抛出异常

 public static void main(String[] args) {
     //数组阻塞队列初始化时需要传入  初始化数组的长度
     ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
     //获取最先添加的元素,即左进右出情况下,获取最右边的元素,获取不到抛出异常
     System.out.println(queue.element());
     System.out.println("1:"+queue.add("a"));
 }


 4、返回特殊值的方法

offer()/poll()/peek()

offer() 插入方法,成功ture失败false

 public static void main(String[] args) {
     //数组阻塞队列初始化时需要传入  初始化数组的长度
     ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
     //添加成功ture,添加失败false
     System.out.println(queue.offer("a"));
     System.out.println(queue.offer("b"));
     System.out.println(queue.offer("c"));
     System.out.println(queue.offer("d"));
 }

------------------------------- 

poll() 移除方法,成功返回出队列的元素,队列里没有就返回null

public static void main(String[] args) {
    //数组阻塞队列初始化时需要传入  初始化数组的长度
    ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
    //添加成功ture,添加失败false
    System.out.println(queue.offer("a"));
    System.out.println(queue.offer("b"));
    System.out.println(queue.offer("c"));
    //移除元素成功返回出队列的元素,移除失败返回null
    System.out.println(queue.poll());
    System.out.println(queue.poll());
    System.out.println(queue.poll());
    System.out.println(queue.poll());
}

-------------------------------

peek() 获取方法,成功返回队列中的元素,没有返回null

public static void main(String[] args) {
    //数组阻塞队列初始化时需要传入  初始化数组的长度
    ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
    //获取成功返回队列中的最右边的元素,没有返回null
    System.out.println(queue.peek());
    
    //添加成功ture,添加失败false
    System.out.println(queue.offer("a"));
    System.out.println(queue.offer("b"));
    System.out.println(queue.offer("c"));
    //获取成功返回队列中的最右边的元素,没有返回null
    System.out.println(queue.peek());
}


5、阻塞等待方法

put() / take()

如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。

当阻塞队列满时,再往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出 ​ 当阻塞队列空时,再从队列里take元素,队列会一直阻塞消费者线程直到队列可用

public static void main(String[] args) throws InterruptedException {
    //数组阻塞队列初始化时需要传入  初始化数组的长度
    ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
    //put给队列中从添加元素,左进右出
    queue.put("a");
    queue.put("b");
    queue.put("c");
    System.out.println("take前,队列大小:"+queue.size());
    
    //take取走队列中最右面的元素
    System.out.println(queue.take());
    System.out.println("take后,队列大小:"+queue.size());
}


6、超时等待方法

offer(  timeout) /poll(timeout)

  • offer( timeout):阻塞等待添加元素,成功返回true 超时失败返回false
  • poll(timeout): 超时不能移除元素返回null
public static void main(String[] args) throws InterruptedException {
    //数组阻塞队列初始化时需要传入  初始化数组的长度
    ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
    //poll(timeout): 超时不能移除元素返回null
    System.out.println(queue.poll(3, TimeUnit.SECONDS));
    //offer(  timeout):阻塞等待添加元素,成功返回true  超时失败返回false
    System.out.println(queue.offer("aa", 3, TimeUnit.SECONDS));
    System.out.println(queue.offer("cc", 3, TimeUnit.SECONDS));
    System.out.println(queue.offer("bb", 3, TimeUnit.SECONDS));
    System.out.println(queue.offer("dd", 3, TimeUnit.SECONDS));
}

如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。 ​


7、阻塞等待方法的源码

put() / take()

请欣赏我的画作,以我的理解应该是这么画的。。。

put:
添加元素时使用ReentrantLock加锁
如果队列的长度等于队列中存入元素的个数代表队列已满,当前线程使用notFull.await()进入阻塞等待状态 

//以下为ArrayBlockingQueue的源码
public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

如果队列的长度不等于队列中存入元素的个数代表队列未满,当前线程将元素添加到队列数组中,notEmpty.signal()唤醒等待消费队列中元素的线程

//以下为ArrayBlockingQueue的源码
private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    notEmpty.signal();
}

take:
移除元素时使用Lock加锁
如果队列中元素个数为0,代表队列是空的,当前线程使用notEmpty.await()让自己等待

//以下为ArrayBlockingQueue的源码
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}

如果队列中元素个数>0,队列中有元素,当前线程获取元素返回 并调用notFull.signal()唤醒向队列添加元素阻塞的线程

//以下为ArrayBlockingQueue的源码
private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal();
    return x;
}

ThreadPool线程池

线程池作用

维护复用线程控制线程的数量,线程对系统比较珍贵 ,一个CPU如果执行的线程过多,会频繁的切换每个线程的上下文,线程过多 性能会下降,所以在多核的系统中会使用多线程 但是线程的数量也不会无限创建

线程池特点

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。​​

  2. 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。

  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

1、线程池架构

Executor接口:线程池顶级接口

//线程池顶级父接口
public interface Executor {
    //只有一个execute方法,执行Runnable任务
    void execute(Runnable command);
}

ExecutorService接口:继承了Executor并对它进行了扩展

public interface ExecutorService extends Executor {

 void shutdown();//关闭线程池

 <T> Future<T> submit(Callable<T> task);//执行Callable任务并返回任务结果

 Future<?> submit(Runnable task);//重载方法,执行Runnable任务

}


    
找实现类,执行延时任务

ThreadPoolExecutor:实现了ExecutorService,是以后最常用的线程池类
ScheduledThreadPoolExecutor:继承了ThreadPoolExecutor,可以执行延迟任务(可以实现简单的定时任务)    

public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService {
}

2、工具类Executors

为了方便创建线程池对象,juc包中提供了工具类Executors可以快速创建线程池对象:

Executors: 内部提供了多种初始化线程池的方法

线程池对象的使用

1、执行任务
void execute(Runnable r);//执行任务 没有返回结果
Future<T> submit(Callable<T> c);//执行任务并返回任务结果,通过返回的future对象调用get方法获取任务结果


2、关闭线程池
shutdown();


3、4种常见线程池

public static ExecutorService newCachedThreadPool():初始化执行短期任务的线程池 

当需要执⾏很多短时间的任务时, CacheThreadPool 的线程复⽤率⽐较⾼, 会显著的提⾼性能。
public static void main(String[] args) throws InterruptedException {
    //执行短期任务的线程池  可以开的线程较多
    //如果没有空闲的线程 它会新创建一个线程处理请求
    ExecutorService pool = Executors.newCachedThreadPool();
    for (int i = 1; i <= 200; i++) {
        int a = i;
        pool.execute(()->{
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"执行任务... i = " + a);
        });
    }
}

控制台效果:直接干到200个线程。。。来处理任务

newCachedThreadPool()

1、如果有新的任务,会立即创建新线程处理,因为线程池使用的是不存储元素的阻塞队列SynchronousQueue

2、最多可以创建Integer.MAX_VALUE多个线程 ,过多线程也会导致以后出现oom,因为每个线程都需要自己的栈空间 

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

public static ExecutorService newFixedThreadPool(int nThreads):初始化固定线程数量线程池

public static void main(String[] args) throws InterruptedException {
    //只有固定数量的线程,不会再新创建,有任务不能及时处理则存到阻塞队列中,线程池使用的是LinkedBlockingQueue
    ExecutorService pool = Executors.newFixedThreadPool(3);
    for (int i = 1; i <= 100; i++) {
        int a = i;
        pool.execute(()->{
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" i = "+ a);
        });
    }
}

控制台效果:不管多少个任务,都是指定个数的(这里是3个)线程来处理任务

newFixedThreadPool(

1、只有固定数量的线程,不会再新创建,有任务不能及时处理则存到阻塞队列中,线程池使用的是LinkedBlockingQueue

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

2、最多可以缓存Integer最大值个任务, 也可能出现oom

LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

public static ExecutorService newSingleThreadExecutor():创建单个线程的线程池

可以用来执行固定耗时任务,,,说实话我觉得这个玩意设计出来有啥用

public static void main(String[] args) throws InterruptedException {
    //和FixedThreadPool一样,但是线程数量固定为1个,任务队列长度为Integer的最大值,任务过多可能出现OOM
    ExecutorService pool = Executors.newSingleThreadExecutor();
    for (int i = 1; i <= 100; i++) {
        int a = i;
        pool.execute(()->{
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" i = "+ a);
        });
    }
}

控制台效果:不管多少个任务,都是1个线程来处理任务

newSingleThreadExecutor()

1、任务队列长度为Integer的最大值,任务过多可能出现OOM

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize):创建执行延迟任务的固定线程数量的线程池

这里的执行任务不是execute,也不是submit。是否考虑上一个任务结束,分为俩种。

public static void main(String[] args) throws InterruptedException {
    //系统线程池之ScheduledThreadPool:延迟任务线程池   一般用来处理简单的定时操作
    ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
    //3秒后执行第一次传入的任务,以后每过5秒执行一次传入的任务
    pool.scheduleWithFixedDelay(()->{
        System.out.println(Thread.currentThread().getName()+"...."+new Date());
    },3,5, TimeUnit.SECONDS);
    System.out.println(new Date());
}

控制台效果:先打印当前日期时间,过了3秒后打印第一行,接着每隔5秒打印一行 

newScheduledThreadPool()

1、和CachedThreadPool类似,通过一个空的延迟阻塞队列缓存任务,有新的任务时会分配一个线程来处理

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}


public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService {

    //静态内部类
    static class DelayedWorkQueue extends AbstractQueue<Runnable>
        implements BlockingQueue<Runnable> {
    }
}

2、任务过多会创建多个线程处理请求,线程数量最大为Integer的最大值,也会导致OOM

总结

线程池不允许使用 Executors 去创建,而是通过自定义线程池方式,这样可以更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 各个方法的弊端:
1)newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。 
2)newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

开发中如果使用线程池 强制使用ThreadPoolExecutor来创建

阿里开发手册


4、自定义线程池

线程池的7大参数

int corePoolSize,  核心线程数
    线程池运行稳定后最终收缩到的 线程数


int maximumPoolSize, 最大线程数
    线程池运行时,核心线程可能不够用,为了保证线程池可以及时处理并发访问的大量请求,可以设置最大线程数
    最大线程数-核心线程数 为 线程池最多可以临时创建的处理高并发请求的线程数


long keepAliveTime, 存活时间
    多余的空闲线程的存活时间 当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到 只剩下corePoolSize个线程为止


TimeUnit unit, 存活时间单位


BlockingQueue<Runnable> workQueue, 任务队列
    核心线程不能及时处理的任务 优先存到阻塞任务队列中缓存
    核心线程空闲后 会自动取出队列中的任务执行(这话不对改为 :任务队列的任务由空闲的线程处理)


ThreadFactory threadFactory, 线程工厂
    用来初始化线程对象,一般使用Executors提供的defaultThreadFactory


RejectedExecutionHandler handler 拒绝策略(任务不能处理时的拒绝处理器)
    当任务没有新的线程可以分配,同时任务队列已满,此时线程池会使用拒绝策略来拒绝请求

自定义线程池:

线程池是维护管理一组线程的。for循环主线程执行的,只是为了给线程池添加任务

public static void main(String[] args) {
    //核心线程数为3,最大可以创建10个线程,任务队列长度为5,线程空闲存活时间为1秒的线程池
    ThreadPoolExecutor pool = new ThreadPoolExecutor(3,10,
            1000,TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(5),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());
    for (int i = 1; i <= 15; i++) {
        int a = i;
        pool.execute(()->{
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"执行了任务:i = "+a);
        });
    }
}

控制台运行效果:

我还以为我懂了呢,现实给我一大嘴巴子。。。我还他妈傻乎乎的以为阻塞队列中的任务都是由核心线程来处理呢!

1.任务队列的任务由空闲的线程处理

2.还有核心线程数量固定,但是不一定哪个线程才能活到最后,最终剩余的存活的线程数量是配置的核心线程数


5、线程池的底层工作原理

以线程池为核心来看,任务是外来的。也就是说在任务达到时,不同的状况的线程池对任务的处理方式是不一样的。

线程池初始化时,线程池中的线程数量为0

重要的事情说三遍:以下重要:以下重要:以下重要:

  1. 在创建了线程池后,线程池中的线程数为零(懒加载)。等到有任务过来的时候才会创建线程。也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建corePoolSize个线程

  2. 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:

    1. 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

    2. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列

    3. 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

    4. 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。

  4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:

    如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。

    所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小


6、拒绝策略

据绝策略:当来了新的任务,且线程池没有空闲的线程处理,任务队列已满时。使用拒绝策略拒绝不能被及时处理的任务

AbortPolicy: 默认拒绝策略

不能处理的任务抛出异常,这种方式好在:捕捉到异常便于动态调整线程池的参数

public static void main(String[] args) {
    ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy() //AbortPolicy: 默认拒绝策略,不能处理的任务抛出异常
            );
    //选8:是因为最大线程数+任务队列=5+2=7
    for (int i = 1; i <= 8; i++) {
        int a = i;
        pool.execute(()->{
            try {
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+" 执行了任务:"+a);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}

控制台运行会抛出异常,符合预期效果

CallerRunsPolicy: 调用者执行的拒绝策略

线程池不能处理的任务,将任务回退到调用线程

此时控制台打印,符合预期。由main线程处理第8个任务,其实是手动调用run方法

DiscardPolicy: 丢弃任务的拒绝策略
线程池将不能处理的任务直接丢弃掉,不会抛出异常

此时控制台打印如下,符合预期,因为第8个任务被丢弃了。。丢弃任务应用场景:评论或者点赞啥的

DiscardOldestPolicy: 丢弃等待时间最长的任务的拒绝策略
线程池将任务队列中等待时间最长的任务丢弃

此时控制打印如下,符合预期。因为队列中的元素是4,5,6.任务4是最早放入队列即队列中等待时间最长的任务,直接丢弃

自定义拒绝策略:有些任务不能处理时 我们也不希望抛出异常 也不希望丢弃消息  可以自定义策略

RejectedExecutionHandler:可以通过Lambda表达式创建对象

public interface RejectedExecutionHandler {
    //该接口只有这么一个方法
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

参数1:表示线程池不能执行的任务

参数2:表示不能处理任务的线程池

自定义拒绝策略

这里是使用list集合,把不能处理的任务存起来,再通过额外的线程去处理。

也可以使用run让主线程处理这个任务

public static void main(String[] args) {
    //创建一个线程安全的list集合
    List<Runnable> list = Collections.synchronizedList(new ArrayList<>());
    ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(),
            (r,executor)->{//参数1:表示线程池不能执行的任务, 参数2:表示不能处理任务的线程池
                System.out.println("executor: "+executor);
                list.add(r);//把不能处理的任务存起来,再通过额外的线程去处理
                //r.run();//手动在主线程中调用runnable任务的run方法
            });
    System.out.println("pool: "+pool);
    //选8:是因为最大线程数+任务队列=5+2=7
    for (int i = 1; i <= 8; i++) {
        int a = i;
        pool.execute(()->{
            try {
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+" 执行了任务:"+a);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}

控制台打印如下:


7、线程复⽤原理

ThreadPoolExecutor 在创建线程时,会将线程封装成⼯作线程 worker , 并放⼊⼯作线程组中,然后这个worker 反复从阻塞队列中拿任务去执⾏。

8、线程池的线程数如何设置

面试题:有没有使用过线程池,自定义时如何配置线程数?

开发中我们可以把任务分为计算(CPU)密集型和IO密集型。

CPU计算密集型任务:因为每个任务都需要cpu持续计算操作,如果一个cpu执行了多个这样的线程任务频繁的在多个线程中切换,花在任务切换的时间就越多,CPU执行任务的效率就越低 ,所以线程池中线程数应该设置为 cpu核心数+1

IO密集型任务: 每个任务进行IO操作时,CPU计算可能连1%的时长都用不到,99%的时间都花在IO上。此时一个CPU执行多个任务效率最高。阻塞系数一般设置为(0.8~0.9) 。

所以线程池中线程数设置为:cpu核数/(1-阻塞系数)

生产环境核心线程数设置: IO密集型:10*cpu核数

队列一般使用有界队列

ArrayBlockingQueue(10)   内存连续 查询快 内存利用率不高
LinkedBlockingQueue(10) 可以利用内存碎片提高内存使用率 查询慢

猜你喜欢

转载自blog.csdn.net/m0_56799642/article/details/126600473