Java – 多线程 – 基础使用

简介

多线程分两个概念,分别是【并发】和【并行】。

并发指的是单个CPU在多个代码段之间进行游走执行,在一个时间段内,多个代码段只有一个能被执行。

并行指的是多个CPU在多个代码段之间同时执行,在一个时间段内,多个代码段能同时被执行。

 

多线程实现

方法一:继承Thread类

通过继承Thread 类,并重写run 方法。调用时先new 多线程类,调用start() 方法启动多线程。

public class MyThread extends Thread {
    /*
     * 重写run方法
     * */
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + ":HelloWorld");
        }
    }
}

MyThread t1 = new MyThread();
t1.setName("线程1");
t1.start();

优缺点:

优点:编程比较简单,可以直接使用Thread类中的方法

缺点:可扩展性较差,不能再继承其它的类,不支持返回值

 

 

方法二:实现Runnable接口

通过类实现接口Runnable 中的run 方法。

创建对象,并创建Thread类,把多线程对象传入Thread类中调用start() 方法即可。

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        /**
         * 通过 Thread.currentThread() 获取当前线程的信息,比如 getName() 方法等
         */
        Thread t = Thread.currentThread();
        for (int i = 0; i < 100; i++) {
            System.out.println(t.getName() + ":HelloWorld");
        }
    }
}

// 创建多线程对象
MyRunnable m1 = new MyRunnable();

// 创建Thread对象
Thread t = new Thread(m1);
t.setName("线程1");

// 启动线程
t.start();

 

 

使用Lambda表达式

new Thread(() -> {
     for (int i = 0; i < 100; i++) {
          System.out.println("" + i);
     }
}).start();

 

优缺点:

优点:扩展性强,实现该接口的同时还可以继承其它的类

缺点:编程相对复杂,不能直接使用Thread类中的方法,不支持返回值

 

方法三:利用Callable接口和Future接口实现

这种方法可以获取到多线程运行的结果

1.创建一个类MyCallable实现Callable接口

2.重写call(有返回值,表示多线程要执行的任务)

3.创建MyCallable的对象(表示多线程要执行的任务)

4.创建FutureTask的对象(作用管理多线程运行的结果)

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

 

// 创建一个类MyCallable实现Callable接口
public class MyCallable implements Callable<Integer> {
    /**
     * 重写 call() 方法(有返回值,表示多线程要执行的任务)
     * @return
     * @throws Exception
     */
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            sum += i;
        }
        return sum;
    }
}


/**
 * 创建多线程类的对象
 * 创建MyCallable的对象(表示多线程要执行的任务)
 */
MyCallable mc = new MyCallable();

/**
 * 创建 FutureTask 任务管理对象
 * 因为 Callable 接口提供带有返回值的多线程方法
 * 因此 FutureTask 是用于管理返回值的
 * 创建FutureTask的对象(作用管理多线程运行的结果)
 */
FutureTask<Integer> ft = new FutureTask<>(mc);

/**
 * 创建 Thread 线程对象
 * 创建Thread类的对象,并启动(表示线程)
 */
Thread t = new Thread(ft);

/**
 * 启动线程
 */
t.start();

/**
 * 取得线程运行后的结果
 */
Integer result = ft.get();

System.out.println(result);

 

使用Lambda表达式

    public static void CallableTwo() {
        new Thread(new FutureTask<Integer>(() -> {
            int sum = 0;
            for (int i = 0; i < 100; i++) {
                sum += i;
            }
            return sum;
        })).start();
    }

 

优缺点:

优点:扩展性强,实现该接口的同时还可以继承其它的类,支持返回值

缺点:编程相对复杂,不能直接使用Thread类中的方法

 

多线程常用方法

getName 获取线程名

返回此线程的名称。

注意:如果创建线程时不设置线程名称,默认会以【Thread-序号】命名。会从【Thread-0】开始,【Thread-1】、【Thread-2】...如此类推

Thread thread = new Thread("线程名");
thread.getName();

 

setName 设置线程名

设置线程的名字(在Thread类构造方法中也可以直接设置名字)

Thread thread = new Thread("线程名");
thread.setName("线程名");

注意:如果是继承Thread类的子类(如上文方法一),在构造函数中定义线程名会报错,这是因为子类中没有对应的构造函数,可以让子类重写父类的带参构造函数

 

static currentThread 获取当前线程

