xiaoWang3486

博客园 首页 新随笔 联系 订阅 管理
												 <font size=25>线程</font>

进程和线程

进程是系统正在运行的程序,有独立的地址空间。

一个进程可以有多个线程,每个线程使用所属进程的占空间,

java中有两种线程

​ 1、守护线程,比如垃圾回收线程,就是最典型的守护线程。

​ 2、用户线程,就是应用程序里的自定义线程

并发和并行

并发是指一个处理器同时处理多个任务。
并行是指多个处理器或者是多核的处理器同时处理多个不同的任务,并行时也会发生并发。
并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。

开启线程用start()

Java 中实现真正的多线程是 start 中的 start0() 方法,run() 方法只是一个普通的方法。

start() 方法调用 start0() 方法后,该线程并不一定会立马执行,只是将线程变成了可运行状态(NEW ---> RUNNABLE)。具体什么时候执行,取决于 CPU ,由 CPU 统一调度。

创建线程

1.继承Thread类

public class Thread02 {
    public static void main(String[] args) {
//        创建线程对象
        CreateThread01 createThread01 = new CreateThread01();
//        开启线程
        createThread01.start();
    }
}
class CreateThread01 extends Thread{
//    重写父类方法
    @Override
    public void run() {
        System.out.println("线程创建了");
    }
}

2.实现Runnable接口

public class RunnableMethod {
    public static void main(String[] args) {
//        创建实现Runnable接口的类对象
        CreateThread02 createThread02 = new CreateThread02();
//        创建线程对象
        new Thread(createThread02).start();
    }
}
class CreateThread02 implements Runnable{
    @Override
    public void run() {
        System.out.println("线程创建了");
    }
}

3.实现Collable接口

/**
 * 创建实现Callable接口的线程对象重写call方法
 */
class Thread01 implements Callable{
    @Override
    public Object call() throws Exception {
        System.out.println("实现Callable()接口的线程执行了!!");
        return "我有返回值!!";
    }
}

线程的方法

public class Thread03 {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
//        setName()
        thread.setName("新名字设置成功");
//        getName()
        System.out.println(thread.getName());
//        serProperty()
        thread.setPriority(10);
//        getProperty
        System.out.println(thread.getPriority());
//        interrupt()
        Thread thread1 = new Thread(new InThread03());
        thread1.start();
        thread1.interrupt();
    }
}

1.sleep( ) 使线程在一定的时间内进入阻塞状态,不能得到cpu时间,但不会释放锁资源。指定的时间一过,线程重新进入可执行状态

2.wait( ) 使线程进入阻塞状态,同时释放自己占有的锁资源,和notify( )搭配使用,属于Object类的方法

3.suspend( ) 使线程进入阻塞状态,并且不会自动恢复,必须其对应的resume( )被调用,才能使线程重新进入可执行状态

4.yield( )使得线程放弃当前分得的CPU时间,但是*不使线程阻塞*,即线程任然处于可执行状态,随时可能再次分得CPU时间。

5.join()实际工程中很多场景都会涉及某个线程需要依赖另外一个或几个线程的执行结果,这就要被依赖的线程需要先执行完,这时就需要join操作。在当前线程中,插入其他线程,等待其他线程执行结束之后,继续执行当前线程。

6.其中sleep(),suspend(),rusume(),yield()均为Thread类的方法,wait()为Object类的方法

7.守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程,守护线程,专门用于服务其他的线程,如果其他的线程(即用户自定义线程)都执行完毕,连main线程也执行完毕,那么jvm就会退出(即停止运行)——此时,连jvm都停止运行了,守护线程当然也就停止执行了。设置守护线程时,要在线程开启之前设置

Thread thread1 = new Thread(new CreateThread02());
thread1.setDaemon(true);
thread1.start();

线程的状态

线程的六种状态:

new : 创建线程对象 new Thread()

runnabled : 调用thread.start()方法进入runnabled状态

blocked : 阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了 CPU 使用权

​ 等待阻塞:运行的线程执行了 Thread.sleep 、wait()、 join() 等方法JVM 会把当前线程设置为等待状态,当 sleep 结束、join 线程终 止或者线程被唤醒后,该线程从等待状态进入到阻塞状态,重新抢占锁后进行线程恢复;

