多线程与高并发编程系列(七)之初识线程池

2021/03/21 JUC 共 5285 字,约 16 分钟

本篇将会介绍一道经典面试题的解法,在介绍完毕后将会开始认识线程池,进行线程池入门

经典面试题:两个线程顺序打印:A1B2C3D4

LockSupport解决

public class Test_LockSupport {

    static final char[] c = new char[]{'A','B','C','D','E','F','G','H','I','J'};
    static final int[] cN = new int[]{1,2,3,4,5,6,7,8,9,10};

    static Thread t1 = null;
    static Thread t2 = null;

    public static void main(String[] args) {



        t1 = new Thread(()->{
            for(int i = 0; i < c.length; i++){
                System.out.print(c[i]);
                LockSupport.unpark(t2);
                LockSupport.park();
            }

            LockSupport.unpark(t2);
        }, "T1");

        t2 = new Thread(()->{
            for(int i = 0; i < cN.length; i++){
                LockSupport.park();
                System.out.print(cN[i]);
                LockSupport.unpark(t1);
            }
        }, "T2");

        t1.start();
        t2.start();
    }
}

image

  • 这是一种非常简单的写法,通过LockSupport来有序地阻塞线程,让两个线程有序地打印结果,要注意的地方是,优先打印的线程在for循环结束之后必须在将另外一个线程给unpark,不然会有一个线程永远被park

Wait 、Notify解决

public class Test_WaitNotify {

    static final char[] c = new char[]{'A','B','C','D','E','F','G','H','I','J'};
    static final int[] cN = new int[]{1,2,3,4,5,6,7,8,9,10};

    static Thread t1 = null;
    static Thread t2 = null;

    static Object lock = new Object();
    static volatile boolean t1Started = false;

