浅谈Java多线程

148 阅读10分钟

多线程模型是Java程序最基本的并发模型,本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。

一、进程和线程

进程

在计算机中,一个任务就是一个进程,比如打开Word就是打开一个进程。在进程内部还需要同时执行多个子任务。例如,Word可以让我们一边打字,一边进行拼写检查,我们把子任务称为线程。一个进程可以包含一个或多个线程,但至少会有一个线程。因为同一个应用程序,既可以有多个进程,也可以有多个线程。

线程

线程是cpu调度的最小单位,线程是进程的一个执行单元,负责当前进程中程序的执行。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

区别

进程和线程是包含关系,通常一个进程都有若干个线程,至少包含一个线程。 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。 线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。 多进程是指操作系统能同时运行多个任务(程序)。

  • 根本区别: 进程是操作系统资源分配的基本单位(资源分配的最小单位),而线程是处理器任务调度和执行的基本单位(cpu调度的最小单位)

  • 资源开销: 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

  • 包含关系: 进程和线程是包含关系,一个进程至少包含一个线程。线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

  • 内存分配: 同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

  • 影响关系: 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

  • 执行过程: 每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。


二、并行和并发

并发是指一个处理器同时处理多个任务。并行是指多个处理器同时处理多个不同的任务。 并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。 来个比喻:并发是一个人同时吃三个馒头,而并行是三个人同时吃三个馒头。

并行

指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。

并发

指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。

总结:

  • 并行:你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行
  • 并发:你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。

三、多线程

多线程是指在同一程序中有多个顺序流在执行。和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。

Java语言内置了多线程支持,当Java程序启动的时候,实际上是启动了一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动其他线程。此外,JVM还有负责垃圾回收的其他工作线程等。


四、实现多线程

Java语言内置了多线程支持,其多线程实现方式主要有三种:继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果的多线程。其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。

继承Thread类

只需要继承Thread类,重写run(),然后在run方法里写下线程要实现的任务即可。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。

通过继承Thread类实现多线程的步骤:

  1. 定义一个类,继承Thread类,并重写该类的run方法,该run方法的方法体就代表了线程需要完成的任务,因此,run方法的方法体被称为线程执行体;
  2. 创建Thread子类的对象,即创建了子线程;
  3. 用线程对象的start方法来启动该线程;

代码如下:

class MyThread extends Thread {
    private String name;
    static int count = 10;

    MyThread(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        // 循环售票
        while (count > 0) {
            count--;
            //System.out.println(Thread.currentThread().getName() + "售出一张票,剩余" + count);
            System.out.println(name + " > 售出一张票,剩余:" + count);
        }
    }
}

class Main {
    public static void main(String[] args) {
        // 创建两个线程来售票
        Thread th1 = new MyThread("1号窗口");
        Thread th2 = new MyThread("2号窗口");
        // 启动线程
        th1.start();
        th2.start();
    }
}

在这里插入图片描述 注意到start()方法会在内部自动调用实例的run()方法。

实现Runable接口

通过实现Runable接口实现多线程是受欢迎。因为Java只能单继承,继承了Thread类就不能再继承其他类了。

通过Runnable接口实现多线程的步骤:

  1. 定义一个Runnable接口的实现类,并重写该接口中的run方法,该run方法的方法体同样是该线程的线程执行体;
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象;
  3. 调用线程对象的start方法来启动该线程;

代码如下:

class MyThread2 implements Runnable {
    static int count = 10;
    private String name;

    public MyThread2(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        while (count > 0) {
            count--;
            System.out.println(name + " > 售出一张票,剩余:" + count);
        }
    }
}

class Main {
    public static void main(String[] args) {
    // 为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:
        Thread th3 = new Thread(new MyThread2("3号窗口"));
        Thread th4 = new Thread(new MyThread2("4号窗口"));

        th3.start();
        th4.start();
    }
}

在这里插入图片描述

通过callable

使用ExecutorService、Callable、Future实现有返回结果的多线程 ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。

Thread与Runnable的比较

继承Thread类的方式

  • 编写简单,如果要访问当前线程,除了可以通过Thread.currentThread()方式之外,还可以使用 super关键字
  • 弊端:因为线程类已经继承了Thread类,则不能再继承其他类【单继承】 实际上大多数的多线程应用都可以采用实现Runnable接口的方式来实现【推荐使用匿名内部类】java类是单知继承的

实现Runnable接口的方式

  • 线程类只是实现了Runnable接口,还可以继承其他类【一个类在实现接口的同时还可以继承另外一个类】
  • 可以多个线程共享同一个target对象,所以非常适合多个线程来处理同一份资源的情况
  • 弊端:编程稍微复杂,不直观,如果要访问当前线程,必须使用Thread.currentThread()

start()与run()方法的区别

必须调用Thread实例的start()方法才能启动新线程,查看Thread类的源代码,会看到start()方法内部调用了一个private native void start0()方法,native修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的。

start()方法会新建一个线程,并且让这个线程执行run()方法。

Thread thread = new Thread();
thread.start();

调用run()也能正常执行。但是,却不能新建一个线程,而是在当前线程中调用run()方法,只是作为一个普通的方法调用。

Thread thread = new Thread();
thread.run();

效果如下: 在这里插入图片描述 所以不要用run()来开启新线程,它只会在当前线程中,串行执行run()方法中的代码,相当于调用了一个普通的Java方法,当前线程并没有任何改变,也不会启动新线程。


五、线程的状态

在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:

  • New:新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行- sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。
         ┌─────────────┐
         │     New     │
         └─────────────┘
                │
                ▼
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
 ┌─────────────┐ ┌─────────────┐
││  Runnable   │ │   Blocked   ││
 └─────────────┘ └─────────────┘
│┌─────────────┐ ┌─────────────┐│
 │   Waiting   │ │Timed Waiting│
│└─────────────┘ └─────────────┘│
 ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
                │
                ▼
         ┌─────────────┐
         │ Terminated  │
         └─────────────┘

当线程启动后,它可以在Runnable、Blocked、Waiting和Timed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。

线程终止的原因

  • 线程正常终止:run()方法执行到return语句返回;
  • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
  • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("hello");
        });
        System.out.println("start");
        t.start();
        t.join();
        System.out.println("end");
    }
}

当main线程对线程对象t调用join()方法时,主线程将等待变量t表示的线程运行结束,即join就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是main线程先打印start,t线程再打印hello,main线程最后再打印end。

如果t线程已经结束,对实例t调用join()会立刻返回。此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。


感谢大家的耐心阅读,如有建议请私信或评论留言