并发[0]-基础篇

173 阅读10分钟

1 进程 & 线程

1.1 背景

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

批处理操作系统

把一系列的指令写下来,形成一个清单一次性交给计算机,一次性交给计算机。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。 批处理系统一定程度上提高计算机的效率,但是指令执行仍然是串行执行,一个指令须等待上一个指令执行完才能开始执行,其中可能有IO、网络等原因阻塞,导致后续指令全部等待影响效率。

1.2 进程

为解决批处理系统的问题,提出进程的概念,把一系列的的计算任务组装在一起形成一个进程,操作系统分配独立的地址空间,每个个进程相互之间独立,操作系统采用进程+时间片轮转的方式执行,在某个进程阻塞时,可以切换到另一个进程执行,实现让多个进程可以在一段时间内并发执行,但事实上对于单核CPU来说,同一时间只有一个任务在执行。

进程:是程序执行的一次过程,是操作系统分配和管理资源的基本单位,拥有独立的地址空间,多个进程相互之间独立。 时间片轮转:CPU为每一个进程分配一个时间段,称为时间片,在该时间片内执行该进程的计算任务,如果时间片内未执行完,则暂停这个进程的运行,并且CPU分配给另一个进程(这个过程叫做上下文切换)。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完。

1.3 线程

虽然进程的出现,使得操作系统的性能大大提升,但是一个进程在一段时间只能做一件事情,如果一个进程有多个子任务时,只能逐个得执行这些子任务,很影响效率。进而提出线程,将进程内多个子任务分派给子线程去执行,操作系统基于线程任务去分配CPU执行。

线程:进程内一个子任务的执行路径,线程是CPU调度和分派的基本单位,共享进程的地址空间。

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

1.4 进程和线程的区别

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

1.5 上下文切换

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

程序计数器:用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置. 寄存器:cpu内部的少量的速度很快的闪存,保存计算的缓存数据 当发生上下文切换时需要保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态

上下文切换通常是计算密集型的,意味着此操作会消耗大量的CPU时间,故线程也不是越多越好。

2 Thread & Runnable

2.1 线程的创建

public class ThreadTest {
    public static void main(String[] args) {
        //实现Runnable的方式
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        },"thread1");
        //使用继承Thread的方式
        new Thread("thread2"){
            @Override
            public void run(){
                System.out.println(Thread.currentThread().getName());
            }
        };
        thread.start();//启动线程
    }
}

通过继承Thread或者实现Runnable的方式创建了一个线程,调用start()方法通知线程调度器,该线程已经就绪,可以执行,等待系统时间片。

2.2 线程的状态

public enum State {
    NEW,//初始状态,创建线程未执行start()方法
    RUNNABLE,//运行状态
    BLOCKED,//阻塞状态,处于BLOCKED状态的线程正等待锁的释放以进入同步区。
    WAITING,//等待状态,处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒
    TIMED_WAITING,//超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。
    TERMINATED;//终止状态。此时线程已执行完毕。
}

WAITING状态,会在掉调用以下几个方法后进入该状态:

  • Object.wait():需调用notify唤醒;
  • Thread.join():等待线程执行完毕,底层调用的是Object实例的wait方法;
  • LockSupport.park():尝试获取许可因子,只有unpark释放许可因子才能唤醒
2.2.1 状态流转图

2.3 Thread的几个方法

  • start():启动一个线程,当调用该方法后,相应线程就会进入就绪状态,该线程中的run()方法会在某个时机被调用。
  • run():普通用户方法,线程获得时间片后会执行Thread类run方法。
  • join():父线程中调用join()方法后会等待子线程执行完成或一定时间后继续执行,
  • yield():静态方法,当前线程放弃CPU,重新进入就绪状态,但是不确定具体放弃时间,也还有可能继续运行。
  • sleep():静态方法,当前线程休眠一点时间,期间不会放弃锁及CPU;
  • currentThread():静态方法,返回对当前正在执行的线程对象的引用;
  • stop():释放占用的全部锁并抛出ThreadDeath异常,不安全。
2.3.1 start()

start方法:启动一个线程,同步方法,重复调用start(),抛出IllegalThreadStateException异常。

Thread内部维护了threadStatus的变量,初始值为0,如果它不等于0,调用start()是会直接抛出异常的。

查看当前线程状态的源码:

// Thread.getState方法源码:
public State getState() {
    // get current thread state
    return sun.misc.VM.toThreadState(threadStatus);
}

