Java多线程

目录

1、定义

2.并发和并行

3.实现方式

(1)继承Thread类

(2)实现Runnable接口

(3)利用Callable接口和Future接口

 4.成员方法

(1)getName 和 setName 方法 

(2)currentThread 方法

(3)sleep 方法

(4)setPriority 和 getPriority 方法

(5)setDaemon 方法(守护线程)

(6)yield 方法(出让线程 / 礼让线程)

(7)join 方法(插入线程)

5.线程的生命周期

6.线程的安全问题

7.同步代码块

8.同步方法

9.Lock锁

10.死锁

11.等待唤醒机制(生产者与消费者)

(1)一般写法 

(2)利用阻塞队列实现

12.线程的完整状态

 13.线程栈

14.线程池

(1)定义

(2)实现

Ⅰ. newCachedThreadPool 方法

Ⅱ. newFixedThreadPool 方法

15.自定义线程池

(1)定义

 (2)过程

(3)最大并行数


1、定义

进程:进程是程序的基本执行实体(每个软件都有对应的一个进程)

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

(简单理解:应用软件中相互独立,可以同时运行的功能,就是不同的线程)


单线程程序:CPU从上往下依次运行,不会切换到其他代码中运行

多线程程序:CPU会在不同程序间进行切换,将等待的时间充分利用起来

 简单理解:多线程可以让程序同时做多件事情,从而提高效率。

2.并发和并行

并发:在同一时刻,有多个指令在单个CPU上交替执行

并行:在同一时刻,有多个指令在多个CPU上同时执行

以CPU是 2 核 4 线程为例,4 线程代表CPU可以同时并行 4 个线程。

每个物理核心在支持超线程技术的情况下,最多可以同时执行 2 条线程,所以 2*2=4

如果超出了 4 个线程需要运行,就会在多个线程之间依靠算法进行调度(切换),也就是并发。

 所以,在计算机中,并发和并行是可能同时存在的。

3.实现方式

(1)继承Thread类

步骤:

1.自定义一个类继承Thread类

2.重写run方法

3.创建子类对象,并启动线程

public class MyThread extends Thread {
    @Override
    public void run() {
        //书写线程要执行的代码
        for (int i = 0; i < 5; i++) {
            System.out.println(getName() + "HelloWorld");
        }
    }
}

注意:由于自定义类继承了 Thread类,所以可以使用 Thread类中的 getName 方法。

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        //给线程起名字
        t1.setName("线程1");
        t2.setName("线程2");

        //启动线程
        t1.start();
        t2.start();
    }
}

 运行结果:

根据结果,可以发现,两个线程会不停的交替执行。

(2)实现Runnable接口

步骤:

1.自定义一个类实现 Runnable 接口

2.重写 run 方法

3.创建自定义类对象

4.创建 Thread 类对象,并开启线程

public class MyRun implements Runnable {
    @Override
    public void run() {
        //书写线程要执行的代码
        for (int i = 0; i < 5; i++) {
            //获取到当前线程的对象
            Thread t = Thread.currentThread();
            System.out.println(t.getName() + "HelloWorld");
        }
    }
}

注意: 

由于自定义类没有继承 Thread类,和 Thread类无关,所以不能直接使用 getName 方法

需要先使用 Thread类的静态方法 currentThread 来获取执行当前任务的线程对象

public class ThreadDemo {
    public static void main(String[] args) {
        //创建自定义类的对象(表示多线程要执行的任务)
        MyRun mr=new MyRun();

        //创建线程对象(将任务传递给该线程对象)
        Thread t1=new Thread(mr);
        Thread t2=new Thread(mr);

        //给线程起名字
        t1.setName("线程1");
        t2.setName("线程2");

        //启动线程
        t1.start();
        t2.start();
    }
}

运行结果:

(3)利用Callable接口和Future接口

特点:可以获取到多线程执行的结果

前两种方式都是通过重写 run 方法,而 run 方法的返回值类型是 void 类型。