获取当前线程的对象,如果是在main函数中调用,则返回的是main线程(JVM主线程)

Thread.currentThread();

 

static sleep 线程休眠

让线程休眠指定的时间,单位为毫秒

Thread.sleep(1000);

 

setPriority 线程优先级

设置线程的优先级,优先给最低是1,最高是10,如果默认的优先级是5.

包括main线程在内的所有线程,默认优先给都是5.

优先给是指该线程是否更大概率争抢到CPU的执行权,但不保证一定,只是概率大小上更具优势。

// 使用 getPriority() 取得优先级
Thread.currentThread().getPriority(); // 取得 main 的优先级为5

// 设置优先级
Thread.currentThread().setPriority(10); // 设置优先级为10后,在多个线程运行过程中,该线程会总体运行更多代码

 

setDaemon 守护线程

设置守护线程,即守护线程会实时监测非守护线程的执行情况,如果非守护线程在执行过程,守护线程也会执行。当非守护线程执行结束,守护线程也会陆续结束(即使守护线程未执行完成都直接结束)

Thread t1 = new Thread("非守护线程");
Thread t2 = new Thread("守护线程");
// 设置 t2 为守护线程 
t2.setDaemon(true);

/**
 * 当t1没有结束时,t2依然运行,当t1运行结束时,t2不管是否运行完成都会马上结束
 */
t1.start();
t2.start();

 

 

static yield 协程、出让、礼让线程

在线程的运行过程中,CPU并非会绝对性的在多个线程代码中随机游走,而是可能会在对某一条线程执行过程中提供更久的处理资源,使得其它线程会在某些不确定的时候多等待一会。

举例子,比如线程A和线程B都同时执行100个循环,CPU实际执行过程中并非绝对性的【先A再B再A再B】的顺序执行,有可能是【AAAAAAAABBBBBBBABABBBBBB】的随机顺序。

因此,可以在代码中加入 yield 使得该线程执行完某一个循环时,主动出让处理权,使CPU在执行完成一次循环后,再一次公平选择执行A还是执行B,使得AB线程更均衡地执行。

    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + ":HelloWorld");
            // 执行完一次循环后,出让执行权让CPU重新选择执行
            Thread.yield();
        }
    }

 

 

join 插入、插队线程

当本线程和多线程出现线程争夺时(如在main方法中调用多线程对象时,main线程和另一个线程争夺CPU处理权)

可以使用 join 强制插入到当前线程之前,优先处理。

        Thread thread = new Thread("线程1");
        // 使用插入线程后,执行 main 方法中的循环的资源将被优先提供给【线程1】处理完
        thread.join();
        thread.start();
        
        // 在 main 方法中执行循环(使用 main 线程)
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }

 

多线程生命周期

 

线程锁

在多线程执行过程中,CPU对代码的执行是随机性的,也就是说CPU不会把一段代码执完完全后再给其它线程进行执行,有可能是执行了一半的代码就跑去执行其它线程了

这使得非常大的可能出现多条线程共同运行在同一段代码块中,引起部分变量被同时改变,影响变量在各个线程的精准度。

Java提供一种线程锁,在线程锁代码内,允许有且仅有一条线程在代码块中执行,其它线程只能在代码块外等待。

synchronized(锁对象){}

线程锁参照锁对象 来识别那些线程是一组的,同一组的线程在一个时间内只允许有一条线程在执行。

锁对象 只作为线程分组的参照,所以可以是任意对象,可以创建一个 Object 空对象作为锁对象,若多个线程希望分为一组,则执行线程的锁对象必须一样。

 

public class MyThread extends Thread {

    private static Integer length = 100;
    
    // 设定一个静态的对象,用于做线程锁的分组参照
    private static Object object = new Object();

    @Override
    public void run() {

        while (length > 0){
            // 创建线程锁,使多个线程进去代码时,有且仅有一条线程能进入执行
            synchronized (object){ // 通常使用 MyThread.class 作为锁对象
                System.out.println(getName() + ":HelloWorld" + length);
                length--;
            }
        }
    }
}

 

同步方法

若整个方法都需要被加同步锁,不需要在方法中加入 synchronized ,可以使用同步方法的方式,用synchronized关键字修饰方法

 

public class MyThread extends Thread {

    private static Integer length = 100;

    // 设定一个静态的对象,用于做线程锁的分组参照
    private static Object object = new Object();

