前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【进击面试_02】Java 多线程

【进击面试_02】Java 多线程

作者头像
Demo_Null
发布2021-03-04 10:20:58
3220
发布2021-03-04 10:20:58
举报
文章被收录于专栏:Java 学习Java 学习

1.1 基本概念

1.1.1 线程与进程

进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

1.1.2 并行与并发

并发:在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。 并行:在多个 CPU 系统中,这些可以并发执行的程序可以分配到多个处理器(CPU)上,实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。

1.1.3 线程创建方式

㈠ 继承 Thread 类创建线程

  Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start( ) 实例方法。start( ) 方法是一个 native 方法,它将启动一个新线程,并执行 run( ) 方法。

代码语言:javascript
复制
public class Test {
    public static void main(String[] args) {
        // 创建自定义线程对象
        MyThread mt = new MyThread();
        // 开启新线程
        mt.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("main 线程 正在执行: " + i);
        }
    }
}

class MyThread extends Thread {
    /**
     * 重写run方法,完成该线程执行的逻辑
     * 该 run( ) 方法的方法体就代表了线程需要完成的任务,因此把 run( ) 方法称为线程执行体。
     */
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("MyThread 线程 正在执行: " + i);
        }
    }
}
㈡ 实现 Runnable 接口创建线程

  Java 类只支持单继承,若类已经继承了其他类,我们可以通过实现 Runnable 接口,使该类具有多线程类的特征。run( ) 方法是多线程程序的一个执行目标。所有的多线程代码都在 run( ) 方法里面。Thread 类实际上也是实现了 Runnable 接口的类。   在启动的多线程的时候,需要先通过 Thread 类的构造方法 Thread(Runnable target) 构造出对象,然后调用 Thread 对象的 start( ) 方法来运行多线程代码。实际上所有的多线程代码都是通过运行 Thread 的 start( ) 方法来运行的。因此,不管是继承 Thread 类还是实现 Runnable 接口来实现多线程,最终还是通过 Thread 的对象的 API 来控制线程。

代码语言:javascript
复制
public class Test {
    public static void main(String[] args) {
        // 通过匿名内部类创建 Runnable 实现类对象
        Runnable myRunnable = new Runnable() {
		    @Override
		    public void run() {
		        for (int i = 0; i < 100; i++) {
		            System.out.println(Thread.currentThread().getName() + " 正在执行: " + i);
		        }
		    }
		};

		// 通过 MyRunnable 创建线程
        Thread thread = new Thread(myRunnable);
        thread.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("main 线程 正在执行: " + i);
        }
    }
}
㈢ Callable 和 Future 返回线程

  Java 提供了 Callable 接口,该接口像是 Runnable 接口的增强版,Callable 接口有一个 call( ) 方法可以作为线程执行体,但 call( ) 方法比 run( ) 方法功能更强大。    ♘ call( ) 方法可以有返回值。    ♘ call( ) 方法可以声明抛出异常。 我们希望可以提供一个 Callable 对象作为 Thread 的 target,而该线程的线程执行体就是该 Callable 对象的 call( ) 方法。但是 Callable 它不是 Runnable 接口的子接口,所以 Callable 对象不能直接作为 Thread 的 target。   Java 为我们提供了 Future 接口来解决这个问题,使用 Future 接口来代表 Callable 接口里 call( ) 方法的返回值,并为 Future 接口提供了一个 FutureTask 实现类,该实现类实现了 Future 接口,并实现了 Runnable 接口可以作为 Thread 类的 target。

代码语言:javascript
复制
public class Test {

