进程,线程,线程池(万字文)

目录

(一)进程

(1)进程是什么

(2)进程隔离

(3)进程通信(了解)

(二)线程

(1)线程是什么

1.1为什么要有线程

1.1.1更好的实现并发编程

1.1.2更快,更轻量

1.2线程与进程的关系

(3)线程的创建

(4)前台线程与后台线程

(5)线程调度

5.1调度由谁来执行

5.2调度方式

5.3优先级

5.4影响调用的方法

(6)Thread类

6.1构造方法

6.2常用属性及获取方法

6.3内部标志位

6.3.1标志位中断通知方式

6.3.2 使用Thread.currentThread().isInterrupted()为标志位的好处

(三)线程安全

(1)案例解析

(2)线程安全问题原因

(3)线程安全问题解决

3.1synchronized

3.1.1synchronized特性

3.1.2synchronized执行过程

3.1.3synchronized用的锁是存在Java对象头里的

3.1.4synchronized是可重入锁

 3.2volatile关键字

3.2.1案例解析

​编辑 3.2.2volatile作用

(四)线程间通信

 (1)wait与notify与noifyAll

1.1wait的作用

1.2wait的执行过程

1.3wait结束等待条件

1.4notify方法的作用

1.5wait 和notify方法要与synchronized搭配使用

1.6wait释放锁后不会再去主动竞争这个锁 

1.7notify与notifyAll区别

(五)方法区别

(1)start与run

(2)wait与sleep

(六)线程池

(1)什么是线程池

(2)线程池的目的

(3)线程池中的参数

(4)利用Executors创建线程池

4.1Executors帮我们配置好了大约四种线程池

4.2创建代码演示

4.2.1创建单线程化线程池

4.2.2创建固定线程数量线程池

4.2.3创建“可缓存线程池”

4.2.4创建定时任务线程池


(一)进程

(1)进程是什么

进程是计算机中程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位——>百度百科

通俗的说,正在运行的程序叫做进程,进程是操作系统分配资源的最小单位

(2)进程隔离

上面说到,进程是操作系统分配资源的最小单位,也就是说不同的进程使用内存中的不同区域,互相之间不会干扰,这便使进程具有了隔离性

(3)进程通信(了解)

但是现在的应用,只靠一个进程是无法完成复杂的任务的(比如实时共享文档),于是就有了进程间交换信息的需求,即进程间通信。

现在常用的进程间通信方式有:管道,消息队列,信号量,共享内存,网络,等....

(二)线程

(1)线程是什么

线程是操作系统能够进行运算调度的最小单位,被包含在进程中,是进程中的实际运作单位。

1.1为什么要有线程

1.1.1更好的实现并发编程

当单线程编程的时候,由于cpu运行效率比IO设备快的多,在线程等待IO时,cpu将处于空闲状态,为了充分的利用cup,提升效率,多线程编程成为刚需,当一个线程在等待IO的时候,可以让cpu执行另外一个线程,提升cpu运行效率。

1.1.2更快,更轻量
创建线程比创建进程更快,销毁线程比销毁进程更快,调度线程比调度进程更快,这也是使用线程进行并发编程的原因

1.2线程与进程的关系

一个进程包含至少一个线程(即至少包含一个主线程)

(3)线程的创建

下面我使用了五种方法创建线程对象,并使用的全代码

1.继承Thread类重写run方法

2.实现Runnable接口重写run方法

3.使用匿名内部类创建Thread子类重写run方法

4.使用匿名内部类创建Runnable子类重写run方法

5使用lambda表达式重写run方法(最简单)