    @Override
    public void run() {

        while (length > 0) {
            // 把需要同步的代码拆分到一个方法中,该方法的所有代码都需要多线程处理时
            // 则这个代码需要加线程锁,可以声明该方法为线程锁方法
            method();
        }
    }

    /**
     * 定义一个线程锁方法
     * 当这个方法有可能被多个线程同时执行时,有可能出现线程问题,可以声明定义线程锁方法
     * 当方法是对象方法时,该线程锁的锁对象为当前对象 this
     * 当方法是静态方法时,该线程锁的锁对象为 MyThread.class 类
     */
    private synchronized void method() {
        System.out.println(getName() + ":HelloWorld" + length);
        length--;
    }
}

 

Lock 锁

对于 synchronized 线程锁已经为我们自动设定何时加锁何时解锁,但是我们并没有直接看到在哪里加上了锁,又在哪里释方了锁。

为了更清晰的表达如何加锁和释方锁,JDK5以后提供了一个新的锁对象Lock,Lock对象允许我们定义何时加锁,何时解锁。

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作,Lock中提供了获得锁和释放锁的方法

void lock(); 获得锁

void unlock(); 释放锁

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化

Lock lock = new ReentrantLock(); 创建一个ReentrantLock的实例。

 

public class MyThread extends Thread {

    private static Integer length = 100;
    
    // 创建一个Lock线程锁对象,可以手动设置何时加锁何时解锁
    private static Lock lock = new ReentrantLock();

    @Override
    public void run() {

        while (length > 0) {
            // 为下面的代码加锁
            lock.lock();
            System.out.println(getName() + ":HelloWorld" + length);
            length--;
            // 执行完成后解锁
            lock.unlock();
        }
    }
}

 

死锁

死锁是一种错误的做法,当线程锁被嵌套时,嵌套的线程锁就会有可能早就被锁住了,此时线程会卡住,如果互相锁住嵌套锁,则这些线程都因为在等待对放释放锁而无限等待,使得程序卡住。

public class MyDeadlock extends Thread{

    Object objA = new Object();
    Object objB = new Object();
    
    
    @Override
    public void run() {
        while (true){
            if ("线程A".equals(getName())){
                synchronized (objA){
                    System.out.println("线程A拿到了A锁并锁住了");
                    synchronized (objB){
                        System.out.println("线程A拿到了B锁并锁住了");
                    }
                }
            }else if ("线程B".equals(getName())){
                synchronized (objB){
                    System.out.println("线程B拿到了B锁并锁住了");
                    synchronized (objA){
                        System.out.println("线程B拿到了A锁并锁住了");
                    }
                }
            }
        }
    }
}wait()

 

等待唤醒机制

等待唤醒机制是指通过两个进程的等待和唤醒,使两个线程轮流被CPU执行处理。

抽象举例:消费者与生产者

消费者与生产者都是一个线程,具体关系如下:

消费者:

1.当启动时消费者发现生产者未生产完成时,进入等待wait(),使CPU转到生产者处执行

2.当启动消费者发现生产者已完成生产时,直接使用。

3.当消费者使用完成后,通过唤醒notify() 或 notifyAll()其它生产者线程继续生产

生产者:

1.当启动时生产者发现没有生产时,随即马上生产

2.当启动时生产者发现已存在生产时,进入等待wait() ,待消费者完成消费后,消费者唤醒notify() 或 notifyAll()后再次生产

3.当生产完成后,通过唤醒notify() 或 notifyAll()消费者进行消费。

 

日常举例:吃货与厨师

吃货:

1.当启动时CPU先执行吃货,若吃货发现没有食品,则进入等待状态wait() ,使CPU执行厨师

2.当启动时CPU先执行吃货,若吃货发现已存在食品,则进行消费,完成后唤醒notify() 或 notifyAll()厨师

厨师:

1.当启动时CPU先执行厨师,若厨师发现没有食品,则进行生产,生产完成后,通过唤醒notify() 或 notifyAll()吃货。

2.当启动时CPU先执行厨师,若厨师发现已存在食品,则进入等待状态wait() ,使CPU执行吃货。

 

案例代码:

桌子类:

Desk.java
/**
 * 桌子类
 * 用于管理【吃货】与【厨师】之间的线程调度
 * 当桌子上有食物时,应让CPU执行吃货线程以消费
 * 当桌子上没有食物时,应让CPU执行厨师线程以生产
 * 来达到 A->B->A->B... 这样的线程执行规律
 */
