多线程基础知识篇

336

进程与线程

最初的计算机只能接受一些特定的指令,用户每输入一个指令,计算机就做出一个操作。当用户在思考或者输入时,计算机就在等待。这样效率非常低下,在很多时候,计算机都处在等待状态。

后来有了批处理操作系统,把一系列需要操作的指令写下来,形成一个清单,一次性交给计算机。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。

批处理操作系统在一定程度上提高了计算机的效率,但是由于批处理操作系统的指令运行方式仍然是串行的,内存中始终只有一个程序在运行,后面的程序需要等待前面的程序执行完成后才能开始执行,而前面的程序有时会由于I/O操作、网络等原因阻塞,所以批处理操作效率也不高

人们对于计算机的性能要求越来越高,现有的批处理操作系统并不能满足人们的需求,而批处理操作系统的瓶颈在于内存中只存在一个程序,那么内存中能不能存在多个程序呢?这是人们亟待解决的问题。

于是,科学家们提出了进程的概念。

进程就是应用程序在内存中分配的空间,也就是正在运行的程序,各个进程之间互不干扰。同时进程保存着程序每一个时刻运行的状态。

此时,CPU采用时间片轮转的方式运行进程:CPU为每个进程分配一个时间段,称作它的时间片。如果在时间片结束时进程还在运行,则暂停这个进程的运行,并且CPU分配给另一个进程(这个过程叫做上下文切换)。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完。

当进程暂停时,它会保存当前进程的状态(进程标识,进程使用的资源等),在下一次切换回来时根据之前保存的状态进行恢复,接着继续执行。

使用进程+CPU时间片轮转方式的操作系统,在宏观上看起来同一时间段执行多个任务,换句话说,进程让操作系统的并发成为了可能。虽然并发从宏观上看有多个任务在执行,但在事实上,对于单核CPU来说,任意具体时刻都只有一个任务在占用CPU资源。

虽然进程的出现,使得操作系统的性能大大提升,但是随着时间的推移,人们并不满足一个进程在一段时间只能做一件事情,如果一个进程有多个子任务时,只能逐个得执行这些子任务,很影响效率。

那么能不能让这些子任务同时执行呢?于是人们又提出了线程的概念,让一个线程执行一个子任务,这样一个进程就包含了多个线程,每个线程负责一个单独的子任务。

总之,进程和线程的提出极大的提高了操作系统的性能。进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。

多进程方式确实可以实现并发,但使用多线程,有以下几个好处:

  • 进程间的通信比较复杂,而线程间的通信比较简单,通常情况下,我们需要使用共享资源,这些资源在线程间的通信比较容易。
  • 进程是重量级的,而线程是轻量级的,故多线程方式的系统开销更小。

进程和线程的区别

进程是一个独立的运行环境,而线程是在进程中执行的一个任务。

他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O)

  • 进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
  • 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
  • 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。

另外一个重要区别是,进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位 。

上下文切换

上下文切换(有时也称做进程切换或任务切换)是指 CPU 从一个进程(或线程)切换到另一个进程(或线程)。上下文是指某一时间点 CPU 寄存器和程序计数器的内容。

  • 寄存器是cpu内部的少量的速度很快的闪存,通常存储和访问计算过程的中间值提高计算机程序的运行速度。

  • 程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体实现依赖于特定的系统。

CPU通过为每个线程分配CPU时间片来实现多线程机制。CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。

但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的,意味着此操作会消耗大量的 CPU 时间,故线程也不是越多越好。如何减少系统中上下文切换次数,是提升多线程性能的一个重点课题。

线程的创建

1. 实现 Runnable 接口

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("我是实现Runnable接口。。");
    }

    public static void main(String[] args) {
        MyRunnable myRunner = new MyRunnable();
        Thread thread = new Thread(myRunner);
        thread.start();
    }
}

2. 继承 Thread

public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("我是继承Thread类。。");
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

3. 实现 Callable 接口

public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        return 123;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
    }
}

4. 通过线程池创建线程

看过《阿里Java开发手册》的你,应该知道

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

4. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这
样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 
2) CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

ThreadPoolExecutor机制

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

下面是ThreadPoolExecutor最核心的构造方法参数:

  1. corePoolSize 核心线程池的大小;
  2. maximumPoolSize 最大线程池大小;
  3. keepAliveTime 线程池中超过corePoolSize数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true)使得核心线程超出有效时间也关闭;
  4. TimeUnit keepAliveTime的时间单位;
  5. workQueue阻塞任务队列;
  6. threadFactory新建线程工厂;
  7. RejectedExecutionHandler当提交任务数超过maximumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理;

重点讲解

corePoolSizemaximumPoolSizeworkQueue三者之间的关系:

  1. 当线程池小于corePoolSize时,新提交的任务会创建一个新线程执行任务,即使线程池中仍有空闲线程。
  2. 当线程池达到corePoolSize时,新提交的任务将被放在workQueue中,等待线程池中的任务执行完毕。
  3. workQueue满了,并且maximumPoolSize > corePoolSize时,新提交任务会创建新的线程执行任务。
  4. 当提交任务数超过maximumPoolSize,新任务就交给RejectedExecutionHandler来处理。
  5. 当线程池中超过 corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程。
  6. 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭。

线程的生命周期

  1. NEW:初始状态,线程被构建,但是还没有调用 start 方法。
  2. RUNNABLED:运行状态,JAVA 线程把操作系统中的就绪和运行两种状态统一称为“运行中”。调用线程的 start() 方法使线程进入就绪状态。
  3. BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了 CPU 使用权。比如访问 synchronized 关键字修饰的方法,没有获得对象锁。
  4. Waiting :等待状态,比如调用wait()方法。
  5. TIME_WAITING:超时等待状态,超时以后自动返回。比如调用 sleep(long millis) 方法
  6. TERMINATED:终止状态,表示当前线程执行完毕。

看下源码:

public enum State {          
    NEW,     
    RUNNABLE,       
    BLOCKED,             
    WAITING,           
    TIMED_WAITING,       
    TERMINATED; 
} 

线程的优先级

  1. 线程的最小优先级:1
  2. 线程的最大优先级:10
  3. 线程的默认优先级:5
  4. 通过调用 getPriority()setPriority(int newPriority) 方法来获得和设置线程的优先级

看下源码:

/**
 * The minimum priority that a thread can have.
 */
public final static int MIN_PRIORITY = 1;

/**
 * The default priority that is assigned to a thread.
 */
public final static int NORM_PRIORITY = 5;

/**
 * The maximum priority that a thread can have.
 */
public final static int MAX_PRIORITY = 10;

看下代码:

public class MyThread extends Thread {

    public static void main(String[] args) {
        MyThread a = new MyThread();
        System.out.println(a.getPriority());//5
        a.setPriority(8);
        System.out.println(a.getPriority());//8
    }
}

特性:

  1. 继承性:比如A线程启动B线程,则B线程的优先级与A是一样的。

  2. 规则性:高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。

  3. 随机性:优先级较高的线程不一定每一次都先执行完。

线程的停止

  1. stop() 方法,这个方法已经标记为过时了,强制停止线程,相当于kill -9。
  2. interrupt() 方法,优雅的停止线程。告诉线程可以停止了,至于线程什么时候停止,取决于线程自身。

看下停止线程的代码:

public class InterruptDemo {
    private static int i ;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            //默认情况下isInterrupted 返回 false、通过 thread.interrupt 变成了 true
            while (!Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println("Num:" + i);
        }, "interruptDemo");
        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt(); //不加这句,thread线程不会停止
    }
}

看上面这段代码,主线程main方法调用 thread线程的 interrupt() 方法,就是告诉 thread 线程,你可以停止了(其实是将 thread 线程的一个属性设置为了true),然后 thread 线程通过 isInterrupted() 方法获取这个属性来判断是否设置为了true。

小结

以上介绍了一些java线程的一些基本知识,掌握了进程与线程的概念以及线程的创建、生命周期、停止等概念,后面我们再来详细聊聊java并发相关的知识。