多线程与并发基础


进程(process) 概念:程序的一次执行过程,或是正在运行的一个程序。 说明:进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。

线程(thread) 概念:进程可进一步细化为线程,是一个程序内部的一条执行路径。 说明:线程作为CPU调度和执行的单位,每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小。

协程(Coroutine) 概念: 协程是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

进程可以细化为多个线程。 每个线程拥有自己独立的:本地方法栈、虚拟机栈、程序计数器。多个线程间共享同一个进程中的:方法区、堆。


一、什么是多进程

进程(process)的概念在上面已经提到了,进程是由一个或多个线程构成的。当运行一个程序时,就启动了一个进程。凡是用于完成操作系统的各种功能的进程就是系统进程,而所有由你启动的进程都是用户进程。

同理,多进程就是指计算机同时执行多个进程,一般是同时运行多个软件。

二、什么是多线程

多线程是指一个进程中同时有多个线程正在执行。

为什么要使用多线程

  • 在一个程序中,有许多操作是非常耗时的,如数据库读写操作、IO操作等,如果使用单线程,那么程序就必须等待这些操作执行完成之后才能执行其他操作。使用多线程,可以在将耗时任务放在后台继续执行的同时,同时执行其他操作。
  • 多线程可以提高程序的效率。
  • 在一些需要等待的任务上,如用户输入、文件读取、数据处理等,多线程就非常有用了。

多线程的缺点

  • 使用太多线程很耗系统资源,因为线程需要开辟内存。更多线程需要更多内存。
  • 影响系统性能,因为操作系统需要在线程之间来回切换。
  • 需要考虑线程操作对程序的影响,如线程挂起,中止等操作对程序的影响。
  • 线程使用不当会发生很多问题。

总结:多线程是异步的,但这不代表多线程真的是几个线程是在同时进行,实际上是系统不断地在各个线程之间来回的切换(因为系统切换的速度非常的快,所以给我们在同时运行的错觉)。

三、多线程与多进程的理解

  • 单进程单线程:一个人在一个桌子上吃菜。
  • 单进程多线程:多个人在同一个桌子上一起吃菜。
  • 多进程单线程:多个人每个人在自己的桌子上吃菜。

多线程的问题是:多个人同时吃一道菜的时候容易发生争抢。例如两个人同时夹一个菜,一个人刚伸出筷子,结果伸到的时候已经被夹走菜了。此时就必须等一个人夹一口之后,再还给另外一个人夹菜,也就是说资源共享就会发生冲突争抢。

对于 Windows 系统来说,【开桌子】的开销很大,因此 Windows 鼓励大家在一个桌子上吃菜。因此 Windows 多线程学习重点是要大量面对资源争抢与同步方面的问题。

对于 Linux 系统来说,【开桌子】的开销很小,因此 Linux 鼓励大家尽量每个人都开自己的桌子吃菜。这带来新的问题是:坐在两张不同的桌子上,说话不方便。因此,Linux 下的学习重点是大家要学习进程间通讯的方法。

大量创建进程的典型例子有两个,一个是 gnu autotools 工具链,用于编译很多开源代码的,他们在 Windows 下编译速度会很慢,因此软件开发人员最好是避免使用 Windows。另一个是服务器,某些服务器框架依靠大量创建进程来干活,甚至是对每个用户请求就创建一个进程,这些服务器在 Windows 下运行的效率就会很差。这”可能”也是放眼全世界范围,Linux 服务器远远多于 Windows 服务器的原因。

四、并发、并行、高并发

1、单核CPU与多核CPU的理解

  • 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费。)但是因为CPU时间单元特别短,因此感觉不出来。
  • 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
  • 一个Java应用程序java.exe,其实至少三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

2、并行与并发的理解

  • 并行:多个CPU实例或多台机器同时执行一段处理逻辑,是真正的同时。
  • 并发:通过CPU调度算法,让用户看上去同时执行,实际上CPU操作层面不是真正的同时。并发时如果操作了公用资源,可能产生线程安全问题。

3、高并发

高并发指的是系统运行过程中遇到的一种”短时间内遇到大量操作请求”的情况,主要发生在web系统集中大量访问或者socket端口集中性收到大量请求(例如:12306的抢票情况;天猫双十一活动)。该情况的发生会导致系统在这段时间内执行大量操作,例如对资源的请求,数据库的操作等。如果高并发处理不好,不仅仅降低了用户的体验度(请求响应时间过长),同时可能导致系统宕机,严重的甚至导致OOM异常,系统停止工作等。如果要想系统能够适应高并发状态,则需要从各个方面进行系统优化,包括,硬件、网络、系统架构、开发语言的选取、数据结构的运用、算法优化、数据库优化……。