​ 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么jvm会把当前的线程放入到锁池中 ;

​ 其他阻塞:发出了 I/O请求时,JVM 会把当前线程设置为阻塞状态,当 I/O处理完毕则线程恢复

waitting ;等待状态,没有超时时间,要被其他线程或者有其它的中断操作;执行wait()、join()、LockSupport.park();

time_waitting :超时等待状态,超时以后自动返回;执行 Thread.sleep(long)、wait(long)、join(long)、LockSupport.park(long)、LockSupport.parkNanos(long)、LockSupport.parkUntil(long)

terminated:超时等待状态,超时以后自动返回;

img

互斥锁

什么是互斥锁

​ 互斥锁实际是一种变量,通过置0置1来判断是的线程可以获得和释放锁。

互斥锁的作用

​ 互斥锁可以对临界区进行保护,保证临界区的代码在同一时刻只能被一个线程访问。

什么是互斥和同步

​ 互斥是指:在某一时刻允许程序的某一个程序片,具有排他性和唯一性。就是当线程a对临界资源进行访问时,线程b只能在临界区等待

​ 同步是指:在互斥的基础上,实现进程之间的有序访问。只有当A写完数据(或B取走数据),B才能来读数据(或A才能往里写数据)。这种关系就是一种线程的同步关系。

什么是临界区和临界资源

​ 临界资源:能够被多个线程共享的数据/资源。

​ 临界区:对临界资源进行操作的那一段代码

死锁

什么是死锁

​ 死锁是指进程中的各个线程都不释放已占有的资源,同时依赖其他线程占有的资源处于的一种永久等待状态。

死锁产生的条件

​ 1.互斥属性:每次只能有一个线程访问资源

​ 2.不可剥夺:线程获得到的资源,在没有主动释放时,不可被剥夺。

​ 3.请求和保持:已经申请到资源的线程,继续申请,即抱锁找锁。

​ 4.循环等待:多个线程形成环路,都在等待相邻线程的资源

释放锁

​ 执行完同步代码块,就会释放锁。(synchronized)
​ 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。(exception)
​ 在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放锁,进
​ 入对象的等待池。(wait)

除了以上情况以外,只要持有锁的线程还没有执行完同步代码块,就不会释放锁。
在下面情况下,线程是不会释放锁的:
(1)执行同步代码块的过程中,执行了Thread.sleep()方法,当前线程放弃CPU,开始睡眠,进入堵塞状态,在睡眠中不会释放锁。
(2)在执行同步代码块的过程中,执行了Thread.yield()方法,当前线程放弃CPU,回到就绪状态,但不会释放锁。**

线程池

什么是线程池

线程池时一种利用池化技术,将线程放在吃中进行管理,复用,将线程的创建,执行和销毁解耦开来。

在java中提供了ThreadPoolExecutor创建线程池技术,JDK中提供Executor来创建线程

线程池的优点:

降低线程的创建,执行,销毁对资源的消耗。

提高响应速度,当任务到达时,不用等待线程的创建直接执行。

提高线程的可管理性,使用线程池能够统一的分配、调优和监控。

线程的七个参数:

核心线程,最大线程数,最大空闲时间,阻塞队列,时间单位,线程工厂,拒绝策略

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {}

执行流程:

image-20230411171201650

线程池的状态:

STOP( 0 停止接收,停止处理队列,停止当前线程)

RUNNING(  -1:正常接收任务)TIDYING( 2 过度状态,当前线程池即将结束) TERMINATED( 3 线程池结束)

SHUTDOWN( 1 停止接收,继续处理阻塞队列)

博客

1 线程池的使用方法

一般我们最常用的线程池实现类是ThreadPoolExecutor,我们接下来会介绍这个类的基本使用方法。JDK已经对线程池做了比较好的封装,相信这个过程会非常轻松。

*1.1 创建线程池*

既然线程池是一个Java类,那么最直接的使用方法一定是new一个ThreadPoolExecutor类的对象,例如ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>() )。那么这个构造器的里每个参数是什么意思呢?

下面就是这个构造器的方法签名:

public ThreadPoolExecutor(int corePoolSize,



                              int maximumPoolSize,



                              long keepAliveTime,



                              TimeUnit unit,



                              BlockingQueue<Runnable> workQueue)

各个参数分别表示下面的含义:

  1. corePoolSize,核心线程池大小,一般线程池会至少保持这么多的线程数量;
  2. maximumPoolSize,最大线程池大小,也就是线程池最大的线程数量;
  3. keepAliveTime和unit共同组成了一个超时间,keepAliveTime是时间数量,unit是时间单位,单位加数量组成了最终的超时时间。这个超时时间表示如果线程池中包含了超过corePoolSize数量的线程,则在有线程空闲的时间超过了超时时间时该线程就会被销毁;
  4. workQueue是任务的阻塞队列,在没有线程池中没有足够的线程可用的情况下会将任务先放入到这个阻塞队列中等待执行。这里传入的队列类型就决定了线程池在处理这些任务时的策略。

线程池中的阻塞队列专门用于存放待执行的任务,在ThreadPoolExecutor中一个任务可以通过两种方式被执行:第一种是直接在创建一个新的Worker时被作为第一个任务传入,由这个新创建的线程来执行;第二种就是把任务放入一个阻塞队列,等待线程池中的工作线程捞取任务进行执行。

上面提到的阻塞队列是这样的一种数据结构,它是一个队列(类似于一个List),可以存放0到N个元素。我们可以对这个队列进行插入和弹出元素的操作,弹出操作可以理解为是一个获取并从队列中删除一个元素的操作。当队列中没有元素时,对这个队列的获取操作将会被阻塞,直到有元素被插入时才会被唤醒;当队列已满时,对这个队列的插入操作将会被阻塞,直到有元素被弹出后才会被唤醒。这样的一种数据结构非常适合于线程池的场景,当一个工作线程没有任务可处理时就会进入阻塞状态,直到有新任务提交后才被唤醒。

*1.2 提交任务*

当创建了一个线程池之后我们就可以将任务提交到线程池中执行了。提交任务到线程池中相当简单,我们只要把原来传入Thread类构造器的Runnable对象传入线程池的execute方法或者submit方法就可以了。execute方法和submit方法基本没有区别,两者的区别只是submit方法会返回一个Future对象,用于检查异步任务的执行情况和获取执行结果(异步任务完成后)。

我们可以先试试如何使用比较简单的execute方法,代码例子如下:

public class ThreadPoolTest {



 



    private static int count = 0;



 



    public static void main(String[] args) throws Exception {



        Runnable task = new Runnable() {



            public void run() {



                for (int i = 0; i < 1000000; ++i) {



                    synchronized (ThreadPoolTest.class) {



                        count += 1;



                    }



                }



            }



        };



 



        // 重要:创建线程池



        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 0L,



        TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());



 



        // 重要:向线程池提交两个任务



        threadPool.execute(task);



        threadPool.execute(task);



 



        // 等待线程池中的所有任务完成



        threadPool.shutdown();



        while (!threadPool.awaitTermination(1L, TimeUnit.MINUTES)) {



            System.out.println("Not yet. Still waiting for termination");



        }



 



        System.out.println("count = " + count);



    }



}

1.3 关闭线程池

上面的代码中为了等待线程池中的所有任务执行完已经使用了shutdown()方法,关闭线程池的方法主要有两个:

  1. shutdown(),有序关闭线程池,调用后线程池会让已经提交的任务完成执行,但是不会再接受新任务。
  2. shutdownNow(),直接关闭线程池,线程池中正在运行的任务会被中断,正在等待执行的任务不会再被执行,但是这些还在阻塞队列中等待的任务会被作为返回值返回。

1.4 监控线程池运行状态

我们可以通过调用线程池对象上的一些方法来获取线程池当前的运行信息,常用的方法有:

  • getTaskCount,线程池中已完成、执行中、等待执行的任务总数估计值。因为在统计过程中任务会发生动态变化,所以最后的结果并不是一个准确值;
  • getCompletedTaskCount,线程池中已完成的任务总数,这同样是一个估计值;
  • getLargestPoolSize,线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否充满过,也就是达到过maximumPoolSize;
  • getPoolSize,线程池当前的线程数量;
  • getActiveCount,当前线程池中正在执行任务的线程数量估计值。

