Java多线程

注意:本章节会涉及到 操作系统 相关知识。
在了解多线程之前,让我们回顾一下操作系统中提到的进程概念:
5-1 线程基础 - 图1
进程是程序执行的实体,每一个进程都是一个应用程序(比如我们运行QQ、浏览器、LOL、网易云音乐等软件),都有自己的内存空间,CPU一个核心同时只能处理一件事情,当出现多个进程需要同时运行时,CPU一般通过时间片轮转调度算法,来实现多个进程的同时运行。
5-1 线程基础 - 图2
在早期的计算机中,进程是拥有资源和独立运行的最小单位,也是程序执行的最小单位。但是,如果我希望两个任务同时进行,就必须运行两个进程,由于每个进程都有一个自己的内存空间,进程之间的通信就变得非常麻烦(比如要共享某些数据)而且执行不同进程会产生上下文切换,非常耗时,那么能否实现在一个进程中就能够执行多个任务呢?
5-1 线程基础 - 图3
后来,线程横空出世,一个进程可以有多个线程,线程是程序执行中一个单一的顺序控制流程,现在线程才是程序执行流的最小单元,各个线程之间共享程序的内存空间(也就是所在进程的内存空间),上下文切换速度也高于进程。
在Java中,我们从开始,一直以来编写的都是单线程应用程序(运行main()方法的内容),也就是说只能同时执行一个任务(无论你是调用方法、还是进行计算,始终都是依次进行的,也就是同步的),而如果我们希望同时执行多个任务(两个方法同时在运行或者是两个计算同时在进行,也就是异步的),就需要用到Java多线程框架。实际上一个Java程序启动后,会创建很多线程,不仅仅只运行一个主线程:

  1. public static void main(String[] args) {
  2. ThreadMXBean bean = ManagementFactory.getThreadMXBean();
  3. long[] ids = bean.getAllThreadIds();
  4. ThreadInfo[] infos = bean.getThreadInfo(ids);
  5. for (ThreadInfo info : infos) {
  6. System.out.println(info.getThreadName());
  7. }
  8. }

关于除了main线程默认以外的线程,涉及到JVM相关底层原理,在这里不做讲解,了解就行。


线程的创建和启动

通过创建Thread对象来创建一个新的线程,Thread构造方法中需要传入一个Runnable接口的实现(其实就是编写要在另一个线程执行的内容逻辑)同时Runnable只有一个未实现方法,因此可以直接使用lambda表达式:

  1. @FunctionalInterface
  2. public interface Runnable {
  3. /**
  4. * When an object implementing interface <code>Runnable</code> is used
  5. * to create a thread, starting the thread causes the object's
  6. * <code>run</code> method to be called in that separately executing
  7. * thread.
  8. * <p>
  9. * The general contract of the method <code>run</code> is that it may
  10. * take any action whatsoever.
  11. *
  12. * @see java.lang.Thread#run()
  13. */
  14. public abstract void run();
  15. }

创建好后,通过调用start()方法来运行此线程:

public static void main(String[] args) {
    Thread t = new Thread(() -> {    //直接编写逻辑
        System.out.println("我是另一个线程!");
    });
    t.start();   //调用此方法来开始执行此线程
}

可能上面的例子看起来和普通的单线程没两样,那我们先来看看下面这段代码的运行结果:

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        System.out.println("我是线程:"+Thread.currentThread().getName());
        System.out.println("我正在计算 0-10000 之间所有数的和...");
        int sum = 0;
        for (int i = 0; i <= 10000; i++) {
            sum += i;
        }
        System.out.println("结果:"+sum);
    });
    t.start();
    System.out.println("我是主线程!");
}

我们发现,这段代码执行输出结果并不是按照从上往下的顺序了,因为他们分别位于两个线程,他们是同时进行的!如果你还是觉得很疑惑,我们接着来看下面的代码运行结果:

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50; i++) {
            System.out.println("我是一号线程:"+i);
        }
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50; i++) {
            System.out.println("我是二号线程:"+i);
        }
    });
    t1.start();
    t2.start();
}

我们可以看到打印实际上是在交替进行的,也证明了他们是在同时运行!

注意:我们发现还有一个run方法,也能执行线程里面定义的内容,但是run是直接在当前线程执行,并不是创建一个线程执行!

5-1 线程基础 - 图4
实际上,线程和进程差不多,也会等待获取CPU资源,一旦获取到,就开始按顺序执行我们给定的程序,当需要等待外部IO操作(比如Scanner获取输入的文本),就会暂时处于休眠状态,等待通知,或是调用sleep()方法来让当前线程休眠一段时间:

public static void main(String[] args) throws InterruptedException {
    System.out.println("l");
    Thread.sleep(1000);    //休眠时间,以毫秒为单位,1000ms = 1s
    System.out.println("b");
    Thread.sleep(1000);
    System.out.println("w");
    Thread.sleep(1000);
    System.out.println("nb!");
}

我们也可以使用stop()方法来强行终止此线程:

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        Thread me = Thread.currentThread();   //获取当前线程对象
        for (int i = 0; i < 50; i++) {
            System.out.println("打印:"+i);
            if(i == 20) me.stop();  //此方法会直接终止此线程
        }
    });
    t.start();
}

虽然stop()方法能够终止此线程,但是并不是所推荐的做法,有关线程中断相关问题,我们会在后面继续了解。

思考:猜猜以下程序输出结果:

private static int value = 0;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) value++;
        System.out.println("线程1完成");
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) value++;
        System.out.println("线程2完成");
    });
    t1.start();
    t2.start();
    Thread.sleep(1000);  //主线程停止1秒,保证两个线程执行完成
    System.out.println(value);
}

我们发现,value最后的值并不是我们理想的结果,有关为什么会出现这种问题,在我们学习到线程锁的时候,再来探讨。