4、多线程与高并发的关系

多线程只是在同/异步角度上解决高并发问题的其中的一个方法手段,是在同一时刻利用计算机闲置资源的一种方式。

多线程在高并发问题中的作用就是充分利用计算机资源,使计算机的资源在每一时刻都能达到最大的利用率,不至于浪费计算机资源使其闲置。

五、创建线程的方法

1、继承Thread类

示例:遍历100以内的所有的偶数

  • 创建一个继承于Thread类的子类

  • 重写Thread类的run() –> 将此线程执行的操作声明在run()中

  • 创建Thread类的子类的对象

  • 通过此对象调用start()

    // 1 继承于Thread类
    public class MyThread extends Thread{
        // 2 重写run()方法
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                if(i % 2 == 0){
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }
        }
    }
    
    public class ThreadTest {
        public static void main(String[] args) {
            // 3 创建Thread类的子类的对象
            MyThread t1 = new MyThread();
            // 4 调用start()方法
            t1.start();
    
            MyThread t2 = new MyThread();
            t2.start();
        }
    }

2、实现Runnable接口

  1. 创建一个实现了Runnable接口的类

  2. 实现类去实现Runnable中的抽象方法:run()

  3. 创建实现类的对象

  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象

  5. 通过Thread类的对象调用start()

    public class MyThread implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                if(i % 2 == 0){
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }
        }
    }
    
    public class ThreadTest {
        public static void main(String[] args) {
            MyThread mThread = new MyThread();
            Thread t1 = new Thread(mThread);
            t1.setName("线程1");
            t1.start();
    
            Thread t2 = new Thread(mThread);
            t2.setName("线程2");
            t2.start();
        }
    }

    比较创建线程的两种方式。 开发中:优先选择:实现Runnable接口的方式

    原因:实现的方式没有类的单继承性的局限性,实现的方式更适合来处理多个线程有共享数据的情况。

3、实现Callable接口

public class MyThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int x = 0; x < 10; x++) {
            System.out.println("*******线程执行,x=" + x + "********");
        }
        return "线程执行完毕";
    }
}

class Demo1 {
    public static void main(String[] args) throws Exception {
        FutureTask<String> task = new FutureTask<>(new MyThread());
        new Thread(task).start();
        System.out.println("线程返回数据" + task.get());
    }
}

Callable最主要的就是提供带有返回值的call方法来创建线程。不过Callable要和Future实现类连着用,关于Future的一系列知识会在后面几个系列讲到。

六、Thread和Runnable接口的联系

相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。

Runnable接口构造线程源码

/*下面是Thread类的部分源码*/

// 1.用Runnable接口创建线程时会进入这个方法
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

// 2.接着调用这个方法
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
    init(g, target, name, stackSize, null, true);
}

// 3.再调用这个方法
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }
    this.name = name;

    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
        if (security != null) {
            g = security.getThreadGroup();
        }
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }

    g.checkAccess();

    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }

    g.addUnstarted();

    this.group = g;
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext();
    // 4.最后在这里将Runnable接口(target)赋值给Thread自己的target成员属性     
    this.target = target;
    setPriority(priority);
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    /* Stash the specified stack size in case the VM cares */
    this.stackSize = stackSize;

    /* Set thread ID */
    tid = nextThreadID();
}

/*如果你是实现了runnable接口,那么在上面的代码中target便不会为null,那么最终就会通过重写的
规则去调用真正实现了Runnable接口(你之前传进来的那个Runnable接口实现类)的类里的run方法*/
@Override
public void run() {
    if (target != null) {
        target.run();
    }
}
  1. 多线程的设计使用了代理设计模式的结构,用户自定义的线程主体只是负责项目核心功能的实现,而所有的辅助实现全部交由Thread类来处理。
  2. 在进行Thread启动多线程的时候调用的是start()方法,而后找到的是run()方法,但通过Thread类的构造方法传递了一个Runnable接口对象的时候,该接口对象将被Thread类中的target属性所保存,在start()方法执行的时候会调用Thread类中的run()方法。而这个run()方法去调用实现了Runnable接口的那个类所重写过run()方法,进而执行相应的逻辑。多线程开发的本质实质上是在于多个线程可以进行同一资源的抢占,那么Thread主要描述的是线程,而资源的描述是通过Runnable完成的。