如果需要获取多线程执行的结果(返回一个其他类型的返回值),将无法实现。

步骤:

 1.自定义类实现 Callable 接口

2.重写 call 方法(是有返回值的,表示多线程运行的结果)

3.创建自定义类对象(表示多线程要执行的任务)

4.创建 Future 接口的实现类 FutureTask 的对象(作用:管理多线程运行的结果)

5.创建 Thread 类的对象,并启动(表示线程)

public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        //求1~100的和
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

注意: Callable 接口中的泛型就是多线程运行结果的返回值类型,所以需要指定该数据类型。

public class ThreadDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建自定义类对象(表示多线程要执行的任务)
        MyCallable mc = new MyCallable();

        //创建Future接口的实现类FutureTask的对象(作用:管理多线程运行的结果)
        FutureTask<Integer> ft = new FutureTask<>(mc);

        //创建线程的对象
        Thread t1 = new Thread(ft);
        //启动线程
        t1.start();

        //获取多线程运行的结果
        Integer result = ft.get();
        System.out.println(result);
    }
}

细节:

 FutureTask 对象就是用来管理多线程运行的结果的,所以该对象可以调用 get 方法获取多线程运行的结果,返回值类型就是泛型中指定的数据类型。

三种实现方式的对比:

注意:Java中只支持单继承,不支持多继承。所以继承了 Thread 类,就不能再继承其他的类。

 4.成员方法

(1)getName 和 setName 方法 

细节:

① 如果我们没有使用 setName 方法给线程设置名字,线程默认也是有名字的。

     默认格式:Thread-X(X是序号,从 0 开始)

在 Thread 类的空参构造方法, 第三个参数就是给线程起名字。

nextThreadNum 方法就是线程的序号,其中 threadInitNumber 没有赋值,默认初始化值为 0。


 ② 不仅可以通过 Thread 对象调用 setName方法给线程起名字,Thread 的构造方法也可以设置名字。

注意:

Thread 类的构造方法可以传递参数设置名字,而 Java 中构造方法是不能继承的。

自定义类想要通过构造方法设置名字,就必须在本类的构造方法中调用父类 Thread 的构造方法。

(2)currentThread 方法

细节:

 当 JVM 虚拟机启动后,会自动的启动多条线程。

其中,有一条线程就叫做 main 线程,作用是去调用 main 方法,执行其中的代码。

(3)sleep 方法

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("aaaa");

        //main线程睡眠5s
        Thread.sleep(5000);

        System.out.println("bbbb");
    }
}

细节:

① sleep 方法是静态方法,直接通过 类名.方法名 调用即可。

② 哪条线程执行 sleep 方法,那它就会在这里停留对应的时间。

③ 方法的参数:就表示睡眠的时间,单位是毫秒 ms 。

④ 当时间到了之后,线程会自动苏醒,继续执行下面的代码。

(4)setPriority 和 getPriority 方法

线程的调度分为两种:抢占式调度和非抢占式调度

抢占式调度:多个线程抢夺CPU的执行权,执行哪个线程,执行多长时间都是随机的。

非抢占式调度:所有的线程轮流执行。

注意:在 Java 当中,线程的调度采用抢占式调度方式。


优先级就表示该线程随机到的概率,优先级越大,抢到 CPU 的概率越大。

细节:

① 如果没有设置优先级,那么默认优先级都是 5 ,main 线程的优先级也是5。

② 在 Thread 类的成员变成中,优先级最小为 1,最大为 10,默认为 5 。

③ 优先级大的只是概率大,不是绝对的优先。

(5)setDaemon 方法(守护线程)

细节:当其他的非守护线程执行完毕之后,守护线程也会(在极短时间内)陆续结束。

运行结果:

在线程 1(非守护线程)执行完毕后,线程 2(守护线程)也就随之结束了(即使没执行完)。