public class Desk {
    /**
     * 定义一个Lock对象
     */
    public static Object lock = new Object();

    /**
     * 是一个标志
     * 它用于标记当前轮到那个线程来执行
     * 此处的代表是【是否有食物】.1=有食物,0=没有食物
     * 1=有食物:意味着下一次将引导CPU调用吃货线程进行消费
     * 0=没有食物:意味着下一次将引导CPU调用厨师线程进行生产
     */
    public static Integer foodFlag = 0;

    /**
     * 定义线程执行次数
     * 在【吃货与厨师】案例中,如果吃货吃,厨师煮,将无限执行下去
     * 此处是用于定义其执行次数
     * 设定吃货最多能吃10份,当吃货吃完10份后也不再吃,厨师也不再生产
     */
    public static Integer count = 10;
}

 

吃货类:


public class Footie extends Thread {
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    /**
                     * 当吃货已经吃了10份了,停止吃
                     */
                    break;
                } else {
                    /**
                     * 当吃货未吃完10份
                     * 1.判断桌上是否有食物,如果没有,则等待
                     */
                    if (Desk.foodFlag == 0) {
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {


                        /**
                         * 2.判断桌上是否有食物,如果有,则消费
                         */
                        System.out.println("吃货正在吃一份食物");

                        /**
                         * 3.吃货已完成一份食物
                         */
                        Desk.count--;

                        /**
                         * 4.完成后桌上为没有食物状态
                         */
                        Desk.foodFlag = 0;

                        /**
                         * 5.唤醒厨师进行生产
                         */
                        Desk.lock.notifyAll();


                    }
                }
            }
        }
    }
}

 

厨师类:


public class Cook extends Thread {