Thread类构造线程源码

MyThread t2 = new MyThread(); 	// 这个构造函数会默认调用super();也就是Thread类的无参构造
// 代码从上往下顺序执行
public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}
    
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
    init(g, target, name, stackSize, null, true);
}
    
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;

    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
        /* Determine if it's an applet or not */

        /* If there is a security manager, ask the security manager what to do. */
        if (security != null) {
            g = security.getThreadGroup();
        }

        /* If the security doesn't have a strong opinion of the matter use the parent thread group. */
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }

    /* checkAccess regardless of whether or not threadgroup is explicitly passed in. */
    g.checkAccess();

    /*
     * Do we have the required permissions?
     */
    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }

    g.addUnstarted();

    this.group = g;
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    /* Stash the specified stack size in case the VM cares */
    this.stackSize = stackSize;

    /* Set thread ID */
    tid = nextThreadID();
}

/*由于这里是通过继承Thread类来实现的线程,那么target这个东西就是Null。但是因为你继承了Runnable接口并且重写了run(),所以最终还是调用子类的run()*/
@Override
public void run() {
    if (target != null) {
         target.run();
    }
}

最直观的描述

class Window extends Thread{
    private  int ticket = 100;
    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}

public class WindowTest {
    public static void main(String[] args) {
        // 实际上有300张票
        Window t1 = new Window();
        Window t2 = new Window();
        Window t3 = new Window();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
class Window1 implements Runnable{
    private int ticket = 100;
    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}


public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

1、继承Thread类的方式,new了三个Thread,实际上是有300张票。

2、实现Runnable接口的方式,new了三个Thread,实际上只有100张票。

3、也就是说实现Runnable接口的线程中,成员属性是所有线程共有的。但是继承Thread类的线程中,成员属性是各个线程独有的,其它线程看不到,除非采用static的方式才能使各个线程都能看到。

4、就像上面说的Runnable相当于资源,Thread才是线程。用Runnable创建线程时,new了多个Thread,但是传进去的参数都是同一个Runnable(资源)。用Thread创建线程时,就直接new了多个线程,每个线程都有自己的Runnable(资源)。在Thread源码中就是用target变量(这是一个Runnable类型的变量)来表示这个资源。

5、同时因为这两个的区别,在并发编程中,继承了Thread的子类在进行线程同步时不能将成员变量当做锁,因为多个线程拿到的不是同一把锁,不过用static变量可以解决这个问题。而实现了Runnable接口的类在进行线程同步时没有这个问题。

七、策略模式在Thread和Runnable中的应用

Runnable接口最重要的方法—–run方法,使用了策略者模式将执行的逻辑(run方法)和程序的执行单元(start0方法)分离出来,使用户可以定义自己的程序处理逻辑,更符合面向对象的思想。

Thread的构造方法

  • 创建线程对象Thread,默认有一个线程名,以Thread-开头,从0开始计数,如“Thread-0、Thread-1、Thread-2 …”
  • 如果没有传递Runnable或者没有重写Thread的run方法,该Thread不会调用任何方法
  • 如果传递Runnable接口的实例或者重写run方法,则会执行该方法的逻辑单元(逻辑代码)
  • 如果构造线程对象时,未传入ThreadGroup,Thread会默认获取父线程的ThreadGroup作为该线程的ThreadGroup,此时子线程和父线程会在同一个ThreadGroup中
  • stackSize可以提高线程栈的深度,放更多栈帧,但是会减少能创建的线程数目
  • stackSize默认是0,如果是0,代表着被忽略,该参数会被JNI函数调用,但是注意某些平台可能会失效,可以通过“-Xss10m”设置

具体的介绍可以看Java的API文档

/*下面是Thread的部分源码*/
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(String name) {
    init(null, null, name, 0);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
    init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
    // 中间源码省略
    this.target = target;	// ①
}

/* What will be run. */
private Runnable target; // Thread类中的target属性

@Override
public void run() {
    if (target != null) { 	// ②
        target.run();
    }
}
源码标记解读:
1、如果Thread类的构造方法传递了一个Runnable接口对象
①那么该接口对象将被Thread类中的target属性所保存。
②在start()方法执行的时候会调用Thread类中的run()方法。因为target不为null, target.run()就去调用实现Runnable接口的子类重写的run()。
2、如果Thread类的构造方传没传Runnable接口对象
①Thread类中的target属性保存的就是null。
②在start()方法执行的时候会调用Thread类中的run()方法。因为target为null,只能去调用继承Thread的子类所重写的run()。

JVM一旦启动,虚拟机栈的大小就确定了。但是如果你创建Thread的时候传了stackSize(该线程占用的stack大小),该参数会被JNI函数去使用。如果没传这个参数,就默认为0,表示忽略这个参数。注:stackSize在有一些平台上是无效的。

start()源码

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();	// ①

    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */
        }
    }
}