2 四种常用线程池

很多情况下我们也不会直接创建ThreadPoolExecutor类的对象,而是根据需要通过Executors的几个静态方法来创建特定用途的线程池。目前常用的线程池有四种:

  1. 可缓存线程池,使用Executors.newCachedThreadPool方法创建
  2. 定长线程池,使用Executors.newFixedThreadPool方法创建
  3. 延时任务线程池,使用Executors.newScheduledThreadPool方法创建
  4. 单线程线程池,使用Executors.newSingleThreadExecutor方法创建

下面通过这些静态方法的源码来具体了解一下不同类型线程池的特性与适用场景。

2.1 可缓存线程池

JDK中的源码我们通过在IDE中进行跳转可以很方便地进行查看,下面就是Executors.newCachedThreadPool方法中的源代码。从代码中我们可以看到,可缓存线程池其实也是通过直接创建ThreadPoolExecutor类的构造器创建的,只是其中的参数都已经被设置好了,我们可以不用做具体的设置。所以我们要观察的重点就是在这个方法中具体产生了一个怎样配置的ThreadPoolExecutor对象,以及这样的线程池适用于怎样的场景。

从下面的代码中,我们可以看到,传入ThreadPoolExecutor构造器的值有:

  • corePoolSize核心线程数为0,代表线程池中的线程数可以为0
  • maximumPoolSize最大线程数为Integer.MAX_VALUE,代表线程池中最多可以有无限多个线程
  • 超时时间设置为60秒,表示线程池中的线程在空闲60秒后会被回收
  • 最后传入的是一个SynchronousQueue类型的阻塞队列,代表每一个新添加的任务都要马上有一个工作线程进行处理
public static ExecutorService newCachedThreadPool() {



    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,



                                  60L, TimeUnit.SECONDS,



                                  new SynchronousQueue<Runnable>());



}

所以可缓存线程池在添加任务时会优先使用空闲的线程,如果没有就创建一个新线程,线程数没有上限,所以每一个任务都会马上被分配到一个工作线程进行执行,不需要在阻塞队列中等待;如果线程池长期闲置,那么其中的所有线程都会被销毁,节约系统资源。

  • 优点
    • 任务在添加后可以马上执行,不需要进入阻塞队列等待
    • 在闲置时不会保留线程,可以节约系统资源
  • 缺点
    • 对线程数没有限制,可能会过量消耗系统资源
  • 适用场景
    • 适用于大量短耗时任务和对响应时间要求较高的场景

2.2 定长线程池

传入ThreadPoolExecutor构造器的值有:

  • corePoolSize核心线程数和maximumPoolSize最大线程数都为固定值nThreads,即线程池中的线程数量会保持在nThreads,所以被称为“定长线程池”
  • 超时时间被设置为0毫秒,因为线程池中只有核心线程,所以不需要考虑超时释放
  • 最后一个参数使用了无界队列,所以在所有线程都在处理任务的情况下,可以无限添加任务到阻塞队列中等待执行
public static ExecutorService newFixedThreadPool(int nThreads) {



    return new ThreadPoolExecutor(nThreads, nThreads,



                                  0L, TimeUnit.MILLISECONDS,



                                  new LinkedBlockingQueue<Runnable>());



}

定长线程池中的线程数会逐步增长到nThreads个,并且在之后空闲线程不会被释放,线程数会一直保持在nThreads个。如果添加任务时所有线程都处于忙碌状态,那么就会把任务添加到阻塞队列中等待执行,阻塞队列中任务的总数没有上限。

  • 优点
    • 线程数固定,对系统资源的消耗可控
  • 缺点
    • 在任务量暴增的情况下线程池不会弹性增长,会导致任务完成时间延迟
    • 使用了无界队列,在线程数设置过小的情况下可能会导致过多的任务积压,引起任务完成时间过晚和资源被过度消耗的问题
  • 适用场景
    • 任务量峰值不会过高,且任务对响应时间要求不高的场景

2.3 延时任务线程池