线程 1 执行完毕后,系统会告知线程 2 结束,告知的这个过程也要时间,所以线程 2会继续执行。


 应用场景: 主功能(聊天)结束/关闭了,次功能(传输文件)也就随之结束了。

(6)yield 方法(出让线程 / 礼让线程)

细节:

 yield 方法表示该线程出让当前CPU的执行权,这样其他线程就能抢到,使运行结果尽可能的均匀

注意:

只是使结果尽可能的均匀,并不完全一定

因为 t1 在出让执行权之后,很有可能继续抢到该执行权。

(7)join 方法(插入线程)

在正常情况下 ,线程 1 会和 main 线程互相抢夺 CPU 的执行权。

只不过 main 线程在抢到之后,瞬间就执行完毕了。


Question:有没有什么办法,先让线程1先执行完毕,再轮到main 线程中的 for 循环代码执行呢?

使用 join 方法将线程 1 插入到当前线程(main线程)之前,就可以使得线程1 执行完毕之后,才轮到main 线程继续执行后续代码。

5.线程的生命周期

 注意:

就绪状态下,线程有执行资格,也就是有资格去抢夺CPU的执行权。

正在抢,但还没有抢到,所以没有执行权。一旦抢到执行权,就会变成运行状态。


Question:sleep方法会让线程睡眠,睡眠时间到了之后,就立马会执行下面的代码吗?

 不会,sleep方法结束后,线程会变成就绪状态, 先去抢夺CPU的执行权,抢到才会执行代码。

6.线程的安全问题

需求:有100张票,三个窗口在卖票,设计程序进行模拟。

由于 ticket 是成员变量,new 创建了 3 个线程对象,这三个对象中都有自己独立的 ticket 属性。

所以实际上,卖了 300 张票。为了 让 ticket 共享,要将其设置为静态变量。

但即使将 ticket 修改为静态变量后,我们发现,同一张票也可能被买了多次,甚至可能出现漏票、错票的情况。 


Question1:为什么同一张票会卖多次?

这时由于线程执行时,具有随机性,也就是随时随地,都有可能被抢夺CPU的执行权。

当线程 2 首次抢夺到 CPU的执行权后,执行了打印语句。

这时,线程 1 突然抢到了 CPU 的执行权,就会使得线程 2 来不及执行 ticket++;的代码。

所以 ticket 此时还是为 1,线程 1 会继续卖第 1 张票。

同理,此时线程 3 突然抢到了 CPU的执行权,线程 1 也就会来不及执行 ticket++;的代码。

所以 ticket 此时还是为 1,线程 3 也会继续卖第 1 张票。、


Question2:为什么会漏票?

由于线程执行时,具有随机性。

当线程 1 卖完第 4 张票后,如果执行权还在,继续执行 ticket++;的代码,ticket自增为 5。

此时线程 2 突然抢到了 CPU 的执行权,由于也卖完了第 4 张票,会执行 ticket++,ticket自增为 6

接下来,线程 3 突然抢到了 CPU 的执行权,它就会卖出第 6 张票。

7.同步代码块

为了解决上述的线程安全问题,我们可以将操作共享数据 ticket 的代码进行加锁。

当线程运行到被锁住的代码的时候,只有该线程能执行操作,其他线程将等待锁被释放。

只有当该线程执行完毕后,其他线程才有资格去抢夺执行权。

同步代码块:把操作共享数据的代码锁起来

特点:

① 锁默认打开,当有一个线程进去了,锁自动关闭。

② 锁中代码全部执行完毕,线程出来后,锁自动打开。

运行结果:

注意:

① synchronized 同步代码块要写在循环内,不能写在循环外。

如果写在循环外,那么线程将会一次性将内部循环全部执行完毕,就轮不到其他线程执行。

运行结果 :

线程 1 在抢到执行权之后,会运行完整个 while 语句,才能从锁中出来。

所以 其他线程就算抢到执行权,也会被关在锁外,全程只有线程 1 在执行代码。