    public static void main(String[] args) {
        t1 = new Thread(()->{
            synchronized(lock){
                for(int i = 0; i < c.length; i++){
                    if(!t1Started)
                        t1Started = true;

                    System.out.print(c[i]);
                    lock.notify();
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        });

        t2 = new Thread(()->{
            synchronized (lock){
                while (!t1Started) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                for(int i = 0; i < cN.length; i++){
                    System.out.print(cN[i]);
                    lock.notify();
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }
        });

        t1.start();
        t2.start();
    }
}

image

  • 使用Wait 、Notify方法解决问题的时候,思路基本是和LockSupport一致的,注意的细节也是一样的,后打印的线程要激活另一个线程,不然会有线程无法结束
  • 这里面的wait可以背替换为sleep吗?其实这是不行的,因为sleep和wait的最大差别就在于sleep不会释放锁资源;wait会释放锁资源

ReentrantLock解决

public class Test_ReentrantLock {
    static ReentrantLock lock = new ReentrantLock();
    static Condition t1C = lock.newCondition();
    static Condition t2C = lock.newCondition();

    static final char[] c = new char[]{'A','B','C','D','E','F','G','H','I','J'};
    static final int[] cN = new int[]{1,2,3,4,5,6,7,8,9,10};

    static Thread t1 = null;
    static Thread t2 = null;

    public static void main(String[] args) {
        t1 = new Thread(()->{
            try{
                lock.lock();

                for(int i = 0; i < c.length; i++){
                    System.out.print(c[i]);
                    t2C.signal();
                    try {
                        t1C.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                t2C.signal();
            } catch (Exception e){
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "T1");

        t2 = new Thread(()->{
            try {
                lock.lock();

                for(int i = 0; i < cN.length; i++){
                    try {
                        t2C.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.print(cN[i]);
                    t1C.signal();
                }
            } catch (Exception e){
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "T2");

        t2.start();
        t1.start();
    }
}

image

  • 这个方法比较优秀的地方是我们new了两个新的condition,不同的condition就可以存放不同类型的线程,这样在唤醒的时候就可以指定唤醒特定种类的线程,这背后的原理和BlockingQueue类似

线程池学习必备知识

Executor

  • Executor的是在Java 1.5 的时候引入的机制,用处是将工作单元和执行单元分离,而且Executor.executor()方法比Thread.start()更加的优秀,并且能够更加方便的管理线程

Callable

  • Callable接口可以认为是Runnable接口的升级版本,它一样可以交给线程来执行,但是它比Runnable方法多添加了一个返回值

Future

  • Future,顾名思义就是未来,我们使用一个简单的demo来讲解

    public class Executor_Test {
        public static void main(String[] args) {
            ExecutorService srv = Executors.newCachedThreadPool();
      
            Callable<String> c = new Callable<String>() {
                @Override
                public String call() throws Exception {
                    TimeUnit.SECONDS.sleep(3);
                    System.err.println("执行完毕");
                    return "Hello Callable";
                }
            };
            Future<String> future = srv.submit(c);
      
            try {
                String res = future.get();
                System.out.println(res);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
    

    image

  • 最后这个结果的打印其实是要等待线程执行完毕才打印出来了的

  • 当我们向线程池里面submit一个callable的时候,这个方法的返回值就是一个 future 对象,这个future得到值的过程是一个异步的过程

  • future.get()方法是一个阻塞方法,因为future是一个异步得到结果的对象,第一时间这个future内可能不存在返回值,如果我们调用future.get()方法的话,那么我们就会一直阻塞,直到future拿到返回结果

FutureTask

  • FutureTask就是Future+Runnable,是一个Future和Runnable结合,既是一个Future,又可以交给线程去执行

    image

    public class T06_00_Future {
    	public static void main(String[] args) throws InterruptedException, ExecutionException {
      		
    		FutureTask<Integer> task = new FutureTask<>(()->{
    			TimeUnit.MILLISECONDS.sleep(500);
    			return 1000;
    		}); //new Callable () { Integer call();}
      		
    		new Thread(task).start();
      		
    		System.out.println(task.get()); //阻塞
      
      
    	}
    }
    

    我们可以发现,这个FutureTask简单来说就是Future和Task的结合体

线程池简单认识

线程池分类

  • 线程池一共有两种,一个是 ThreadPoolExecutor ,一个是 ForkJoinPool
  • ThreadPoolExecutor和ForkJoinPoo的最大区别就在于ThreadPoolExecutor只有一个共享的任务队列;ForkJoinPool里面有复数的任务队列

new ThreadPoolExecutor

  • 线程池的创建所需7个参数

    public class Executor_Test {
        public static void main(String[] args) {
            ThreadPoolExecutor tpe = new ThreadPoolExecutor(4,6,
                    60,TimeUnit.SECONDS,
                    new LinkedBlockingQueue<>(),
                    Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.AbortPolicy());
        }
    }
    
    1. CorePoolSize:核心线程数
    2. MaxPoolSize:最大线程数
    3. KeepAliveTime:非核心线程的存活时间(不被使用)
    4. TimeUnit:非核心线程存活时间的单位
    5. BolckingQueue:任务队列
    6. ThreadFactory:线程工厂
    7. RejectPolicy:拒绝策略
  • corePoolSize:核心线程数量定义了一个线程池的核心线程数量,核心线程一般来说是不会被回收的,哪怕存留再久也不会被回收,只有当线程池被回收后才会被回收

  • maxPoolSize:最大线程数,最大线程数减去核心线程数就是非核心线程数,非核心线程在存在了一段时间后如果没有被调用就会被回收

  • KeepAliveTime:决定非核心线程被回收的时间

  • TimeUnit:KeepAliveTime的时间单位

  • BolckingQueue:任务队列是用于存放任务的队列

  • ThreadFactory:线程工厂就是用于生产线程的工厂,这个工厂需要实现ThreadFactory接口

  • RejectPolicy:当一个线程池被创建的时候是没有线程的,当往线程池种submit一个任务之后,线程池会创建核心线程,随着任务的添加,如果超过了核心线程数量的话,那么就会将任务放置到任务队列种等待消化,如果任务队列也满了,那么就会开始创建非核心线程来消化任务,如果连非核心线程数量都已经创建到了极限,那么就会执行拒绝策略,拒绝策略除了可以自定义以外,还可以使用JDK提供的拒绝策略,一共有以下4种

    1. AbortPolicy:当额外的任务到来的时候,直接报错
    2. CallerRunsPolicy:发送来任务的线程自己执行这个任务
    3. DiscardPiolicy:将发送过来的任务直接扔掉,不做任何处理
    4. DiscardOldestPolicy:将任务队列中最早加入队列的任务删除,将新的任务加入队列

关于线程池的细节

  1. 线程池操作线程比自己显式创建线程要优秀许多,原因是使用线程池管理线程可以减少在创建和销毁线程上所浪费的系统资源
  2. 线程池、线程都必须要有有意义的名字,这样可以方便纠错

文档信息

Search

    Table of Contents