private native void start0();
	@Override
	public void run() {
    	if (target != null) {
        	target.run();
    }
}
源码标记解读:
①当多次调用start(),会抛出throw new IllegalThreadStateException()异常。也就是每一个线程类的对象只允许启动一次,如果重复启动则就抛出此异常。

为什么线程的启动不直接使用run()而必须使用start()呢

1、如果直接调用run()方法,相当于就是简单的调用一个普通方法。

为什么必须要有start()方法启动多线程呢?

在Java程序执行的过程中,考虑到对于不同层次开发者的需求,所以其支持有本地的操作系统函数调用,而这项技术就被称为JNI(Java Native Interface),但是Java开发过程中并不推荐这样使用,利用这项技术可以使用一些操作系统提供的底层函数进行特殊处理,而在Thread类里面提供的start0()就表示需要将此方法依赖于不同的操作系统实现。

任何情况下,只要定义了多线程,多线程的启动永远只有一种方案:Thread类中的start()方法

2、run()的调用是在start0()这个Native C++方法里调用的

八、线程的生命周期

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态,这几个状态在Java源码中用枚举来表示。

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。

1、由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。

2、操作系统隐藏 Java 虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。

3、调用sleep()方法,会进入Blocked状态。sleep()结束之后,Blocked状态首先回到的是Runnable状态中的Ready(也就是可运行状态,但并未运行)。只有拿到了cpu的时间片才会进入Runnable中的Running状态。

九、Thread常用API

  • 获取当前存活的线程数:public int activeCount()
  • 获取当前线程组的线程的集合:public int enumerate(Thread[] list)

十、一个Java程序有哪些线程?

1、当调用一个线程start()方法的时候,此时至少有两个线程,一个是调用你的线程,还有一个是被你创建出来的线程。

例子:

public static void main(String[] args) {
    Thread t1 = new Thread() {
        @Override
        public void run() {
            System.out.println("==========");
        }
    };
    t1.start();
}

这里面就是一个调用你的线程(main线程),一个被你创建出来的线程(t1,名字可能是Thread-0)

2、当JVM启动后,实际有多个线程,但是至少有一个非守护线程(比如main线程)。

  • Finalizer:GC守护线程
  • RMI:Java自带的远程方法调用(秋招面试,有个面试官问过)
  • Monitor :是一个守护线程,负责监听一些操作,也在main线程组中
  • 其它:我用的是IDEA,其它的应该是IDEA的线程,比如鼠标监听啥的。