public class Mythread extends Thread {//继承Thread类重写run方法
    @Override
    public void run() {

       while (true){

           try {
               Thread.sleep(1000);
               System.out.println("Mythread线程运行");
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
    }
}
class MyRunnable implements Runnable{//实现Runnable接口重写run方法

    @Override
    public void run() {
        while (true){

            try {
                Thread.sleep(2000);
                System.out.println("MyRunnable线程运行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
 class Main {

    public static void main(String[] args) {
        Thread t3 = new Thread(){//匿名内部类创建Thread子类对象

            @Override
            public void run() {
                while (true){

                    try {
                        Thread.sleep(3000);
                        System.out.println("匿名内部类Thread线程线程运行");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread t4  = new Thread(new Runnable() {//匿名内部类创建Runnable子类对象
            @Override
            public void run() {
                while (true){

                    try {
                        Thread.sleep(4000);
                        System.out.println("匿名内部类Runnable线程运行");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread t5 = new Thread(()->{//lambda表达式创建线程
            while (true){

                try {
                    Thread.sleep(5000);
                    System.out.println("lambda表达式线程运行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t1 = new Mythread();
        t1.start();
        Thread t2 = new Thread(new MyRunnable());
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

(4)前台线程与后台线程

前台线程:只要应用程序中存在一个或多个前台线程未退出,应用程序就会持续运行。只有当所有的前台线程都完成后,应用程序才会退出。

后台线程:应用程序是否退出与后台线程是否运行完没有直接关系,当前台线程全部终止后,应用程序退出,后台线程也会被强行终止。

(5)线程调度

5.1调度由谁来执行

在Java中,线程的调度是由Java虚拟机(JVM)和底层操作系统共同完成的。JVM会将Java线程映射为操作系统的线程(通常是一对一映射),然后操作系统会根据其调度策略和算法来为这些线程分配CPU时间。

5.2调度方式

调度模型分为抢占式调度模型和非抢占式调度模型,java的虚拟机默认采取的是抢占式的调度模型

这也就意味着java线程的调度是随机的

如下图为上方代码运行结果并不规律,可见线程的调度是抢占式执行的,是随机的。

5.3优先级

虽然线程的调度是随机的,但是我们可以设置一些参数来影响线程调度的行为,java中提供了线程的优先级这一概念(1-10,默认为5),优先级越大被调度的概率就越高

虽然Java提供了设置线程优先级的API : Thread.setPriority(int priority),但实际的调度效果很大程度上取决于JVM的实现和操作系统的调度策略。在某些JVM实现中,高优先级的线程可能更容易获得CPU时间,但这不是绝对的

5.4影响调用的方法

1.join()

当一个线程实例A执行了另一个线程实例B的 join() 方法时,A线程会暂停执行,直到B线程执行完毕,释放资源

比如在主线程中调用t.join(),那么主线程阻塞,等待t线程运行结束,若join加参数,则阻塞参数时间。

2.sleep()

让调用该方法的线程暂停执行指定的时间(以毫秒为单位),在此期间,线程不会释放它所持有的锁。当指定的时间过去后,线程会自动苏醒并继续执行。

3.yield()

调用该线程的方法会主动释放持有的资源,但释放之后又会主动再次去抢占资源

(6)Thread类

6.1构造方法

Thread() :创建线程对象
Thread(Runnable target) :使用 Runnable 对象创建线程对象
Thread(String name) :创建线程对象,并命名
Thread(Runnable target, String name) :使用 Runnable 对象创建线程对象,并命名
使用Runnable创建线程对象在上面代码中实现Runnable接口时用到,给线程命名是为了在调试时更好的认清是哪个线程

6.2常用属性及获取方法

紫色为属性,橙色为方法

ID getId() :ID 是线程的唯一标识,不同线程不会重复

名称 getName():名称是调试所需重要属性

状态 getState(): 获取当前线程状态 初始化(NEW),阻塞(BLOCKED),运行(RUNNABLE),TERMINATED(工作完成)等....如下图
优先级 getPriority() :获取当前线程优先级
是否后台线程 isDaemon() :判断此线程是否为后端线程
是否存活 isAlive(): 判断此线程run方法是否执行完毕
是否被中断 isInterrupted() :判断此线程是否被中断

6.3内部标志位

当我们想要随时中断一个线程的时候(即使它处于阻塞状态),可以通过内部标志位Thread.currentThread().isInterrupted(),来对线程进行终止,currentThread()方法表示获取当前对象的引用。

6.3.1标志位中断通知方式
1.如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通
知,此时Java虚拟机会自动清除该线程的中断状态
2.否则,只是内部的一个中断标志被设置, thread 可以通过
Thread.interrupted() 判断当前线程的中断标志被设置, 清除中断标志
Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置, 不清除中断标志
public class Main {

    public static void main(String[] args) throws InterruptedException {
       
        Thread t5 = new Thread(()->{//lambda表达式创建线程
            while (!Thread.currentThread().isInterrupted()){//isInterrupted不会清除标志位

                try {
                    Thread.sleep(1000);
                    System.out.println("lambda表达式线程运行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t5");
        t5.start();
       Thread.sleep(5000);//主线程休眠5秒
        t5.interrupt();//将线程唤醒,即使此线程处于阻塞状态也能立即执行
      

    }
}

如上代码理想状态下是在将t5线程的标志位设置为true之后,t5线程便会立即停止执行

但运行结果如下,发现当t5线程因sleep阻塞挂起时,标志位以InterruptedException 异常的形式通知t5线程后,t5线程并没有停止运行而是继续打印,为什么呢?

原因:当interrupt唤醒线程之后,此时sleep方法抛出异常,同时清楚刚刚设置的标志位,这时我们刚设置的标志位,就好像没设置过一样,之所以这样设计是因为,java希望当线程收到中断指令之后,线程也能够决定,接下来该怎么处理。此时要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.

此时若在catch中写一个break 线程便会退出循环,如下图

6.3.2 使用Thread.currentThread().isInterrupted()为标志位的好处

假如说我们手动设置一个标志位,那么需要等到线程的休眠时间(sleep())过后才能够唤醒结束线程,但是此处使用的interrupt可以使sleep内部触发一个异常,从而能够被提前唤醒,结束线程。

(三)线程安全

(1)案例解析

现在看一个代码,是两个线程同时对一个变量count修改,按照正常的情况应该是打印出的count为5000.

public class Main2 {
   static int count = 0;//变量count
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{//线程1对count加2500次
            for (int i = 0; i < 2500; i++) {
                count++;
            }
        });
        Thread t1 =new Thread(()->{//线程2对count加2500次
            for (int i = 0; i <2500 ; i++) {
                count++;
            }
        });
        t.start();//线程1开启
        t1.start();//线程2开启
        t.join();
        t1.join();//防止线程1,2没运行主线程就打印
        System.out.println(count);
    }
}

运行结果如下,发现运行结果并不是 5000,并且每次运行结果都不一样,这是为什么呢?

线程运行过程讲解:

count变量++分为三步,load (从内存中读取变量),add(在cpu中执行+操作) , save是(将+操作后的变量保存到内存中)

理想情况下执行顺序应是如下图所示

但实际情况可能是

那么这种 load,add, save操作,多个线程交叉操作的情况会发生什么呢,我们以上图第一个情况来讲述

可见线程1和线程2执行了两次++操作,理想状态下count的值应该变为2,但在实际情况中,可能会出现如下执行两次加操作而实际上却只加了一次的情况出现,这就是典型的线程安全问题。

(2)线程安全问题原因

现在的造成线程安全问题的原大致可分为如下几类

1.处理机抢占式调度

2.多个线程同时修改同一变量

3.操作不是原子性的(不可分开执行的)

4.内存可见性问题(变量的修改不能被其他线程立即感知,由编译器优化造成)

5.指令重排序问题(编译器或处理器为提升性能,对指令执行顺序进行优化)

(3)线程安全问题解决

3.1synchronized

我们对原来代码进行修改,在给++操作加上sychronized修饰符

public class Main2 {

   static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t = new Thread(()->{
           synchronized (locker){//给线程1加锁
               for (int i = 0; i < 2500; i++) {
                   count++;
               }
           }
        });
        Thread t1 =new Thread(()->{
         synchronized (locker){//给线程2加锁
             for (int i = 0; i <2500 ; i++) {
                 count++;
             }
         }
        });
        t.start();
        t1.start();
        t.join();
        t1.join();
        System.out.println(count);
    }
}

多次运行后结果都为5000 ,接下来我将从synchronized特性,作用,工作过程和原理来详细阐述synchronized

3.1.1synchronized特性
synchronized 会起到互斥效果 , 某个线程执行到某个对象的 synchronized 中时 , 其他线程如果也 执行到同一个对象 synchronized 就会 阻塞等待 .
就像上厕所一样,如果你进了wc把门锁上(加锁),那么别的线程想要上这个厕所,就只有等你解决完(执行完毕)后主动开门(释放锁),别人才能进厕所方便
(执行结果正确原因) 如上方代码中,线程一获取到锁locker进行执行++操作, 线程二 也想执行++操作,但他需要 获取到锁locker后才能执行 ,但现在 锁locker被线程一持有 ,所以线程二就需要阻塞等待线程一释放锁locker之后才能获取到锁locker再上cpu运行。
执行流程大致如图所示

修饰范围 

注意:假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能
获取到锁, 而是和 C 重新竞争, 并 不遵守先来后到的规则 .
3.1.2synchronized执行过程
1. 获得互斥锁
2. 主内存 拷贝变量的最新副本到 工作的内存
3. 执行代码
4. 将更改后的共享变量的值刷新到主内存
5. 释放互斥锁
主内存,工作内存,线程关系大概如图所示,当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据。当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存,因此 synchronized在修改内存变量前后加锁 ,也能够 避免线程安全中内存可见性 的问题
3.1.3synchronized用的锁是存在Java对象头里的

类型对象在内存中的布局大致如下图所示

 

 因为synchronized的本质就是要修改指定的对象的对象头中的锁状态标志为有锁状态,所以synchronized使用时也必须搭配一个具体的对象使用

下图是StringBuffer类中的被synchronized修饰的方法,所以此处synchroized修改的是StringBuffer的对象头中的锁状态标志

此处是synchronized修饰代码块,明确指定了锁locker对象,所以此处修改的是locker对象头中的锁状态标志

3.1.4synchronized是可重入锁

如果一个锁是不可重入锁,那么当一个线程对自己加了两次锁,那么就会发生死锁现象,按照之前锁的设定,二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成,那么就会发生死锁。

如下图,t2线程load前时加锁,到add时又想加锁,但因第一次锁没释放而第二次锁阻塞等待,因为第二次锁的阻塞等待导致第一次锁无法释放。这就是不可重入锁带来的死锁问题

如下图是可重入锁

如果某个线程加锁的时候 , 发现 锁已经被人占用, 但是恰好 占用的正是自己 , 那么仍然 可以继续获取
到锁 , 并让计数器自增 .
解锁的时候计数器递减为 0 的时候 , 才真正释放锁 . ( 才能被别的线程获取到 )

 3.2volatile关键字

3.2.1案例解析

下面是运行整体代码,理想效果应该是在控制台中输入一个非零整数,t线程会终止,并打印“线程结束”的字样,但实际却是在控制台输入后,线程t并未停止,而是继续运行

public class Main2 {
 static class Flag{
     int flag = 0;
 }

    public static void main(String[] args) throws InterruptedException {
        Flag flag1 = new Flag();
        Object locker = new Object();
        Thread t = new Thread(()->{
          while (flag1.flag == 0){//输入0线程结束打印线程结束字样
             
          }
            System.out.println("线程结束");
        });

        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数");
            flag1.flag = scanner.nextInt();

        });
        t.start();
        t2.start();
        t.join();
        t2.join();


    }
}

原因:当线程t多次从主存中读取读取flag的时候,编译器发现,flag的读取次数非常高,那么就把flag=0变量复制一份放在工作内存(高速缓存,寄存器)中,再读取只需要在工作内存中读就行了(从工作内存读更快),那么当t2线程,修改了flag变量的值,并刷新到主存中时,线程t并没有立刻感知到flag被修改(因为线程t是从工作内存中读取flag变量的),这也就导致了内存可见性导致的线程安全问题。

 3.2.2volatile作用

当一个变量被声明为 volatile 时,它会保证所有线程访问这个变量时都会直接从主内存中读取其值,而不是从某个线程的缓存(工作内存)中读取,保证了内存可见性。

如下图当flag被volatile修饰后,线程t将只会在主存中读取flag变量,确保了当一个线程修改了变量的值后,其他线程能够立即看到这个修改。

 

注意:volatile只能保证内存的可见性,保证变量是从主存中 读取的,但并不能保证原子性。

(四)线程间通信

 (1)wait与notify与noifyAll

1.1wait的作用

wait方法的作用是让当前线程(即调用wait方法的线程)等待,直到其他线程调用该对象的notify()或notifyAll()方法,将其唤醒,或者等待时间结束(如果使用了带参数的wait方法)

1.2wait的执行过程

使当前执行代码的线程进行等待 . ( 把线程放到等待队列中 )
释放当前的锁 (synchronized使用的对象,必须和调用wait方法的对象相同)
满足一定条件时被唤醒 , 重新尝试获取这个锁 .

1.3wait结束等待条件

其他线程调用该对象的 notify 方法 .
wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本 , 来指定等待时间 ).
其他线程调用该等待线程的 interrupted 方法 , 导致 wait 抛出 InterruptedException 异常 .

1.4notify方法的作用

该方法是用来通知那些可能等待 该对象的对象锁 (调用notify方法的对象,必须和要唤醒线程的调用wait方法的对象相同) 的其它线程,对其发出notify通知 ,唤醒此线程,并使它们重新获取该对象的对象锁。(不重新获取到对象锁不会执行)
如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程,不会按照先来后到唤醒
执行完notify()方法后 ,当前 线程不会马上释放该对象锁 ,要等到执行 notify() 方法的线程将程序执行完,也就是退出同步代码块(sychronized修饰的代码块)之后才会释放对象锁。

1.5wait 和notify方法要与synchronized搭配使用

如下图所示,线程t3与t4的wait与notify方法未配合synchronized使用报出,监视器状态异常

正确使用代码演示 


public class Main2 {

 private static  int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();

        Thread t3 = new Thread(()->{
      synchronized (locker) {

          try {
            if (count != 5){//由于条件不满足释放锁等待
                System.out.println("t3线程由于count不等于5开始等待");
                locker.wait();//t3释放locker锁,并开始等待
            }
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          System.out.println("count等于5t3线程等待结束");

      }
        });
        Thread t4 = new Thread(()->{
synchronized (locker) {//t4拿到locker锁
    try {

        while (count < 5){
            Thread.sleep(1000);
            System.out.println("count = "+count);
         count++;
        }
        if (count == 5){//条件满足了开始唤醒
            System.out.println("count等于5开始唤醒t3线程");
            locker.notify();//t4唤醒等待locker对象锁的线程t3,但此时还未释放locker锁
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
      }//在这里才会释放locker锁
            });
         t3.start();
         t4.start();

        }
}

必须搭配使用原因 

为了正确的完成线程间的通信,当一个线程调用某个对象的 wait 方法时,它实际上是在告诉其他线程:“我已经完成了某些操作,现在需要等待某个条件成立才能继续执行。请你在条件成立时唤醒我。” 而 notify/notifyAll 则是其他线程用来告诉正在等待的线程:“你等待的条件已经成立了,你可以继续执行了。” 这种通信方式需要一种机制来确保等待的线程能够正确地被唤醒,并且唤醒后能够正确地检查条件,由于wait(),notify()/notifyAll(),这些方法涉及到线程状态的改变,以及锁的释放与获取,所以这些操作必须是原子性的,synchronized便保证的操作的原子性

比如,若没有synchronized,调用wait的线程正在等待条件成立,调用notify的线程刚把调用wait方法的线程唤醒,就和调用notify的线程挤占cpu,也不管notify线程是否还有一些让条件成立的必要操作没执行完,导致线程安全问题。

1.6wait释放锁后不会再去主动竞争这个锁 

本来就是你有些条件不满足,主动释放锁了,但是如果你释放锁后还去竞争这个锁(刚释放竞争比较容易得到),那条件还是不成立,这来回进进出出不就浪费时间吗,这可能会导致其他的线程饿死,所以wait方法规定调用的线程主动释放锁后,就不会再主动再去竞争锁了,有效的避免了线程饿死的情况。

1.7notify与notifyAll区别

看名字也能看出来,notify只会随机唤醒一个线程,notifyAll会唤醒所有等待这个对象锁的线程,并让他们争抢这个锁。

下图是notify的情况

下图是notifyAll的情况

(五)方法区别

(1)start与run

作用功能不同:

  1. run方法的作用是描述线程具体要执行的任务;
  2. start方法的作用是真正的去申请系统线程

运行结果不同:

  1. run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;
  2. start调用方法后, start方法内部会调用Java 本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段

(2)wait与sleep

1.wait方法会释放锁,sleep方法不会释放锁

2.wait必须搭配synchronized使用,sleep不需要

3.wait属于Object类中的方法,sleep是Thread类中的静态方法

4.调用wait方法后,线程会进入WAITING(无时限等待)或TIMED_WAITING(有时限等待,如果指定了超时时间)状态。调用sleep方法后,线程会进入TIMED_WAITING状态。

(六)线程池

(1)什么是线程池

定义:线程池是指在初始化一个多线程应用程序的时侯预先创建一定数量的线程形成一个线程集合(线程池)。当需要执行新的任务时,不是新建一个线程,而是从线程池中取出一个空闲的线程来执行该任务。当任务执行完毕后,线程将返回到线程池,等待执行下一个任务

(2)线程池的目的

线程的创建和销毁是一个比较费时的操作,它涉及到在操作系统内核中完成用户态到内核态的切换,以及内核中数据结构的创建和销毁等。这些操作都会消耗大量的CPU资源和时间。

线程池通过预先创建一定数量的线程并在需要时复用这些线程,从而避免了频繁地创建和销毁线程,所带来的开销

(3)线程池中的参数

核心线程数(corePoolSize):线程池中始终存活的线程数。这些线程在创建线程池时就会立即被创建,并等待任务的到来。

最大线程数(maximumPoolSize):线程池中允许的最大线程数。当工作队列满后,且线程数小于最大线程数时,线程池会创建新的线程来处理任务。

存活时间(keepAliveTime):非核心线程在没有任务执行时的存活时间。当线程数超过核心线程数时,超出的线程(即非核心线程)在空闲指定时间后会被销毁,以节省资源。

时间单位(unit):与存活时间配合使用,用于指定存活时间的时间单位,如秒、毫秒等。

工作队列(workQueue):用于存储待执行的任务。当线程池中的线程都在执行任务时,新的任务会被放入工作队列中等待执行。

拒绝策略拒绝策略用在当线程池满,并且工作队列也已经满了的时候,线程池会怎么对待新加的任务,java中给的拒绝策略有四种

1:AbortPolicy:当任务无法被线程池执行时,会抛出一个RejectedExecutionException异常

2:CallerRunsPolicy:当任务无法被线程池执行,会让调用这个线程池的线程执行这个任务,如果调用者线程在执行一个任务,则会创建一个新线程来执行这个被拒绝的任务

3:DiscardOldestPolicy:当任务无法被线程池执行,丢弃工作队列中最旧的任务,然后尝试重新提交当前任务

4:DisCardPolicy:当任务无法被线程池执行,任务直接被丢弃,不抛出异常,也不执行任务

(4)利用Executors创建线程池

4.1Executors帮我们配置好了大约四种线程池

1.单线程化线程池(SingleThreadExecutor)

2.固定线程数量线程池(FixedThreadPool)

3.可缓存线程池(CachedThreadPool)

4.定时任务线程池(ScheduledThreadPool)

4.2创建代码演示

4.2.1创建单线程化线程池
class MyThread extends Thread{//定义第一个线程类

    @Override
    public void run() {//定义任务内容
            try {
                for (int i = 0; i <5 ; i++) {
                System.out.println("第一个线程");
                Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
         
                e.printStackTrace();
            }

    }
}
class MyThread2 extends Thread{//定义第二个线程类

    @Override
    public void run() {//定义任务内容
        try {
            for (int i = 0; i <5 ; i++) {
                System.out.println("第二个线程");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
           
            e.printStackTrace();
        }

    }
}

public class Main3 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.submit(new MyThread());//提交任务给线程池
        executorService.submit(new MyThread2());//提交任务给线程池
        executorService.shutdown();//关闭线程池
    }
}

单线程化线程池特点及适用场景

特点

1.线程池中只包含一个工作线程,且此线程存活时间无限

2. 单线程化线程池会按照提交顺序依次执行任务,其余线程放入任务队列中

适用场景

任务逐个提交,依次执行的场景

4.2.2创建固定线程数量线程池

由于创建线程池都类似,我就不再浪费篇幅写相同代码

public class Main3 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);//参数意思是设置的线程池中最多线程数量
        executorService.submit(new MyThread());//提交任务给线程池
        executorService.submit(new MyThread2());//提交任务给线程池
        executorService.submit(new MyThread3());//提交任务给线程池
        executorService.shutdown();//关闭线程池
    }
}

固定线程数量线程池特点及适用场景

特点

1.如果线程数没有达到“固定数量”(参数设置的数量),每次提交一个任务,线程池内就创建一个新线程,直到线程达到线程池固定的最大数量,当线程运行结束后,线程池中的线程也不会销毁,而是进入waitting状态,如下图

2.若提交的任务超过了参数设置的最大值,就会把新提交的任务放到任务队列中(阻塞队列)

3.使用的是无界对列(最大值为integer.MAX_VALUE),如果等待队列中任务过多,可能会导致内存溢出

适用场景

需要任务长期执行任务量基本固定的场景

CPU密集型任务(防止cpu过载)

4.2.3创建“可缓存线程池”
public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(new MyThread());//提交任务给线程池
        executorService.submit(new MyThread2());//提交任务给线程池
        executorService.submit(new MyThread3());//提交任务给线程池
       executorService.shutdown();//关闭线程池
    }

可缓存线程池特点及适用场景

特点

1.线程池大小不固定,提交多少创建多少,对于空闲时间大于60秒的线程销毁

2.如果大量的异步任务执行目标实例同时提交,可能会因创建线程过多而导致资源耗尽

适用场景

用于执行大量短期异步任务,如Web服务器中的短连接处理、REST API接口的瞬时削峰

4.2.4创建定时任务线程池

schedule(Runnable command, long delay, TimeUnit unit):提交一个只执行一次的定时任务,该任务在指定的延迟后执行。

scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):提交一个周期性执行的任务,该任务在初始延迟后开始执行,随后以固定的周期重复执行,不考虑任务执行所需的时间。

scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):提交一个周期性执行的任务,但与前一个任务执行完成后的延迟时间有关,即在前一个任务执行完成后,等待指定的延迟时间再执行下一个任务。

class MyThread extends Thread{//定义第一个线程类

    @Override
    public void run() {//定义任务内容
            try {
                for (int i = 0; i <400 ; i++) {
                System.out.println("第一个线程");
                Thread.sleep(100);
                }
            } catch (InterruptedException e) {

                e.printStackTrace();
            }

    }
}
class MyThread2 extends Thread{//定义第二个线程类

    @Override
    public void run() {//定义任务内容
        try {
            for (int i = 0; i <400 ; i++) {
                System.out.println("第二个线程");
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {

            e.printStackTrace();
        }

    }
}
class MyThread3 extends Thread{//定义第二个线程类

    @Override
    public void run() {//定义任务内容
        try {
            for (int i = 0; i <1000 ; i++) {
                System.out.println("第三个线程");
                Thread.sleep(400);
            }
        } catch (InterruptedException e) {

            e.printStackTrace();
        }

    }
}
public class Main3 {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);//参数的意思是核心线程数,会一直存在的线程
         //注意用ScheduleExecutors类接收
            pool.schedule(new MyThread(), 0, TimeUnit.SECONDS);
            //参数         任务对象       延迟时间     时间单位
           pool.scheduleAtFixedRate(new MyThread2(),0,500,TimeUnit.MILLISECONDS);
           //参数                  任务对象 初始延时时间 执行周期   时间单位
          pool.scheduleWithFixedDelay(new MyThread3(),0,1,TimeUnit.MILLISECONDS);
        //参数                     任务对象   初始延迟时间 延迟时间    时间单位
       //pool.shutdown();//关闭线程池
    }
}

定时任务线程池特点及适用场景 

特点

1.定时任务线程池可以精确地控制任务的执行时间,无论是延迟执行还是周期性执行。

适用场景

适用于需要按照特定时间间隔或时间点执行的任务,如定时发送邮件、定时清理日志等。

需要定时备份数据或同步数据的场景

(5)模拟实现一个线程池

1:MyThreadPool类:
使用ArrayBlockingQueue作为工作队列,存储待执行的任务。
submit方法将任务(Runnable对象)添加到工作队列中。
构造函数MyThreadPool创建指定数量的工作线程,每个线程从工作队列中取出任务并执行。
2:Demo2类:
在main方法中,创建了一个包含10个线程的线程池。
提交1000个任务到线程池,每个任务打印一条消息表示任务正在执行。

class MyThreadPool{
    private BlockingQueue workQueue = new ArrayBlockingQueue(1000);//工作队列
    public void submit(Runnable runnable)throws InterruptedException {
        workQueue.put(runnable);//添加任务
    }
    //一个线程池
    public MyThreadPool(int n){
        //用这个来消费任务
        for (int i = 0; i < n; i++){
            new Thread(()->{

                    try {
                        ((Runnable) workQueue.take()).run();//取出任务,并执行
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

            }).start();
        }
    }
}

public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
         MyThreadPool myThreadPool = new MyThreadPool(10);
         for (int i = 0; i < 1000; i++){
             int id = i;
             myThreadPool.submit(new Runnable() {

                 @Override
                 public void run() {
                     System.out.println("执行任务"+id);

                 }
             });

         }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值