JAVA多线程&线程池

懂线程者,更胜于面试,更优于性能,与君共勉;

线程

进程和线程:一个进程包括多个线程。把视频软件理解为一个进程,而把视频+音频+弹幕可以理解为是视频软件中互相协作的三个线程;

并行与并发:并行指的是多个线程同时执行各自的任务;并发则是指,单个线程抢占时间片,高速来回切换执行不同的任务。表面上多个线程持续运行,实际上单个线程走走停停

线程的状态

1.1 new() ,创建线程,该线程进入新建状态;

1.2 start(), 创建线程后,调用线程的start()方法,线程会被加入到一个可运行的线程池中,线程进入**就绪(可运行)**状态;

1.3 可运行线程池中的线程被分配到时间片,从可运行状态变为运行状态;

	1.3.1 此状态下,线程如果调用yield()方法,则会让出时间片,重新回到**就绪**状态;

1.4 线程运行期间,如果调用wait(),sleep(),join()方法,线程变成了等待状态需要等待唤醒或者其他线程的调度;

1.5 针对有些情况,不希望线程一直等待靠别的线程唤醒,可以设置超时时间,此时线程则是超时等待状态;

1.6 多线程间访问同步块或者共享资源,没有拿到对应的资源对象锁,则进入阻塞状态,直到拥有锁的线程释放出锁;

1.7 线程执行结束或者抛出异常,进入终止状态;

线程阻塞与线程等待的区别:

  1. 线程等待是线程主动发起的,比如等待子线程执行完某些任务;线程阻塞是被动发生的;

  2. 线程等待一般是已经拿到了锁,进入了同步代码块内;线程阻塞一般是再拿到锁进入绒布代码块之前,因为

    没有拿到锁所以一直阻塞等待;

线程的创建方式

继承Thread类
public class TestThread extends Thread{
    @Override
    public void run() {
        try {
            Thread.sleep(3000);
            System.out.println("三秒过去了~");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        TestThread thread = new TestThread();
        thread.start();
    }
}
实现Runnable接口
public class TestRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("uzi yyds");
    }

    public static void main(String[] args) {
        TestRunnable runnable = new TestRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}
实现Callable接口配合FutureTask
public class TestCallable implements Callable<List<Integer>> {
    List<Integer> list = new ArrayList<>();

    @Override
    public List<Integer> call() throws Exception {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"正在加入:"+i);
            list.add(i);
        }
        return list;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestCallable callable = new TestCallable();
        FutureTask<List<Integer>> task = new FutureTask(callable);
        Thread thread = new Thread(task);
        thread.start();
        System.out.println(task.get());// 此处的get()方法,会使得主线程阻塞等待子线程的任务结果
    }
}

// 输出
Thread-0正在加入:0
Thread-0正在加入:1
Thread-0正在加入:2
Thread-0正在加入:3
Thread-0正在加入:4
Thread-0正在加入:5
Thread-0正在加入:6
Thread-0正在加入:7
Thread-0正在加入:8
Thread-0正在加入:9
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

几种创建方式对比:

首先,实现Runnable接口相对于继承Thread类更灵活,毕竟java有单继承多实现的限制,如果继承了Thread类,就无法再去继承其他类;

其次,实现Callable接口配合FutureTask使用,能够接口线程的返回值,对于某些需要拿到线程执行结果的情况更适合;

再来看看FutureTask

请添加图片描述

已知实现Callable接口方式创建的线程有返回值,能更灵活的应对业务场景。那么相应的,我们也看看FutureTask这个类里面有没有什么可以为我们所用的东西;

创建一个类,实现Callable接口

public class TestCallable2 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
         Thread.sleep(3000);
         System.out.println("子线程执行任务");
         return 1;
    }
}

Q: 已知get()方法会使得主线程阻塞等待子线程,那么如果我不希望一直等下去怎么办?

A: get(long, TimeUnit) 超时等待

请添加图片描述

get()方法可以使得主线程等待子线程的任务执行结束,但是更多的场景是不希望一直等待,get(long,TimeUnit)方法就是设置一个等待时间,如果超时后,则抛出异常,我们可以根据自己的业务需求,做其他处理;

值得注意的是:如果多个线程同时提交了多个任务,get()方法如果不设置超时时间,那么主线程的阻塞时间是以多个任务中耗时最高的时长为准!!!借用这个机制,可以在我们业务逻辑编写时做一些性能优化。详情参见java性能优化

Q: get(long, TimeUnit) 方法下,主线程不会一直等待,但是子线程的任务仍旧会执行结束,那么在超时后,我们不想继续执行这个任务,以节省资源了,怎么办?