十一、守护线程

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread() {
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " running");
                Thread.sleep(100000); // ①
                System.out.println(Thread.currentThread().getName() + " done.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }; // new
    
    t.setDaemon(true); // ②
    t.start();
    Thread.sleep(5000);   // JDK1.7
    System.out.println(Thread.currentThread().getName());
}
源码标记解读:
①变量名为t的线程Thread-0,睡眠100秒。
②但是在主函数里Thread-0设置成了main线程的守护线程。所以5秒之后main线程结束了,即使在①这里守护线程还是处于睡眠100秒的状态,但由于他是守护线程,非守护线程main结束了,守护线程也必须结束。
1、但是如果Thread-0线程不是守护线程,即使main线程结束了,Thread-0线程仍然会睡眠100秒再结束。

  • 当主线程死亡后,守护线程会跟着死亡
  • 可以帮助做一些辅助性的东西,如“心跳检测”
  • 设置守护线程:public final void setDaemon(boolean flag)
  • 守护线程的作用

    A和B之间有一条网络连接,可以用守护线程来进行发送心跳,一旦A和B连接断开,非守护线程就结束了,守护线程(也就是心跳没有必要再发送了)也刚好断开。

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            Thread innerThread = new Thread(() -> {
                try {
                    while (true) {
                        System.out.println("Do some thing for health check.");
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
          //  innerThread.setDaemon(true);
            innerThread.start();
            try {
                Thread.sleep(1000);
                System.out.println("T thread finish done.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // t.setDaemon(true);
        t.start();
    }
    
    /*
     * 设置该线程为守护线程必须在启动它之前。如果t.start()之后,再t.setDaemon(true);
     * 会抛出IllegalThreadStateException
     */

    十二、join()方法

    例子1

    public class MyThread {
        public static void main(String[] args) throws Exception{
            Thread t1 = new Thread(() -> {
                IntStream.range(1,1000).forEach(i -> System.out.println(Thread.currentThread().getName() + "->" + i));
            });
            Thread t2 = new Thread(() -> {
                IntStream.range(1,1000).forEach(i -> System.out.println(Thread.currentThread().getName() + "->" + i));
            });
    
            t1.start();
            t2.start();
            t1.join();
            t2.join();
    
            Optional.of("All of tasks finish done.").ifPresent(System.out::println);
            IntStream.range(1,1000).forEach(i -> {
                System.out.println(Thread.currentThread().getName() + "->" + i);
            });
        }
    }
    • 默认传入的数字为0,这里是在main线程里调用了两个线程的join(),所以main线程会等到Thread-0和Thread-1线程执行完再执行它自己。

    • join必须在start方法之后,并且join()是对wait()的封装。(源码中可以清楚的看到)

    • 也就是说,t.join()方法阻塞调用此方法的线程(calling thread)进入 TIMED_WAITING或WAITING 状态。直到线程t完成,此线程(main线程)再继续。

    • join也有人理解成插队,比如在main线程中调用t.join(),就是t线程要插main线程的队,main线程要去等待。

    例子2

    public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                IntStream.range(1, 1000)
                        .forEach(i -> System.out.println(Thread.currentThread().getName() + "->" + i));
            });
            Thread t2 = new Thread(() -> {
                try {
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                IntStream.range(1, 1000)
                        .forEach(i -> System.out.println(Thread.currentThread().getName() + "->" + i));
            });
    
            t1.start();
            t2.start();
    //        t1.join();
            t2.join();
    
            Optional.of("All of tasks finish done.").ifPresent(System.out::println);
            IntStream.range(1, 1000)
                    .forEach(i -> System.out.println(Thread.currentThread().getName() + "->" + i));
        }
    • 在这里t2线程会等待t1线程打印完,t2自己才会打印。然后t2.join(),main线程也要等待t2线程。总体执行顺序就是t1 → t2 → main
    • 通过上方例子可以用join实现类似于CompletableFuture的异步任务编排。(后面会讲)

    十三、中断

    1、Java 中的中断和操作系统的中断还不一样,这里就按照状态来理解吧,不要和操作系统的中断联系在一起。

    2、记住中断只是一个状态,Java的方法可以选择对这个中断进行响应,也可以选择不响应。响应的意思就是写相对应的代码执行相对应的操作,不响应的意思就是什么代码都不写。

    几个方法

    // Thread 类中的实例方法,持有线程实例引用即可检测线程中断状态
    public boolean isInterrupted() {}
    /*
     * 1 Thread 中的静态方法,检测调用这个方法的线程是否已经中断
     * 2 注意:这个方法返回中断状态的同时,会将此线程的中断状态重置为 false
     * 如果我们连续调用两次这个方法的话,第二次的返回值肯定就是 false 了
     */
    public static boolean interrupted() {}
    
    // Thread 类中的实例方法,用于设置一个线程的中断状态为 true
    public void interrupt() {}

    小Tip

    public static boolean interrupted()
    public boolean isInterrupted()	// 这个会清除中断状态

    为什么要这么设置呢?原因在于:

    • interrupted()是一个静态方法,可以在Runnable接口实例中使用

    • isInterrupted()是一个Thread的实例方法,在重写Thread的run方法时使用

      public class ThreadInterrupt {
          public static void main(String[] args) throws InterruptedException {
              Thread t1 = new Thread(() -> {
                  System.out.println(Thread.interrupted());
              });  //这个new Thread用的是runnable接口那个构造函数
      
              Thread t2 = new Thread(){
                  @Override
                  public void run() {
                      System.out.println(isInterrupted());
                  }
              };//这个new Thread用的就是Thread的空参构造
          }
      }

      也就是说接口中不能调用Thread的实例方法,只能通过静态方法来判断是否发生中断

    重难点

    当然,中断除了是线程状态外,还有其他含义,否则也不需要专门搞一个这个概念出来了。

    初学者肯定以为 thread.interrupt() 方法是用来暂停线程的,主要是和它对应中文翻译的“中断”有关。中断在并发中是常用的手段,请大家一定好好掌握。可以将中断理解为线程的状态,它的特殊之处在于设置了中断状态为 true 后,这几个方法会感知到:
    1. wait(), wait(long), wait(long, int), join(), join(long), join(long, int), sleep(long), sleep(long, int)

    2. 这些方法都有一个共同之处,方法签名上都有throws InterruptedException,这个就是用来响应中断状态修改的。

    3. 如果线程阻塞在 InterruptibleChannel 类的 IO 操作中,那么这个 channel 会被关闭。
    4. 如果线程阻塞在一个 Selector 中,那么 select 方法会立即返回。
    对于以上 3 种情况是最特殊的,因为他们能自动感知到中断(这里说自动,当然也是基于底层实现),并且在做出相应的操作后都会重置中断状态为 false。然后执行相应的操作(通常就是跳到 catch 异常处)。
    如果不是以上3种情况,那么,线程的 interrupt() 方法被调用,会将线程的中断状态设置为 true。
    那是不是只有以上 3 种方法能自动感知到中断呢?不是的,如果线程阻塞在 LockSupport.park(Object obj) 方法,也叫挂起,这个时候的中断也会导致线程唤醒,但是唤醒后不会重置中断状态,所以唤醒后去检测中断状态将是 true。
    资料:Oracle官方文档The Java® Language Specification Java SE 8 Edition → 第17章 Threads and Locks

    InterruptedException

    它是一个特殊的异常,不是说 JVM 对其有特殊的处理,而是它的使用场景比较特殊。通常,我们可以看到,像 Object 中的 wait() 方法,ReentrantLock 中的 lockInterruptibly() 方法,Thread 中的 sleep() 方法等等,这些方法都带有 throws InterruptedException,我们通常称这些方法为阻塞方法(blocking method)。

    阻塞方法一个很明显的特征是,它们需要花费比较长的时间(不是绝对的,只是说明时间不可控),还有它们的方法结束返回往往依赖于外部条件,如 wait 方法依赖于其他线程的 notify,lock 方法依赖于其他线程的 unlock等等。

    当我们看到方法上带有 throws InterruptedException 时,我们就要知道,这个方法应该是阻塞方法,我们如果希望它能早点返回的话,我们往往可以通过中断来实现。

    除了几个特殊类(如 Object,Thread等)外,感知中断并提前返回是通过轮询中断状态来实现的。我们自己需要写可中断的方法的时候,就是通过在合适的时机(通常在循环的开始处)去判断线程的中断状态,然后做相应的操作(通常是方法直接返回或者抛出异常)。当然,我们也要看到,如果我们一次循环花的时间比较长的话,那么就需要比较长的时间才能感知到线程中断了。

    wait()中断测试

    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                while (true) {
                    synchronized (MONITOR) {
                        try {
                            MONITOR.wait(10);
                        } catch (InterruptedException e) {
                            System.out.println("wait响应中断"); // pos_1
                            e.printStackTrace(); // pos_2
                            System.out.println(isInterrupted()); // pos_3
                        }
                    }
                }
            }
        };
    
        t.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            System.out.println("sleep响应中断");
            e.printStackTrace();
        }
        System.out.println(t.isInterrupted()); // pos_4
        t.interrupt();
        System.out.println(t.isInterrupted()); // pos_5
    }
    注释掉e.printStackTrace();的输出
    false //pos_4 true //pos_5
    wait响应中断 //pos_1 false //pos_3

    join中断测试

    Thread main = Thread.currentThread();
    Thread t2 = new Thread() {
        @Override
        public void run() {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            main.interrupt();  // pos_1
            System.out.println("interrupt");
        }
    };
    
    t2.start();
    try {
        t.join();  // pos_2
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    1、pos_2这里join的是main线程,所以pos_1这里需要中断main线程,才能收到中断信息。


    文章作者: Prannt
    版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Prannt !
    评论
      目录