与之前的两个方法不同,Executors.newScheduledThreadPool返回的是ScheduledExecutorService接口对象,可以提供延时执行、定时执行等功能。在线程池配置上有如下特点:

  • maximumPoolSize最大线程数为无限,在任务量较大时可以创建大量新线程执行任务
  • 超时时间为0,线程空闲后会被立即销毁
  • 使用了延时工作队列,延时工作队列中的元素都有对应的过期时间,只有过期的元素才会被弹出
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {



    return new ScheduledThreadPoolExecutor(corePoolSize);



}



 



public ScheduledThreadPoolExecutor(int corePoolSize) {



    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,



          new DelayedWorkQueue());



}

延时任务线程池实现了ScheduledExecutorService接口,主要用于需要延时执行和定时执行的情况。

2.4 单线程线程池

单线程线程池中只有一个工作线程,可以保证添加的任务都以指定顺序执行(先进先出、后进先出、优先级)。但是如果线程池里只有一个线程,为什么我们还要用线程池而不直接用Thread呢?这种情况下主要有两种优点:一是我们可以通过共享的线程池很方便地提交任务进行异步执行,而不用自己管理线程的生命周期;二是我们可以使用任务队列并指定任务的执行顺序,很容易做到任务管理的功能。

public static ExecutorService newSingleThreadExecutor() {



    return new FinalizableDelegatedExecutorService



        (new ThreadPoolExecutor(1, 1,



                                0L, TimeUnit.MILLISECONDS,



                                new LinkedBlockingQueue<Runnable>()));



}

3 线程池的内部实现

通过前面的内容我们其实已经可以在代码中使用线程池了,但是我们为什么还要去深究线程池的内部实现呢?首先,可能有一个很功利性的目的就是为了面试,在面试时如果能准确地说出一些底层的运行机制与原理那一定可以成为过程中一个重要的亮点。

但是我认为学习探究线程池的内部实现的作用绝对不仅是如此,只有深入了解并厘清了线程池的具体实现,我们才能解决实践中需要考虑的各种边界条件。因为多线程编程所代表的并发编程并不是一个固定的知识点,而是实践中不断在发展和完善的一个知识门类。我们也许会需要同时考虑多个维度,最后得到一个特定于应用场景的解决方案,这就要求我们具备从细节着手构建出解决方案并做好各个考虑维度之间的取舍的能力。

而且我相信只要在某一个点上能突破到相当的深度,那么以后从这个点上向外扩展就会容易得多。也许在刚开始我们的探究会碰到非常大的阻力,但是我们要相信,最后我们可以得到的将不止是一个知识点而是一整个知识面。

3.1 查看JDK源码的方式

在IDE中,例如IDEA里,我们可以点击我们样例代码里的ThreadPoolExecutor类跳转到JDK中ThreadPoolExecutor类的源代码。在源代码中我们可以看到很多java.util.concurrent包的缔造者大牛“Doug Lea”所留下的各种注释,下面的图片就是该类源代码的一个截图。

893fb8edb96d6ec6b33cee7cf1cf48f8.png

这些注释的内容非常有参考价值,建议有能力的读者朋友可以自己阅读一遍。下面,我们就一步步地抽丝剥茧,来揭开线程池类ThreadPoolExecutor源代码的神秘面纱。

3.2 控制变量与线程池生命周期

ThreadPoolExecutor类定义的开头,我们可以看到如下的几行代码:

// 控制变量,前3位表示状态,剩下的数据位表示有效的线程数



private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));



// Integer的位数减去3位状态位就是线程数的位数



private static final int COUNT_BITS = Integer.SIZE - 3;



// CAPACITY就是线程数的上限(含),即2^COUNT_BITS - 1个



private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

第一行是一个用来作为控制变量的整型值,即一个Integer。之所以要用AtomicInteger类是因为要保证多线程安全,在本系列之后的文章中会对AtomicInteger进行具体介绍。一个整型一般是32位,但是这里的代码为了保险起见,还是使用了Integer.SIZE来表示整型的总位数。这里的“位”指的是数据位(bit),在计算机中,8bit = 1字节,1024字节 = 1KB,1024KB = 1MB。每一位都是一个0或1的数字,我们如果把整型想象成一个二进制(0或1)的数组,那么一个Integer就是32个数字的数组。其中,前三个被用来表示状态,那么我们就可以表示2^3 = 8个不同的状态了。剩下的29位二进制数字都会被用于表示当前线程池中有效线程的数量,上限就是(2^29 - 1)个,即常量CAPACITY