A: cancel(boolean)方法

请添加图片描述

当我们等待超时后,捕捉到异常,可以用cancel()方法取消子任务,最后调用isCancelled()方法发现确实被取消了;

Q: 如果我不想在获取的时候根据有没有异常来判断有没有执行结束,就像稳一手,先判断有没有执行结束,然后在获取结果怎么办?

A: IsDoned()方法

请添加图片描述

如上图所示,我们主线程休眠1秒后,我们通过isDone()方法手动判断子任务是否执行结束,如果执行结束了就打印,没有执行结束就取消任务;

此方式与get(long, TimeUnit)方法都是避免主线程一直等待,区别在于,此方式可以优雅的判断任务是否执行结束,没结束可以做取消任务处理;get(long, TimeUnit)不论任务是否执行结束,都会执行获取方法,如果没有执行结束,则抛出异常

线程池

线程池的7个核心参数

     public ThreadPoolExecutor(int corePoolSize, // 核心线程数
                              int maximumPoolSize, // 最大线程数
                              long keepAliveTime, // 空闲线程的存活时间
                              TimeUnit unit, // 时间单位
                              BlockingQueue<Runnable> workQueue, // 线程工作队列
                              ThreadFactory threadFactory, // 创建线程的工厂
                              RejectedExecutionHandler handler // 拒绝策略) {...}

参数及工作流程详解:

  1. corePoolSize

    核心线程数,当线程池初始化后,如果当前有任务提交,并且目前线程池中的线程数(包括空闲线程)小于核心线程数,则会新建一条核心线程执行任务;任务执行完成后,就算当前线程处于空闲状态,也不会被销毁,除非设置了allowCoreThreadTimeOut=true;

  2. maximumPoolSize

    最大线程数,线程池的最大容量,线程池中的线程数量不得超过这么多个;

  3. keepAliveTime

    空闲线程的存活时间,如果当前线程池中的线程数超过corePoolSize,那么超出的线程在存活时间之后就会被自动销毁;

  4. unit

    空闲时间的单位;

  5. workQueue

    队列,线程池中的线程数超过corePoolSize后,新的任务会进入到这个工作队列中。目前有四种队列类型:

    5.1 ArrayBlockingQueue

    ​ 基于数组的有界队列,遵循FIFO原则,当线程池中的线程数超过了corePoolSize,此时有新任务进来,会添加到队列的尾部等待 被调度,如果此时队列的容量已经满了,则会创建一个新的线程,如果线程数超过了maximumPoolSize,则会执行拒绝策略。 此方式可以控制线程数以及任务书,进而有效的控制资源利用

    5.2 LinkedBlockingQuene

    ​ 基于链表的无边界队列,遵循FIFO原则,当线程池中的线程数超过了corePoolSize,再有新任务加入时,也会先加入到这个队列 中。但是,由于队列时近似无边界的(实际容量为Interger.MAX),所以任务几乎都会一直存在这个队列中,不会再去创建新的 线程,所以maximumPoolSize参数不会起作用;

    5.3 SynchronousQuene

    ​ 同步队列(不缓存任务的队列),为什么叫不缓存,因为当线程池中线程数超过了corePoolSize,并且有新任务加进来时,不会 堆积在队列里等待线程调度,而是直接新建线程。或者说,当创建了一个任务进入队列时,对应的会立即新建一个线程来执行这 个任务。如果线程数大于maximumPoolSize,则执行拒绝策略;

    5.4 PriorityBlockingQueue

    ​ 无界的基于数组的优先级阻塞队列,数组的默认长度是11,虽然指定了数组的长度,但是可以无限的扩充,直到资源消耗尽为止,每次出队都返回优先级别最高的或者最低的元素;

  6. handler

    拒绝策略:当线程池中的队列任务书已满,并且线程池的数量达到了maximumPoolSize,则会触发拒绝策略;

    6.1 CallerRunsPolicy

    ​ 该策略下,会将run()方法的执行主体转移给调用方,换句话说,那个线程递交任务给线程池,该拒绝策略下就会把这个任务回退 给对应线程执行(除非线程池shutdown);

    6.2 AbortPolicy(默认拒绝策略)

    ​ 该策略下,直接丢弃任务,抛出RejectedExecutionException异常;

    6.3 DiscardPolicy

    ​ 该策略下,直接丢弃任务,不做任何处理;

    6.4 DiscardOldestPolicy

    ​ 该策略下,会抛弃进入队列最早的那一个任务,并尝试将该被拒绝的任务加入队列中;

线程池工作流程:

假设当前设置核心线程数为X, 最大线程数为Y,,队列容量为Z,任务数量为M,线程池初始化后:

  1. 当有新任务加入时,首先会先创建新线程执行任务,直到线程数达到了核心线程数X------ M<=X;
  2. 当线程池中的线程数达到核心线程数,此时再有任务进来,会优先加入到队列中------------X<M<=Z;
  3. 当队列中存在的任务数量达到了容量Z,此时如果有新任务提交,则会创建普通线程--------Z<M<= Y;
  4. 当队列中的线程数已经达到了最大线程数,此时有新任务提交,触发拒绝策略----------------M>Y;

线程池的四种创建方式

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

此种方式下创建的线程,核心线程数为0,普通线程数近乎无限大;这类线程处理任务时,如果当前线程池的大小超出了任务处理需求,则会优先使用当前已存在的线程,不会额外新建线程;换句话说,线程池初始化后,如果有任务提交,会新建一个线程处理任务。后续又有其他任务提交时,会优先用原先已存在的线程执行任务,而不是优先新建线程;

请添加图片描述

上述代码中,我们循环向线程池中提交任务,但是每次提交前都确保上一个任务已经执行结束,在这个前提下,线程池不会新建线程,而是一直用已经存在的那个线程,所以多个任务打印的都是同一个线程名称;

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

此种方式下会根据参数创建一个固定大小的线程池,超过核心线程数的任务会全部放到队列中。而这边用到的是LinkedBlockingQueue,所以最大线程数实际不起作用,因为队列是无界的,只要线程数超过了核心线程数,此时新加任务都是放到队列中,而队列又是无界的,所以不会再与核心线程数做比较;

请添加图片描述

上述代码中,我们创建线程池时指定核心线程数量为2,有多个任务提交时,超出核心线程数的任务会存入到队列中,并最终由两个核心线程依次处理;

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

此种方式下会创建一个固定核心线程数的,可以定时执行任务的线程池;

提交任务的几种方式:
请添加图片描述

  1. schedule()方法提交任务
schedule(Runnable command,long delay,TimeUnit unit){...}
该方法下,会在指定的延迟时间后执行任务;

请添加图片描述

  1. scheduleAtFixedRate()方法提交任务

    scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit){...}
    该方法下,会在给定的延时后,每个一段时间执行一次任务;
    