② synchronized 的括号中传递的锁对象,可以是任意对象,但一定要是唯一的(static修饰)

如果锁对象是唯一的,那么线程 1进入后立即上锁,线程 2 发现锁关闭后,就会进入等待。

但如果锁对象是不同的,那么同步代码块等于没写:

        线程 1 看 锁对象 1 有没有上锁,没上锁进入代码块执行代码。

        线程 2 看 锁对象 2 有没有上锁,也没上锁进入代码块执行代码。

③ synchronized 的括号中传递的锁对象,我们一般设为当前类的 class 对象,因为其是唯一的。

④ sleep 方法最好写在 synchronized 外,因为写在内部的话,即使其他线程抢到了执行权,也会被锁在外面。

8.同步方法

如果整个方法都需要进行加锁,就没必要将整个方法的所有代码都放进同步代码块中了。

同步方法:将 synchronized 关键字加到方法上,此时这个方法称为同步方法。

特点:

① 同步方法会锁住方法里面所有的代码

② 锁对象不能自己指定,是 java 已经规定好的:

        非静态方法:this

           静态方法:当前类的字节码文件

注意:

在上面,ticket 设置为静态变量,是因为通过继承 Thread 类实现的多线程。

如果要创建多个线程,就会创建多个 MyThread对象。

而现在是通实现 Runnable 接口实现的多线程,MyRunnable 类只会创建一个对象。

因为这个对象是作为参数交给线程去执行的,所以只创建 1 个对象,ticket 就不用设置为静态变量了。

9.Lock锁

在同步代码块和同步方法中,我们并不能清晰的看到在哪里加锁,哪里释放锁。

JDK 5以后提供了一个新的锁对象 Lock,实现提供比使用 synchronized方法和语句更广泛的锁定操作。

Lock 中提供了获得锁和释放锁的方法:

 

注意:

Lock 是一个接口,不能直接实例化,需要采用它的实现类 ReentrantLock 来实例化。

运行结果:

 

Question1:根据结果发现,又出现一张票被卖多次,和漏票的情况,这是怎么回事呢?

在 MyThread 类的成员变量中,我们 new 创建了一个 Lock 的实现类对象。

但是,在创建线程时,我们创建了多个 MyThread 类的对象。

这就会导致每个 MyThread 对象内部都会创建一个 Lock 锁的对象,又导致了不同线程看不同锁的情况。

解决办法:我们必须在 Lock 锁的对象前,加上 static 关键字,让多个线程共享同一把锁

运行结果:

Question2:为什么程序运行结果是正常的,但程序不会停止呢?

当线程第 101 次进入循环,这时 ticket =101,那么将直接从 break 语句跳出循环。

所以第 101 次并没有执行 lock.unlock(); 锁也就不会释放,那么程序也就不会停止。

虽然可以在break 语句前再加上 lock.unlock(); 但这样会导致释放锁的(结束)代码写了两遍。

解决办法:利用 try - catch - finlly 语句的性质,将释放锁的代码写在 finally 中。

 此时就可以保证,锁一定会被释放,程序也就可以正常结束了。

10.死锁

运行结果:

 

原因:

由于线程 B 先抢到执行权,所以执行代码,将 B 锁关闭,执行打印语句(“线程B拿到了B锁...”)

但线程执行时,具有随机性。

此时线程 A 突然抢到了执行权,发现 A锁是打开的,于是执行打印语句("线程A拿到了A锁")。

接下来,线程 A 继续往下执行,发现 B 锁被线程 B拿到,处于关闭状态,则会一直等待B锁释放。

线程 B 继续往下执行,发现 A 锁被线程 A 拿到,处于关闭状态,则会一直等待A锁释放。

这就会造成多个线程处于互相等待状态,谁都无法继续执行下去,这就称为死锁。

结论:在涉及多个锁时,不要让两个锁发生嵌套,否则有可能会产生死锁。

11.等待唤醒机制(生产者与消费者)

在这个过程中,涉及以下方法: 