之后的部分列出了线程池的所有状态:

private static final int RUNNING    = -1 << COUNT_BITS;



private static final int SHUTDOWN   =  0 << COUNT_BITS;



private static final int STOP       =  1 << COUNT_BITS;



private static final int TIDYING    =  2 << COUNT_BITS;



private static final int TERMINATED =  3 << COUNT_BITS;

在这里可以忽略数字后面的<< COUNT_BITS,可以把状态简单地理解为前面的数字部分,这样的简化基本不影响结论。

各个状态的解释如下:

  • RUNNING,正常运行状态,可以接受新的任务和处理队列中的任务
  • SHUTDOWN,关闭中状态,不能接受新任务,但是可以处理队列中的任务
  • STOP,停止中状态,不能接受新任务,也不处理队列中的任务,会中断进行中的任务
  • TIDYING,待结束状态,所有任务已经结束,线程数归0,进入TIDYING状态后将会运行terminated()方法
  • TERMINATED,结束状态,terminated()方法调用完成后进入

这几个状态所对应的数字值是按照顺序排列的,也就是说线程池的状态只能从小到大变化,这也方便了通过数字比较来判断状态所在的阶段,这种通过数字大小来比较状态值的方法在ThreadPoolExecutor的源码中会有大量的使用。

下图是这五个状态之间的变化过程:

f569c8cca0ff508fed6686e8aca8cf5b.png

  1. 当线程池被创建时会处于RUNNING状态,正常接受和处理任务;
  2. shutdown()方法被直接调用,或者在线程池对象被GC回收时通过finalize()方法隐式调用了shutdown()方法时,线程池会进入SHUTDOWN状态。该状态下线程池仍然会继续执行完阻塞队列中的任务,只是不再接受新的任务了。当队列中的任务被执行完后,线程池中的线程也会被回收。当队列和线程都被清空后,线程池将进入TIDYING状态;
  3. 在线程池处于RUNNING或者SHUTDOWN状态时,如果有代码调用了shutdownNow()方法,则线程池会进入STOP状态。在STOP状态下,线程池会直接清空阻塞队列中待执行的任务,然后中断所有正在进行中的任务并回收线程。当线程都被清空以后,线程池就会进入TIDYING状态;
  4. 当线程池进入TIDYING状态时,将会运行terminated()方法,该方法执行完后,线程池就会进入最终的TERMINATED状态,彻底结束。

到这里我们就已经清楚地了解了线程从刚被创建时的RUNNING状态一直到最终的TERMINATED状态的整个生命周期了。那么当我们要向一个RUNNING状态的线程池提交任务时会发生些什么呢?

*3.3 execute方法的实现*

我们一般会使用execute方法提交我们的任务,那么线程池在这个过程中做了什么呢?在ThreadPoolExecutor类的execute()方法的源代码中,我们主要做了四件事:

  1. 如果当前线程池中的线程数小于核心线程数corePoolSize,则创建一个新的Worker代表一个线程,并把入参中的任务作为第一个任务传入Worker。addWorker方法中的第一个参数是该线程的第一个任务,而第二个参数就是代表是否创建的是核心线程,在execute方法中addWorker总共被调用了三次,其中第一次传入的是true,后两次传入的都是false;
  2. 如果当前线程池中的线程数已经满足了核心线程数corePoolSize,那么就会通过workQueue.offer()方法将任务添加到阻塞队列中等待执行;
  3. 如果线程数已经达到了corePoolSize且阻塞队列中无法插入该任务(比如已满),那么线程池就会再增加一个线程来执行该任务,除非线程数已经达到了最大线程数maximumPoolSize;
  4. 如果确实已经达到了最大线程数,那么就拒绝这个任务。

总体上的执行流程如下,下方的黑色同心圆代表流程结束:

6ab9c2a703bcf56f05d252313e2c27c0.png

这里再重复一次阻塞队列的定义,方便大家阅读:

线程池中的阻塞队列专门用于存放待执行的任务,在 ThreadPoolExecutor中一个任务可以通过两种方式被执行:第一种是直接在创建一个新的Worker时被作为第一个任务传入,由这个新创建的线程来执行;第二种就是把任务放入一个阻塞队列,等待线程池中的工作线程捞取任务进行执行。
上面提到的 阻塞队列是这样的一种数据结构,它是一个队列(类似于一个List),可以存放0到N个元素。我们可以对这个队列进行插入和弹出元素的操作,弹出操作可以理解为是一个获取并从队列中删除一个元素的操作。当队列中没有元素时,对这个队列的获取操作将会被阻塞,直到有元素被插入时才会被唤醒;当队列已满时,对这个队列的插入操作将会被阻塞,直到有元素被弹出后才会被唤醒。这样的一种数据结构非常适合于线程池的场景,当一个工作线程没有任务可处理时就会进入阻塞状态,直到有新任务提交后才被唤醒。

下面是带有注释的源代码,大家可以和上面的流程对照起来参考一下:

public void execute(Runnable command) {



    // 检查提交的任务是否为空



    if (command == null)



        throw new NullPointerException();



 



    // 获取控制变量值



    int c = ctl.get();



    // 检查当前线程数是否达到了核心线程数



    if (workerCountOf(c) < corePoolSize) {



        // 未达到核心线程数,则创建新线程



        // 并将传入的任务作为该线程的第一个任务



        if (addWorker(command, true))



            // 添加线程成功则直接返回,否则继续执行



            return;



 



        // 因为前面调用了耗时操作addWorker方法



        // 所以线程池状态有可能发生了改变,重新获取状态值



        c = ctl.get();



    }



 



    // 判断线程池当前状态是否是运行中



    // 如果是则调用workQueue.offer方法将任务放入阻塞队列



    if (isRunning(c) && workQueue.offer(command)) {



        // 因为执行了耗时操作“放入阻塞队列”,所以重新获取状态值



        int recheck = ctl.get();



        // 如果当前状态不是运行中,则将刚才放入阻塞队列的任务拿出,如果拿出成功,则直接拒绝这个任务



        if (! isRunning(recheck) && remove(command))



            reject(command);



        else if (workerCountOf(recheck) == 0)



            // 如果线程池中没有线程了,那就创建一个



            addWorker(null, false);



    }



    // 如果放入阻塞队列失败(如队列已满),则添加一个线程



    else if (!addWorker(command, false))



        // 如果添加线程失败(如已经达到了最大线程数),则拒绝任务



        reject(command);



}

3.4 addWorker方法

在前面execute方法的代码中我们可以看到线程池是通过addWorker方法来向线程池中添加新线程的,那么新的线程又是如何运行起来的呢?

这里我们暂时跳过addWorker方法的详细源代码,因为虽然这个方法的代码行数较多,但是功能相对比较直接,只是创建一个代表线程的Worker类对象,并调用这个对象所对应线程对象的start()方法。我们知道一旦调用了Thread类的start()方法,则这个线程就会开始调用创建线程时传入的Runnable对象。从下面的Worker类构造器源代码可以看出,Worker类正是把自己(this指针)传入了线程的构造器当中,那么这个线程就会运行Worker类的run()方法了,这个run()方法只执行了一行很简单的代码runWorker(this)

Worker(Runnable firstTask) {



    setState(-1); // inhibit interrupts until runWorker



    this.firstTask = firstTask;



    this.thread = getThreadFactory().newThread(this);



}



 



public void run() {



    runWorker(this);



}

3.5 runWorker方法的实现

我们看到线程池中的线程在启动时会调用对应的Worker类的runWorker方法,而这里就是整个线程池任务执行的核心所在了。runWorker方法中包含有一个类似无限循环的while语句,让worker对象可以不断执行提交到线程池中的新任务。

大家可以配合代码上带有的注释来理解该方法的具体实现:

final void runWorker(Worker w) {



    Thread wt = Thread.currentThread();



    Runnable task = w.firstTask;



    w.firstTask = null;



    // 将worker的状态重置为正常状态,因为state状态值在构造器中被初始化为-1



    w.unlock();



    // 通过completedAbruptly变量的值判断任务是否正常执行完成



    boolean completedAbruptly = true;



    try {



        // 如果task为null就通过getTask方法获取阻塞队列中的下一个任务



        // getTask方法一般不会返回null,所以这个while类似于一个无限循环



        // worker对象就通过这个方法的持续运行来不断处理新的任务



        while (task != null || (task = getTask()) != null) {



            // 每一次任务的执行都必须获取锁来保证下方临界区代码的线程安全



            w.lock();



 



            // 如果状态值大于等于STOP(状态值是有序的,即STOP、TIDYING、TERMINATED)



            // 且当前线程还没有被中断,则主动中断线程



            if ((runStateAtLeast(ctl.get(), STOP) ||



                 (Thread.interrupted() &&



                  runStateAtLeast(ctl.get(), STOP))) &&



                !wt.isInterrupted())



                wt.interrupt();



 



            // 开始



            try {



                // 执行任务前处理操作,默认是一个空实现



                // 在子类中可以通过重写来改变任务执行前的处理行为



                beforeExecute(wt, task);



 



                // 通过thrown变量保存任务执行过程中抛出的异常



                // 提供给下面finally块中的afterExecute方法使用



                Throwable thrown = null;



                try {



                    // *** 重要:实际执行任务的代码



                    task.run();



                } catch (RuntimeException x) {



                    thrown = x; throw x;



                } catch (Error x) {



                    thrown = x; throw x;



                } catch (Throwable x) {



                    // 因为Runnable接口的run方法中不能抛出Throwable对象



                    // 所以要包装成Error对象抛出



                    thrown = x; throw new Error(x);



                } finally {



                    // 执行任务后处理操作,默认是一个空实现



                    // 在子类中可以通过重写来改变任务执行后的处理行为



                    afterExecute(task, thrown);



                }



            } finally {



                // 将循环变量task设置为null,表示已处理完成



                task = null;



                // 累加当前worker已经完成的任务数



                w.completedTasks++;



                // 释放while体中第一行获取的锁



                w.unlock();



            }



        }



 



        // 将completedAbruptly变量设置为false,表示任务正常处理完成



        completedAbruptly = false;



    } finally {



        // 销毁当前的worker对象,并完成一些诸如完成任务数量统计之类的辅助性工作



        // 在线程池当前状态小于STOP的情况下会创建一个新的worker来替换被销毁的worker



        processWorkerExit(w, completedAbruptly);



    }



}

runWorker方法的源代码中有两个比较重要的方法调用,一个是while条件中对getTask方法的调用,一个是在方法的最后对processWorkerExit方法的调用。下面是对这两个方法更详细的解释。

getTask方法在阻塞队列中有待执行的任务时会从队列中弹出一个任务并返回,如果阻塞队列为空,那么就会阻塞等待新的任务提交到队列中直到超时(在一些配置下会一直等待而不超时),如果在超时之前获取到了新的任务,那么就会将这个任务作为返回值返回。

getTask方法返回null时会导致当前Worker退出,当前线程被销毁。在以下情况下getTask方法才会返回null:

  1. 当前线程池中的线程数超过了最大线程数。这是因为运行时通过调用setMaximumPoolSize修改了最大线程数而导致的结果;
  2. 线程池处于STOP状态。这种情况下所有线程都应该被立即回收销毁;
  3. 线程池处于SHUTDOWN状态,且阻塞队列为空。这种情况下已经不会有新的任务被提交到阻塞队列中了,所以线程应该被销毁;
  4. 线程可以被超时回收的情况下等待新任务超时。线程被超时回收一般有以下两种情况:
  • 超出核心线程数部分的线程等待任务超时
  • 允许核心线程超时(线程池配置)的情况下线程等待任务超时

processWorkerExit方法会销毁当前线程对应的Worker对象,并执行一些累加总处理任务数等辅助操作。但在线程池当前状态小于STOP的情况下会创建一个新的Worker来替换被销毁的Worker,有兴趣的读者可以自行参考processWorkerExit方法源代码。

posted on 2023-09-25 14:33  xiaoWang3486  阅读(62)  评论(0)    收藏  举报