    @Override
    public void run() {
        /**
         * 1.循环
         * 2.同步代码块
         * 3.判断共享数据是否到了末尾(到了末尾)
         * 4.判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
         */

        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    /**
                     * 当吃货已吃完10份了,不再生产
                     */
                    break;
                } else {
                    /**
                     * 当吃货未吒完10份时
                     * 1.判断桌上是否有食物,如果有,就等待
                     */
                    if (Desk.foodFlag == 1) {
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        /**
                         * 2.判断桌上是否有食物,如果没有,就生产
                         */
                        System.out.println("厨师生产了一份食物");

                        /**
                         * 3.生产完成后,设定该桌上已有食物
                         */
                        Desk.foodFlag = 1;

                        /**
                         * 4.唤醒所有线程(吃货)进行消费
                         */
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

 

启动线程:

        Cook c = new Cook();
        Footie f = new Footie();
        
        c.start();
        f.start();

 

 

阻塞队列实现等待唤醒机制

我们知道等待唤醒机制即是为了实现 A->B->A->B->A 这样的轮流执行方法。

我们可以使用队列来实现等待唤醒机制,即厨师可以在允许的数量范围内生产食物,而吃货也会在允许的数量范围内消费食物,厨师先生产的食物,会先被吃货消费

ArrayBlockingQueue

数组阻塞队列,用数组的形式对生产者生产的任务进行存储,消费者会以先生产先消费的方式对任务进行一一消费。

数组阻塞队列底层是数组,有界限。

厨师:

public class Cook extends Thread {

    /**
     * 定义一个队列
     * 因为厨师需要取得这个队列,并把生产的食物存放到队列中去
     */
    private ArrayBlockingQueue<String> queue;

    public Cook(ArrayBlockingQueue queue){
        this.queue = queue;
    }

    @Override
    public void run() {
        /**
         * 厨师只管生产食物即可
         * 当厨师生产的食物已存满队列时,厨师才会进入等待状态
         */
        while (true){
            try {
                /**
                 * 厨师生产食物并存入队列中
                 */
                queue.put("食物");
                System.out.println("厨师生产了一份食物");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

 

吃货:

public class Footie extends Thread {

    /**
     * 定义一个队列
     * 因为吃货需要取得这个队列,并取得队列中的食物进行消费
     */
    ArrayBlockingQueue<String> queue;

    public Footie(ArrayBlockingQueue queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                /**
                 * 在队列中取得一个食物
                 */
                String take = queue.take();
                System.out.println("吃货正在吃" + take);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

 

启动线程:

        /**
         * 创建一个数组阻塞队列,其队列中最多只能保存10个生产
         * 1.生产者在同一时间可以最多生产10个任务存在数组中,在不足10个任务以内时生产者无需等待消费者消费完后再进行生产
         * 2.消费者可不需等待生产者生产,只要队列中存在任务,就可以取出队列中的任务进行消费
         */
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
        Cook c = new Cook(queue);
        Footie f = new Footie(queue);
        
        c.start();
        f.start();

注部:在使用队列puttake 时,应尽量避免包裹synchronized ,因为ArrayBlockingQueue 内部已经对puttake进行了线程锁的处理。

 

LinkedBlockingQueue

链表阻塞队列用数组的形式对生产者生产的任务进行存储,消费者会以先生产先消费的方式对任务进行一一消费。

 

线程的状态

 

新建状态(NEW) -- >  创建线程对象

就绪状态(NRUNNABLE) -- >  start方法运行后,到抢到CPU执行权之前的状态

阻塞状态(BLOCKED) -- >  无法获得锁对象,如遇到 synchronized 线程且无法进入时

等待状态(WAITING) -- >  wait方法,除非被其它线程进行notify唤醒,否则会一直等待

计时状态(TIMED_WAITING) -- >  sleep方法,时间结束后,会再次进入就绪状态进行抢CPU执行权

结束状态(TERMINATED) -- >  run方法全部代码运行完毕

注意:在JVM中,是没有【运行】状态的,因为当线程抢到了CPU执行权时,JVM会交给系统管理,此时的状态是在系统上,非JVM上了。

 

线程池

当我们创建一个线程并执行代码后,系统会把执行完成的线程当成垃执,被回收。

如果应用场境中,应用的运行过程中需要时常使用线程,而每一次的使用都重新创建一个线程,使用完后都被销毁,这种做法是非常浪费资源的。

Java 提供了一种线程池技术,它将生成一个容器,用于存储空闲的线程资源,当应用第一次使用线程时,会创建线程并使用,但使用完成后并不会把线程销毁,而是把这个线程交给容器中存储,当应用需要再次使用线程时,不需要创建线程,只需要在线程池中取得空闲线程使用即何,而这个工作都由Java底层完成。

创建线程池

Executors 工具类

线程池的工具类通过调用方法返回不同类型的线程对象

/**
 * 创建一个没有上限的线程池,其上限的线程数为int的最大值
 */
public static ExecutorService newCachedThreadPool();

=> Executors.newCachedThreadPool(); // 创建一个无限线程池

/**
 * 创建一个自定义上限的线程池
 */
public static ExecutorService newFixedThreadPool(int nThreads)

=> Executors.newFixedThreadPool(3); // 创建一个线程数为3的线程池

 

提交任务

创建完成后,可以通过 submit() 方法提交多线程任务到线程池中,线程池底层会自动为我们创建好线程并开始执行。

当执行完成后,线程会被线程池回收。当我们再次需要执行代码时,只需要再次submit() 提交任务即可。

当创建一个有限线程池时,若在该时刻有多个任务,但线程池中线程不足且线程池已满时,多出的任务只能进入等待状态,待线程池中有空闲线程时才会进入执行。

// 提交一个 Callable 多线程任务
submit(Callable<T> task)

// 提交一个带返回值的 Callable 多线程任务
submit(Callable<T> task, T result)

// 提交一个 Runnable 多线程任务
submit(Runnable task)

// 提交一个带返回值的 Runnable 多线程任务
submit(Runnable task, T result)

 

销毁线程池

调用 shutdown() 方法,即可销毁线程池,销毁后,线程池内的所有线程都被销毁。

 

自定义线程池

田Java提供的Executors工具类所提供的线程池非常好用,但缺少了一些自定义的能力,实际上Executors所创建的线程池,是来自于自定义线程池的封装。

创建自定义线程池

使用 ThreadPoolExecutor 类创建一个自定义线程池,它的构造函数中包含7个参数控制,它们分别如下

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

corePoolSize : 创建的核心线程数。

说明:线程池在线程数控制上包含【核心线程数】和【临时线程数】概念,核心线程数会长期储存在线程池中,而临时线程数则不会长期储存。

maximumPoolSize : 总的线程数,必须大于等于核心线程数

说明:由corePoolSize 参数可知,线程池是包含【核心线程数】和【临时线程数】,因此如果总的线程数大于核心线程数,则剩下的线程则为临时线程

keepAliveTime : 临时线程的最长存活时间值,不能少于0

说明:临时线程不会长期存放在线程池当中,如果临时线程超过存活时间后,临时线程会被销毁。

unit  : 临时线程的最长存活时间单位,与keepAliveTime 合用,keepAliveTime 表示值,而unit 为单位(如秒),由TimeUnit 提供值

workQueue : 用于存放线程的容器队列。

说明:存放线程的容器队列,可以是ArrayBlockingQueueLinkedBlockingQueue ,当出现任务需要等待处理时,任务将暂存在队列中

threadFactory :创建线程工厂

说明:初始化线程工厂需要。

handler :任务的拒绝策略

说明:当线程池出现过多任务等待时,会有一个拒绝服务的机制,相关拒绝服务机制请看下文拒绝策略。

 

拒绝策略

当任务数远远超出自定义线程所设定的容量后,会对多出的任务进行拒绝处理,有以下几种情况:

ThreadPoolExecutor.AbortPolicy          // 默认策略:丢弃任务并抛出RejectedExecutionException异常
ThreadPoolExecutor.DiscardPolicy        // 丢弃任务,但是不抛出异常
ThreadPoolExecutor.DiscardOldestPolicy  // 抛弃队列中等待最久的任务,然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy     // 调用任务的 run() 方法绕过线程池直接执行

 

 

线程池任务运行原理

前置条件

核心线程数:3

最多线程数:6 (即临时线程数为3)

临时线程数存活时间值:60

临时线程数存活时间单位:秒

容器队列:3

任务的拒绝策略:丢弃抛出异常

 

 

情况一:有2个任务(核心线程未满)

1.线程池中派出2个核心线程执行业务

2.线程池中空闲1个核心线程

 

情况二:有5个任务(核心线程已满,队列未满)

1.线程池中派出3个核心线程执行业务

2.剩下的2个任务会存在于队列中等待执行。

 

情况三:有8个任务(核心线程和队列已满,临时线程未满)

1.线程池中派出3个核心线程执行业务【任务1、2、3】

2.取出3个任务存放在队列中等待执行【任务4、5、6】

3.创建2个临时线程执行业务【任务7、8】

特别注意:当核心线程被占用完后,并非创建临时线程处理后续任务,而是先填满队列空间,如果队列空间都填满时,才会创建临时线程处理最后的业务

 

情况四:有10个任务(核心、临时、队列都满)

1.线程池中派出3个核心线程执行业务【任务1、2、3】

2.取出3个任务存放在队列中等待执行【任务4、5、6】

3.创建2个临时线程执行业务【任务7、8、9】

4.【任务10】丢弃并抛出异常,拒绝执行。

 

线程数规则

对于定义线程数量时,应该以什么标准去定义多少条线程数为准。

CPU密集型运算

对于CPU密集型的运算,CPU的负荷会比较大,因此建议设置线程数最大为【最大并行数+1】.如【4核8线程】,建议设定线程数为【8+1】

 

I/O密集型运算

对于I/O密集型的运算,CPU的负荷会相对较少,硬盘读写会比较多,其中包含两个操作,一是CPU运算操作,二是硬盘读写操作。其中硬盘读写时,CPU基本在等待状态。

为了最优化读写与运算之间的契合度,可以按照以下公式进行计算应该设置的线程数。

建议线程数=最大并行数 * 期望CPU利用率 *(总运行时间【CPU计算时间+等待时间】 / CPU计算时间)

举例:4核8线程的CPU,期望使用100%运算

假如,CPU运算使用1秒,硬盘操作使用1秒,则最后建议线程池设定线程数为:

建议线程数 = 8 * 100% * ( (1+1) / 1 ) = 16

 

 

如果您喜欢本站,点击这儿不花一分钱捐赠本站

这些信息可能会帮助到你: 下载帮助 | 报毒说明 | 进站必看

修改版本安卓软件,加群提示为修改者自留,非本站信息,注意鉴别

THE END
分享
二维码
打赏
海报
Java – 多线程 – 基础使用
简介 多线程分两个概念,分别是【并发】和【并行】。 并发指的是单个CPU在多个代码段之间进行游走执行,在一个时间段内,多个代码段只有一个能被执行。 并行指的是多个CPU在多个代码段之间同……
<<上一篇
下一篇>>