(1)一般写法 

//消费者:消费数据
public class Consumer extends Thread {
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    //1.判断桌子上是否有食物
                    if (Desk.foodFlag == 0) {
                        //2.如果没有,就等待
                        try {
                            Desk.lock.wait();//底层:让当前线程跟Desk.lock锁绑定
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        //3.如果有,就吃
                        Desk.count--;
                        System.out.println("正在吃面条,还能再吃" + Desk.count + "碗");
                        Desk.foodFlag = 0;//修改桌子上食物的状态
                        //4.吃完之后,唤醒生产者继续制作
                        Desk.lock.notifyAll();//唤醒只跟这把锁绑定的所有线程
                    }

                }
            }
        }
    }
}

 注意:

唤醒的时候我们需要唤醒指定的某些线程,而不是唤醒计算机中所有的线程。

Desk.lock.wait();在底层会让当前线程跟Desk.lock锁绑定,这样唤醒的时候就只会唤醒跟这把锁绑定的线程。

public class Producer extends Thread {
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    //1.判读桌子上是否有食物
                    if (Desk.foodFlag == 1) {
                        //2.如果有,就等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        //3.如果没有,就制作食物
                        System.out.println("正在制作食物");
                        Desk.foodFlag=1;//修改桌子上食物的状态
                        //4.唤醒等待的消费者
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}
//中间者(桌子):控制生产者与消费者的执行
public class Desk {
    //是否有食物(0:没有  1:有)
    public static int foodFlag = 0;

    //食物的总个数
    public static int count = 10;

    //锁对象
    public static Object lock = new Object();
}
//完成生产者与消费者交替执行
public class ThreadDemo {
    public static void main(String[] args) {
        //创建线程对象
        Consumer c = new Consumer();
        Producer p = new Producer();

        //给线程起名字
        c.setName("消费者");
        p.setName("生产者");

        //开启线程
        c.start();
        p.start();
    }
}

运行结果:

(2)利用阻塞队列实现

 细节:生产者和消费者必须使用同一个阻塞队列

public class Consumer extends Thread {
    ArrayBlockingQueue<String> queue;