请添加图片描述

上述代码可以看到,首次执行任务时,延迟了3s,后续每次执行都间隔1s;

  1. scheduleWithFixedDelay()方法提交任务

    scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)
    该方法下,也会在一段延迟后,按照固定的间隔循环执行任务;
    

请添加图片描述


注意此处的时间,这也是与scheduleAtFixedRate区别点:假设任务执行耗时需要4s,设置间隔时间2s

scheduleAtFixedRate:首次执行任务后,后续任务的执行时间是4s后,换句话说,下次执行任务的实际间隔取任务耗时与间隔时间的较大值;

scheduleWithFixedDelay:首次执行任务后,后续人物的执行时间是6s,换句话说,执行下一个任务的前提是:上一个任务完全执行结束,并且再等待固定的间隔,下次执行任务的实际间隔时间,为任务耗时+设置的间隔;

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

此种方式会创建一个唯一线程的线程池,多个任务提交到队列后,能保证多个任务的顺序执行;

请添加图片描述
上述代码可见,一个线程执行多个任务,并且任务的返回值也是按照任务的提交顺序返回的;

execute()与submit()方法

// Executor是提供了一个无返回值execute方法
public interface Executor {
    void execute(Runnable command);
}

// ExecutorService 继承了Executor,并且自定义了有返回值的sumbit方法
public interface ExecutorService extends Executor {
    <T> Future<T> submit(Callable<T> task);
	<T> Future<T> submit(Runnable task, T result);
	Future<?> submit(Runnable task);
}

execute()与submit()方法

// Executor是提供了一个无返回值execute方法
public interface Executor {
    void execute(Runnable command);
}

// ExecutorService 继承了Executor,并且自定义了有返回值的sumbit方法
public interface ExecutorService extends Executor {
    <T> Future<T> submit(Callable<T> task);
	<T> Future<T> submit(Runnable task, T result);
	Future<?> submit(Runnable task);
}

submit()方法

// submit方法
public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
}

// newTaskFor方法
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
}

由源码可见,submit()先根据传入的Callable接口的实现类创建了一个FutureTask对象,最终还是调用了execute()方法,并且返回了FututeTask对象,我们也是以此来拿到线程的返回值结果;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值