    public static void main(String[] args) {

        // 创建 Callable 匿名内部类对象
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + "---" + i);
                }
                return "我执行完了";
            }
        };

        // 使用 FutureTask 来包装 Callable
        FutureTask<String> futureTask = new FutureTask<>(callable);

        // 通过 FutureTask 创建线程
        Thread thread = new Thread(futureTask, "我是通过 Callable 接口创建的线程");
        thread.start();

        try {
            // 打印线程返回值
            System.out.println(futureTask.get());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

1.1.4 线程状态

① new(新建):当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值,但是并未启动。 ② Runnable(可运行):当线程对象调用了 start( ) 方法之后,该线程处于可运行状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。简单来说就是线程可以运行但是没有 CPU 执行权。 ③ Running(运行):可运行状态的线程获得了 CPU 时间片,执行程序代码。 ④ Blocked(锁阻塞):阻塞状态是指线程因为某种原因放弃了 CPU 使用权,也即让出了 CPU 时间片,暂时停止运行。直到线程进入可运行状态,才有 机会再次获得 CPU 时间片转到运行状态。阻塞的情况分三种: 等待阻塞:运行( running )的线程执行 o.wait( ) 方法, JVM 会把该线程放 入等待队列( waitting queue )中。 同步阻塞:运行( running )的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。 其他阻塞: 运行( running )的线程执行 Thread.sleep( long ms ) 或 t.join( ) 方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep( ) 状态超时、 join( ) 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入可运行( runnable )状态。 ⑤ Dead(终止):线程执行结束,死亡的线程不可复生。

在这里插入图片描述
在这里插入图片描述

1.1.5 线程方法

方法

说明

wait

作用是让当前线程进入等待状态,只有等待另外线程的通知或被中断才会返回,同时,会让当前线程释放它所持有的锁。因此,wait() 一般用在同步方法或同步代码块中。

sleep

作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。与 wait() 不同的是 sleep() 不会释放当前占有的锁,在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待 cpu 的调度执行。常用来暂停程序的运行。

yield

作用是让当前线程暂停,但不会阻塞该线程,而是由“运行状态”进入到“就绪状态”,从而让 其它具有相同优先级的等待线程获取执行权;但是,有的操作系统对线程优先级并不敏感,所以并不能保证在当前线程调用 yield() 之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是 当前线程又进入到“运行状态”继续运行。

interrupt

作用是中断本线程,方法被调用时,会立即将线程的中断标志设置为“true”。这个线程本身并不会因此而改变状态(如阻塞,终止等)。所以我们可以不断通过 isInterrupted() 来检测中断标记,从而在调用了 interrupt() 后终止线程,这也是通常我们对 interrupt() 的用法。

join

即当前线程内,用某个线程对象调用join()后,会使当前线程等待,直到该线程对象的线程运行完毕,原线程才会继续运行。很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法。

notify

Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的。

1.1.6 Synchronized 与 ReentrantLock 比较

Synchronized

ReentrantLock

原始构成

JVM 层面

API 层面

底层实现

同步阻塞,并发悲观策略

同步非阻塞,并发乐观策略

使用方法

隐式获得/释放锁

显式获得/释放锁

是否可中断

不可中断

可中断

是否公平锁

非公平锁

可实现公平锁

条件唤醒

不可

可绑定多个 Condition

1.2 阻塞队列

1.2.1 简介

☞ 什么是阻塞队列 顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下   ♞ 当阻塞队列是空时,从队列中获取元素的操作会被阻塞。   ♞ 当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。   多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?   理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的 concurrent 包横空出世了,而他也给我们带来了强大的 BlockingQueue。

☞ 阻塞队列的种类

阻塞队列

说明

ArrayBlockingQueue

由数组构成的有界阻塞队列

LinkedBlockingQueue

由链表构成的有界阻塞队列,最大为 Integer.MAX_VALUE,基本可以当作无界队列了

SynchronousQueue

是一个不存储元素的阻塞队列,也即单个元素的队列,消费者想要消费时,生产者才会生产,否则阻塞

PriorityBlockingQueue

支持优先级排序的无界阻塞队列

DelayedQueue

使用优先级队列实现的延迟无界阻塞队列

LinkedTransferQueue

由链表构成的无界阻塞队列

LinkedBlockingDeque

由链表构成的双向阻塞队列

1.2.2 API

☞ 常用 API

方法类型

抛出异常

特殊值

阻塞

超时

插入

add(e)

offer(e)

put(e)

offer(e,time,unit)

移除

remove()

poll()

take()

poll(time,unit)

检查

element()

peek()

不可用

不可用

☞ 抛出异常  ♞ 当阻塞队列满时,再往队列里 add 插入元素会抛 lllegalStateException:Queue full  ♞ 当阻塞队列空时,再往队列里 remove 移除元素会抛 NoSuchElementException

☞ 特殊值  ♞ 插入方法,成功 ture 失败 false  ♞ 移除方法,成功返回出队列的元素,队列里面没有就返回 null

☞ 一直阻塞  ♞ 当阻塞队列满时,生产者线程继续往队列里 put 元素,队列会一直阻塞生产线程直到 put 数据 or 响应中断退出。  ♞ 当阻塞队列空时,消费者线程试图从队列里 take 元素,队列会一直阻塞消费者线程直到队列可用。

☞ 超时退出  ♞ 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过后限时后生产者线程会退出

1.2.3 示例

☞ 传统方法版

代码语言:javascript
复制
/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/26
 * @desc 资源类
 */
public class MyData {
    private Integer num = 0;
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void add() {
        lock.lock();
        try {
            // 使用 while 避免虚假唤醒
            while (0 != num) {
                // 不为 0, 等待 sub
                condition.await();
            }

            // 生产
            num++;
            System.out.println(Thread.currentThread().getName() + " 修改 num = " + num);

            // 唤醒 sub
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void sub() {
        lock.lock();
        try {
            // 使用 while 避免虚假唤醒
            while (0 == num) {
                // 不为 0, 等待 add
                condition.await();
            }

            // 消费
            num--;
            System.out.println(Thread.currentThread().getName() + " 修改 num = " + num);

            // 唤醒 sub
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
}
代码语言:javascript
复制
/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/26
 * @desc 两线程操作 num, 一增一减交替执行
 */
public class BlockingQueueDemo {

    public static void main(String[] args) {
        MyData myData = new MyData();

        new Thread(() -> {
            for (int i = 0; i < 6; i++) {
                myData.add();
            }
        }, "A").start();


        new Thread(() -> {
            for (int i = 0; i < 6; i++) {
                myData.sub();
            }
        }, "B").start();
    }
}
在这里插入图片描述
在这里插入图片描述

  消费者有则消费,无则等待生产者生产;生产者无则生产,有责等待消费者消费。 哎!有木有觉得这个案例非常像容量为 1 的阻塞队列。这里可以升级一下,N 个生产者生产,M 个消费者消费,模拟一下消息队列。

☞ 阻塞队列版

代码语言:javascript
复制
/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/26
 * @desc 资源类
 */
public class MyData {
    private AtomicInteger atomicInteger = new AtomicInteger();
    BlockingQueue<Integer> blockingQueue = null;

    public MyData(BlockingQueue<Integer> blockingQueue) {
        this.blockingQueue = blockingQueue;
    }

    public void add() {

        try {
            // 产生数据
            int i = new Random().nextInt(100);
            // 加 synchronized 是为了保证下面代码不被加塞
            synchronized (this) {
                // 加入队列
                boolean offer = blockingQueue.offer(i, 2L, TimeUnit.SECONDS);
                String name = Thread.currentThread().getName();
                System.out.println((offer ? i : "阻塞") + " >>> " + name + "\t" +blockingQueue);
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void sub() {
        try {
            // 加 synchronized 是为了保证下面代码不被加塞
            synchronized (this) {
                Integer poll = blockingQueue.poll(2L, TimeUnit.SECONDS);
                String name = Thread.currentThread().getName();
                System.out.println((null == poll ? "阻塞" : poll) + " <<< " + name + "\t" +blockingQueue);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
代码语言:javascript
复制
/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/26
 * @desc 模拟消息队列
 */
public class BlockingQueueDemo {

    public static void main(String[] args) {
        MyData myData = new MyData(new ArrayBlockingQueue<>(2));

                for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                for (int j = 0; j < 2; j++) {
                    myData.add();
                    try { Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); }
                }
            }, "A-" + i).start();
            try { Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); }
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        new Thread(() -> {
            for (int j = 0; j < 6; j++) {
                myData.sub();
                try { Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); }
            }
        }, "B-0").start();

    }
}
在这里插入图片描述
在这里插入图片描述

1.3 线程池

1.3.1 简介

☞ 线程池是什么   一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络 sockets 等的数量。

☞ 线程池的优点  ♞ 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。  ♞ 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。  ♞ 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

☞ 线程池的种类

线程池

说明

newCachedThreadPool

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统或者说 JVM 能够创建的最大线程大小。

newFixedThreadPool

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

newScheduledThreadPool

创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

newWorkStealingPool

JDK 1.8 后引入的,它是新的线程池类 ForkJoinPool 的扩展,能够合理的使用 CPU,进行并发运行任务。可以了解为,工作量一样,A、B 同时开发,谁开发的快,谁就多做一些,能者多劳

1.3.2 线程池原理

  线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。

在这里插入图片描述
在这里插入图片描述

☞ 线程复用   每一个 Thread 的类都有一个 start 方法。当调用 start 启动线程时 Java 虚拟机会调用该类的 run 方法。那么该类的 run 方法中就是调用了 Runnable 对象的 run 方法。我们可以继承重写 Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。

☞ 线程池的参数

在这里插入图片描述
在这里插入图片描述

参数

说明

int corePoolSize

线程池核心线程大小

int maximumPoolSize

线程池最大线程数量

long keepAliveTime

空闲线程存活时间

TimeUnit unit

空间线程存活时间单位

BlockingQueue workQueue

工作队列

ThreadFactory threadFactory

线程工厂

RejectedExecutionHandler handler

拒绝策略

☞ 工作过程 ① 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。 ② 当调用 execute 方法添加一个任务时,线程池会做如下判断:  ♞ 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务  ♞ 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列  ♞ 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务  ♞ 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。 ③ 当一个线程完成任务时,它会从队列中取下一个任务来执行。 ④ 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

☞ 拒绝策略   线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。JDK 内置的拒绝策略如下:  ♞ AbortPolicy:默认策略,直接抛出异常,阻止系统正常运行。  ♞ CallerRunsPolicy:只要线程池未关闭,该策略直接将任务交给调用者线程执行。  ♞ DiscardOldestPolicy:丢弃最早的一个任务,并尝试再次提交当前任务。  ♞ DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理。

1.3.3 阿里并发规约

在这里插入图片描述
在这里插入图片描述

1.3.4 手撸线程池

代码语言:javascript
复制
/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/26
 * @desc 自定义线程池
 */
public class ThreadPoolDemo {

    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        try {
        	// 最大支持 maximumPoolSize 加 阻塞队列大小,超过则使用拒绝策略
            for (int i = 0; i < 8; i++) {
                threadPoolExecutor.execute(() -> {
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPoolExecutor.shutdown();
        }
    }
}
在这里插入图片描述
在这里插入图片描述

1.3.5 死锁定位

☞ 先写个死锁

代码语言:javascript
复制
/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/26
 * @desc //TODO
 */
public class MyThread implements Runnable {
    private String lockA;
    private String lockB;

    public MyThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + " 已有 " + lockA + " 尝试获取 " + lockB);

            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + " 已有 " + lockB + " 尝试获取 " + lockA);
            }
        }
    }
}
代码语言:javascript
复制
/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/26
 * @desc 死锁
 */
public class DeadLockDemo {

    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";

        new Thread(new MyThread(lockA, lockB), "A").start();
        new Thread(new MyThread(lockB, lockA), "B").start();

    }
}
在这里插入图片描述
在这里插入图片描述

☞ 再了解两个命令

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

合理配置线程数 a. CPU 密集型   CPU 密集的意思是该任务需要大量的运算,而没有阻塞,CPU 一直全速运行。所以 CPU 密集型任务配置尽可能少的线程数量,一般为 CPU 核数 + 1,Runtime.getRuntime().availableProcessors() 获取当前主机 CPU 核数。 b. IO 密集型 方案一:CPU 核数 * 2 方案二:CPU 核数 / (1- 阻塞系数【0.8 ~ 0.9】)

1.4 调度算法

1.4.1 线程调度

☞ 抢占式调度   抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。

☞ 协同式调度   协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

☞ JVM 中的实现   Java 使用的线程调使用抢占式调度,Java 中线程会按优先级分配 CPU 时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。

1.4.2 进程调度

☞ 优先调度算法

㈠ 先来先服务(FCFS)   当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用FCFS 算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机,特点是:算法比较简单,可以实现基本上的公平。

㈡ 短作业/进程优先   短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度。该算法未照顾紧迫型作业。

☞ 高优先权优先

  为了照顾紧迫型作业,使之在进入系统后便获得优先处理,引入了最高优先权(FPF)优先调度算法。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法时把处理机分配给就绪队列中优先权最高的进程。

㈠ 非抢占式优先权算法   在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时。这种调度算法主要用于批处理系统中,也可用于某些对实时性要求不严的实时系统中。

㈡ 抢占式优先权调度算法   在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于比较严格的实时系统中,以及对性能要求较高的批处理系统和分时系统中。

㈢ 高响应比优先调度算法   高响应比优先调度算法:在批处理系统中,短作业优先算法是一种比较好的算法,其主要的不足之处是长作业的运行得不到保证。如果我们能为每个作业引入前面所述的动态优先权,并使作业的优先级随着等待时间的增加而以速率a提高,则长作业在等待一定时间后,必然有机会分配到处理机。该优先权的变化规律可描述为:

在这里插入图片描述
在这里插入图片描述

 ♞ 如果作业地等待时间相同,则要求服务的时间愈短,其优先权愈高,因而该算法有利于短作业。  ♞ 当要求服务的时间相同时,作业的优先权决定与其等待时间,等待时间越长,其优先权越高,因而它实现的是先来先服务。  ♞ 对于长作业,作业的优先级可以随等待时间的增加而增加,当其等待时间足够长时,其优先级便可升到很高,从而也可获得处理机。该算法既照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务。因此,该算法实现了一种较好的折中。当然,在利用该算法时,每要进行调度之前按,都须先做响应比的计算,这会增加系统开销。

☞ 基于时间片的轮转调度

㈠ 时间片轮转法   在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU分配给队首进程,并令其执行一个时间片。时间片的大小从几ms到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均获得一时间片的处理机执行时间。

㈡ 多级反馈队列调度算法   ♞ 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权越高的队列中,为每个进程所规定的执行时间片就越小。例如,第二队列的时间片要比第一个队列的时间片长一倍,…,第 i+1 个队列的时间片要比 i 个队列的时间片长一倍。   ♞ 当一个新进程进入内存后,首先将它放入第一队列的末尾,按 FCFS 原则排队等待调度。当轮到该进程执行时,如果它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾。再同样地按 FCFS 原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,…,如此下去,当一个长作业从第一队列依次降到第 n 队列后,在第 n 队列便采取按时间片轮转的方式运行。   ♞ 仅当第一队列空闲时,调度程序下才调度第二队列中的进程运行;仅当第 1 ~ (i-1) 队列均空时,才会调度第i队列中的进程运行。如果处理机正在第 i 队列中为某进程服务时,又有新进程进入优先权较高的队列(第 1 ~ (i-1) 中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。   在多级反馈队列调度算法中,如果规定第一个队列的时间片略大于多数人机交互所需之处理时间时,便能够较好的满足各种类型用户的需要。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-02-26 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.1 基本概念
    • 1.1.1 线程与进程
      • 1.1.2 并行与并发
        • 1.1.3 线程创建方式
          • ㈠ 继承 Thread 类创建线程
          • ㈡ 实现 Runnable 接口创建线程
          • ㈢ Callable 和 Future 返回线程
        • 1.1.4 线程状态
          • 1.1.5 线程方法
            • 1.1.6 Synchronized 与 ReentrantLock 比较
            • 1.2 阻塞队列
              • 1.2.1 简介
                • 1.2.2 API
                  • 1.2.3 示例
                  • 1.3 线程池
                    • 1.3.1 简介
                      • 1.3.2 线程池原理
                        • 1.3.3 阿里并发规约
                          • 1.3.4 手撸线程池
                            • 1.3.5 死锁定位
                            • 1.4 调度算法
                              • 1.4.1 线程调度
                                • 1.4.2 进程调度
                                  • ☞ 优先调度算法
                                  • ☞ 高优先权优先
                                  • ☞ 基于时间片的轮转调度
                              相关产品与服务
                              领券
                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档