    public Consumer(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            //不断的从阻塞队列中取出食物
            try {
                String food = queue.take();
                System.out.println("吃了一碗" + food);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

注意:

此时的 run 方法是不需要加同步代码块/方法的,因为在 take 方法底层已经加了Lock锁。

我们不能再在 run 方法中添加 同步代码块/方法,这样就会产生锁的嵌套,从而产生死锁。

//生产者
public class Producer extends Thread {
    ArrayBlockingQueue<String> queue;

    public Producer(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true){
            //不断的把食物放到阻塞队列中
            try {
                queue.put("面条");
                System.out.println("制作了一碗面条");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

注意:

此时的 run 方法是不需要加同步代码块/方法的,因为在 put 方法底层已经加了Lock锁。

我们不能再在 run 方法中添加 同步代码块/方法,这样就会产生锁的嵌套,从而产生死锁。

public class ThreadDemo {
    public static void main(String[] args) {
        //1.定义一个阻塞队列(泛型:表示队列中数据的类型,参数:表示队列中的数据的上限)
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

        //2.创建线程的对象,将阻塞队列传递过去(确保生产者和消费者共用一个阻塞队列)
        Producer p = new Producer(queue);
        Consumer c = new Consumer(queue);

        //3.启动线程
        p.start();
        c.start();
    }
}

 注意:

要通过创建线程对象的方式,将同一个阻塞队列的对象传递过去,从而确保生产者和消费者共用一个阻塞队列。

运行结果:

Question:结果发现,竟然会打印出消费者多次吃食物和生产者多次制造食物的情况,并不是交替轮流执行,这是为什么呢?

因为在代码中,加锁和释放锁的操作是在 take 和 put 语句内执行的,它确保了共享数据的正常执行。

而打印语句,是在 take 和 put 语句外,也就是在锁外,所以它仍是有可能被抢占执行权的。

这就造成了打印语句看起来数据处理不正常的情况。

但实际上,数据都在 take 和 put 语句中执行,是不会受到任何影响的。

12.线程的完整状态

注意:

在 Java中,实际上是没有定义运行状态的,只有其他 6 种状态。

因为线程在抢夺到CPU的执行权之后,此时JVM虚拟机就会将当前的线程交给操作系统进行管理。

由于已经交出去了,JVM不再负责管理,所以Java就没有定义运行状态了。

所以,在Java中,只有以下 6 种 状态:

 13.线程栈

在 Java中,堆内存是唯一的;而栈内存不是唯一的,栈和线程是相关的。

首先 main 线程会创建自己的栈,将 main 方法进栈。

然后创建 t1 和 t2 对象,线程的名字初始化为 Thread-0 和 Thread-1。

接着,通过 setName 方法,重新给两个线程起名字,对象中 name 属性的值就会随之发生改变。

start 方法启动线程后,内存中就会给 线程 1 和 线程 2 开辟两个新的栈。

注意:每一个线程都有一个自己独立的栈空间。

接着,线程 1和 线程 2 都会去执行 run 方法,所以 run 方法会同时进入两个线程的栈。

那么 run 方法中的 局部变量 b,也就会在两个线程的栈中都会定义。

14.线程池

通过之前的方式所创建的线程,会带来以下弊端:

① 用到线程的时候就创建

② 用完之后线程就消失

但这种方式并不好,会过度浪费操作系统的资源

(1)定义

线程池就是一个容器,初始状态下是空的。

当给线程池提交一个任务时,线程池就会自动创建一个线程。

这个线程就会去执行任务,任务执行完毕后,线程不会消失,会回到线程池中。

如果下次继续有新的任务,会再次将该线程拿来复用,不会创建新的线程。

如果任务过多,就会自动创建新的线程。

线程池中容纳线程的数量是有上限的,而且这个上限是可以自行设置的。

如果任务超出了该上限,线程池中没有空闲线程,那么其他任务就只能排队等待。

(2)实现

注意:没有上限并不是真正的没有上限,而是上限为 int 类型的最大值,也就是 21 亿多。

Ⅰ. newCachedThreadPool 方法
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
public class PoolDemo {
    public static void main(String[] args) throws InterruptedException {
        //1.获取线程池对象(无上限)
        ExecutorService pool1= Executors.newCachedThreadPool();

        //2.提交任务
        pool1.submit(new MyRunnable());

        //让main线程睡眠0.1s,使得线程有充足的时间执行完任务,然后回到线程池中
        Thread.sleep(100);
        pool1.submit(new MyRunnable());
        
        Thread.sleep(100);
        pool1.submit(new MyRunnable());

        //3.销毁线程池
        pool1.shutdown();
    }
}

注意:

如果main线程不睡眠,那么它将会和线程中的线程抢夺执行权。

线程池中的线程1还没执行完, main线程就已经会往下执行了。

这时线程池中没有空闲线程,从而线程池只能分配新的线程去执行新的任务。


运行结果:

可以发现,虽然执行了三次任务,但都是同一个线程在执行。

Ⅱ. newFixedThreadPool 方法
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 4; i++) {
            System.out.println(Thread.currentThread().getName() + "@" + i);
        }
    }
}
public class PoolDemo2 {
    public static void main(String[] args) throws InterruptedException {
        //1.获取线程池对象
        ExecutorService pool1= Executors.newFixedThreadPool(2);

        //2.提交任务
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());

        //3.销毁线程池
        pool1.shutdown();
    }
}

运行结果:

可以看出,由于线程池指定了上限为2,所以最多只有两个线程在执行任务。

且只有当有线程执行完毕,回到线程池中后,线程池中才会有空闲线程,才能执行下次任务。

15.自定义线程池

 通过上述两个静态方法可以创建线程池,但仍是不够灵活。

例如:有多个任务需要处理,且没有空闲线程,那么任务就会排队等待,我们能否设置此队列的长度呢?

查看 newCachedThreadPool 和 newFixedThreadPool 的源码,我们发现:

底层事实上 new 创建了一个 ThreadPoolExecutor 对象,就是代表线程池的类。 

(1)定义

ThreadPoolExecutor 的构造方法中,有 7 个参数:

① 允许创建的最大核心线程数量(不能小于0)

② 允许创建的最大线程数量

(不能小于 0 且必须 >= 核心线程数量,

   最大线程数量 - 最大核心线程数量 = 最大临时线程数量)

③ 临时线程的最大存活时间1(设置时间的值,不能小于0)

④ 临时线程的最大存活时间2(设置时间的单位,使用TimeUnit指定)

⑤ 任务队列(其实就是一个阻塞队列,不能为null)

⑥ 创建线程的工厂(也就是怎样创建一个线程,不能为null)

⑦ 任务的拒绝策略(一共有四种策略,四选一,一般选择第一个,不能为null)

 (2)过程

初始状态下,线程池是空的,我们设置核心线程的数量为 3,临时线程的数量也为 3。

当提交了3个任务时,线程池就会创建 3 个核心线程分别去执行任务。

当提交了 5 个任务时,线程池中的 3 个核心线程执行前三个任务。

剩下两个任务进入任务队列排队等待,直到有空闲线程,才会被执行。

我们将任务队列的长度定义为 3。

当提交了 8 个任务时,前三个任务交给核心线程执行,456 三个任务放入任务队列中。

此时任务队列的容量已经装满,那么就会创建 2 个临时线程去执行剩下的任务 7 和 8。

细节:

① 当核心线程都在运行,任务队列已经满了时,才会创建临时线程。

② 任务在执行时,不一定是严格按照提交的顺序执行的(如上:1-2-3-7-8-4-5-6)

当提交了 10 个任务是,前三个任务交给核心线程执行,456 三个任务放入任务队列中。

 此时任务队列的容量已经装满,那么就会创建 3 个临时线程去执行剩下的任务 789。

由于临时线程的数量也已达到最大,那么此时剩下的任务 10 就会触发任务拒绝策略(默认舍弃)

//创建自定义线程池
ThreadPoolExecutor pool2=new ThreadPoolExecutor(
        3, //设置核心线程的最大创建数量
        6, //设置最大线程数量(最大临时线程数量也就是6-3=3)
        60, //设置临时线程的最大空闲时间的值部分
        TimeUnit.SECONDS, //设置临时线程的最大空闲时间的单位部分
        new ArrayBlockingQueue<>(3, //任务队列
        Executors.defaultThreadFactory(), //创建线程的工厂
        new ThreadPoolExecutor.AbortPolicy() //任务的拒绝策略
);

(3)最大并行数

前面说过,以CPU是 2 核 4 线程为例:

2 核代表CPU有 2 个物理核心,4 线程代表CPU可以同时并行 4 个线程。

每个物理核心在支持超线程技术的情况下,最多可以同时执行 2 条线程,所以 2 * 2=4。

最大并行数:就是最多可以同时并行线程的数量,也就是 4。


Question:  那线程池的最大线程数量是不是越大越好呢?

其实不然,线程池的最大线程数量通常是按照规定来的,这取决于开发的项目是CPU密集型还是I/O密集型的。

CPU密集型:项目中频繁涉及计算(计算需要靠 cpu 执行)。

IO密集型:项目中频繁涉及读取本地文件或者数据库(IO操作不需要CPU)--> 目前大多数项目 

 注意:

① +1是为了保证当已经创建了的某个线程出现问题时,可以利用这个多出来的线程继续工作, 尽可能地将CPU利用率最大化。

② 操作 1 和 操作 2中,操作 2 是相加,涉及计算,需要 CPU 执行,所以CPU计算时间 = 1s 。

  • 25
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值