// sun.misc.VM 源码:
public static State toThreadState(int var0) {
    if ((var0 & 4) != 0) {
        return State.RUNNABLE;
    } else if ((var0 & 1024) != 0) {
        return State.BLOCKED;
    } else if ((var0 & 16) != 0) {
        return State.WAITING;
    } else if ((var0 & 32) != 0) {
        return State.TIMED_WAITING;
    } else if ((var0 & 2) != 0) {
        return State.TERMINATED;
    } else {
        return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
    }
}

可以看出TERMINATED状态的线程状态为2,说明线程执行完成后在调用start()也同样会抛出IllegalThreadStateException异常。

2.3.2 join()方法

调用join()方法不会释放父进程锁,会一直等待当前线程执行完毕(转换为TERMINATED状态)。源码方法使用wait()的方法实现等待,默认等待子线程执行完成,也可指定等待时间。

2.3.3 线程中断方法
  • interrupt():中断线程。这里的中断线程并不会立即停止线程,而是设置线程的中断状态为true(默认是flase),阻塞中的线程会抛出InterruptedException;
  • isInterrupted():测试当前线程是否被中断。线程的中断状态受这个方法的影响,默认调用后会清除中断标志;
  • interrupted():测试当前线程是否被中断,不会清除中断标志

在线程中断机制里,当其他线程通知需要被中断的线程后,线程中断的状态被设置为true,但是具体被要求中断的线程要怎么处理,完全由被中断线程自己而定,可以在合适的实际处理中断请求,也可以完全不处理继续执行下去。

2.4 线程优先级

Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都支持10级优先级的划分(比如有些操作系统只支持3级划分:低,中,高),Java只是给操作系统一个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。 Java默认的线程优先级为5,线程的执行顺序由调度程序来决定,线程的优先级会在线程被调用之前设定。 通常情况下,高优先级的线程将会比低优先级的线程有更高的几率得到执行。我们使用方法Thread类的setPriority()实例方法来设定线程的优先级。

public static final int MIN_PRIORITY = 1; 
public static final int NORM_PRIORITY = 5; 
public static final int MAX_PRIORITY = 10;

设置优先级>10|<1,则会抛出IllegalArgumentException异常 线程优先级具有继承性,A和A的子线程B的优先级是一样的。 如果某个线程优先级大于线程所在线程组的最大优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级。

2.5 守护线程

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) ; 只要当前JVM实例中存在任何一个非守护线程没有结束,守护线程就在工作;只有当最后一个非守护线程结束时,守护线程才随着JVM一同结束工作。 Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。

3 ThreadGroup线程组

Java中用ThreadGroup来表示线程组,我们可以使用线程组对线程进行批量控制。 每个Thread必然存在于一个ThreadGroup中,Thread不能独立于ThreadGroup存在。执行main()方法线程的名字是main,如果在new Thread时没有显式指定,那么默认将父线程(当前执行new Thread的线程)线程组设置为自己的线程组。

3.1 线程组的数据结构

public class ThreadGroup implements Thread.UncaughtExceptionHandler {
    private final ThreadGroup parent; // 父亲ThreadGroup
    String name; // ThreadGroupr 的名称
    int maxPriority; // 线程最大优先级
    boolean destroyed; // 是否被销毁
    boolean daemon; // 是否守护线程
    boolean vmAllowSuspension; // 是否可以中断

    int nUnstartedThreads = 0; // 还未启动的线程
    int nthreads; // ThreadGroup中线程数目
    Thread threads[]; // ThreadGroup中的线程

    int ngroups; // 线程组数目
    ThreadGroup groups[]; // 线程组数组
}

总结来说,线程组是一个树状的结构,每个线程组下面可以有多个线程或者线程组。线程组可以起到统一控制线程的优先级和检查线程的权限的作用。

3.2 线程组的常用方法

3.2.1 获取当前的线程组名字
Thread.currentThread().getThreadGroup().getName()
3.2.2 统一的异常处理
public static void main(String[] args) {
    ThreadGroup threadGroup1 = new ThreadGroup("group1") {
        // 继承ThreadGroup并重新定义以下方法
        // 在线程成员抛出unchecked exception
        // 会执行此方法
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println(t.getName() + ": " + e.getMessage());
        }
    };

    // 这个线程是threadGroup1的一员
    Thread thread1 = new Thread(threadGroup1, new Runnable() {
        public void run() {
            // 抛出unchecked异常
            throw new RuntimeException("测试异常");
        }
    });

    thread1.start();
}
3.2.2 复制线程组
// 复制一个线程数组到一个线程组
Thread[] threads = new Thread[threadGroup.activeCount()];
TheadGroup threadGroup = new ThreadGroup();
threadGroup.enumerate(threads);

4 总结

进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。 Java的线程调度采用抢占式调度,每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定,如Thread::yield()方法可以主动让出执行时间,但是如果想要主动获取执行时间,线程本身是没有什么办法的。