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线程池

1、线程池作用

线程池作用

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

线程池特点

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

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

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


2、线程池类结构

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 {
}

3、Executors工具类

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

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

线程池对象的使用

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


2、关闭线程池
shutdown();


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。是否考虑上一个任务结束,分为俩种。

执行的任务 Callable或Runnable接口实现类

schedule方法 

延迟执行任务,延迟10秒后,返回call字符串并输出。

public static void main(String[] args) throws InterruptedException {
    //系统线程池之ScheduledThreadPool:延迟任务线程池   一般用来处理简单的定时操作
    ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
    //延迟任务:延迟15秒后,打印输出
    pool.schedule(()->{
        System.out.println(Thread.currentThread().getName()+"...."+new Date());
    },5, TimeUnit.SECONDS);
    System.out.println(new Date());
}

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

②scheduleAtFixedRate方法

增加initialDelay:第一次执行任务延迟时间

从上一个任务开始执行时计算延迟多少开始执行下一个任务,但是还会等上一个任务结束之后。

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

scheduleWithFixedDelay方法

增加initialDelay:第一次执行任务延迟时间
period:连续执行任务之间的周期,从上一个任务全部执行完成时计算延迟多少开始执行下一个任务

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类似,通过一个空的延迟阻塞队列缓存任务,有新的任务时会分配一个线程来处理

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

总结

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

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

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

阿里开发手册


5、自定义线程池

线程池的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.还有核心线程数量固定,但是不一定哪个线程才能活到最后,最终剩余的存活的线程数量是配置的核心线程数


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

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

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

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

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

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

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

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

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

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

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

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

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

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


7、拒绝策略

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

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();
            }
        });
    }
}

控制台打印如下:


8、线程复⽤原理

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

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

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

开发中我们可以把任务分为计算(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) 可以利用内存碎片提高内存使用率 查询慢

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值