Java 多线程学习笔记

概念

进程

  • 正在运行的程序,是系统进行资源分配和调用的独立单位
  • 每一个进程都有它自己的内存空间和系统资源,一个进程包括由操作系统分配的内存空间,包含一个或多个线程
  • 一个进程一直运行,直到所有的非守护线程都结束运行后才能结束

线程

  • 线程是进程中的单个顺序控制流,是一条执行路径
  • 一个进程如果只有一条执行路径,则称为「单线程程序」
  • 一个进程如果有多条执行路径,则称为「多线程程序」
  • 一个线程不能独立的存在,它必须是进程的一部分
  • 线程是 CPU 调度的最小单位

Java 中对于线程的描述是 Thread,其中,封装了线程的相关信息,最重要的有需要执行的任务的信息;

线程的交互

互斥和同步:

  • 互斥是当一个线程正在运行时,其他的线程只能等待,当完成后就可以运行了。
  • 同步是两个或多个线程同时进行运行。

线程的状态

当你需要使用 Java 线程在多线程环境下进行编程时,理解 Java 的线程周期与线程的状态是非常重要的。

线程分为五个阶段:

  • 创建(new)状态: 准备好了一个多线程的对象
  • 就绪(runnable)状态: 调用了 start() 方法, 它的状态变为 Runnable(可运行的)。控制权被给予线程调度程序来完成它的执行。 是否立即运行此线程或在运行之前将其保持在可运行线程池中,取决于线程调度程序的实现,等待 CPU 进行调度
  • 运行(running)状态: 执行 run() 方法,线程正在执行。线程调度程序从可运行线程池中选择一个线程,并将其状态更改为正在运行,然后 CPU 开始执行这个线程。
  • 阻塞(blocked)状态: 暂时停止执行, 可能将资源交给其它线程使用。
  • 终止(dead)状态: 一旦线程完成执行,它的状态就变成 Dead,线程销毁

当需要新起一个线程来执行某个子任务时,就创建了一个线程。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,在前面的 JVM 内存区域划分一篇博文中知道程序计数器、Java 栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。

当线程进入就绪状态后,不代表立刻就能获取 CPU 执行时间,也许此时 CPU 正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。

线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)。

当由于突然中断或者子任务执行完毕,线程就会被消亡。

在有些教程上将 blocked、waiting、time waiting 统称为阻塞状态,这个也是可以的,只不过这里我想将线程的状态和 Java 中的方法调用联系起来,所以将 waiting 和time waiting 两个状态分离出来。

注: sleep 和 wait 的区别:

  • sleep 是 Thread 类的方法,wait 是 Object 类中定义的方法.
  • Thread.sleep 不会导致锁行为的改变, 如果当前线程是拥有锁的, 那么 Thread.sleep 不会让线程释放锁.
  • Thread.sleep 和 Object.wait 都会暂停当前的线程. OS 会将执行时间分配给其它线程. 区别是, 调用 wait 后, 需要别的线程执行 notify/notifyAll 才能够重新获得 CPU 执行时间.

参考:

多线程的意义

  • 多进程的意义? 提高CPU的使用率
  • 多线程的意义? 提高应用程序的使用率

Java 程序运行原理

Java 命令会启动 Java 虚拟机,启动 JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个「主线程」,然后主线程去调用某个类的 main 方法。

所以,main 方法运行在主线程中。未采用多线程时,程序都是单线程的。

扩展阅读

线程实现方式

线程创建方式——继承 Thread 类

  • 子类继承 Thread 类,子类中覆盖父类方法的 run 方法,将线程运行的代码放在 run 方法中;
  • 创建子类的实例,线程被创建;
  • 调用子类的实例的 start 方法,开启线程;
public class Test {
    public static void main(String[] args)  {
        System.out.println("主线程ID:"+Thread.currentThread().getId());
        MyThread thread1 = new MyThread("thread1");
        thread1.start();
        MyThread thread2 = new MyThread("thread2");
        thread2.run();
    }
}

class MyThread extends Thread{
    private String name;

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

    @Override
    public void run() {
        System.out.println("name:"+name+" 子线程ID:"+Thread.currentThread().getId());
    }
}

执行结果:

主线程ID:1
name:thread2 子线程ID:1
name:thread1 子线程ID:10

同个线程的 start 方法重复调用的话,会出现 java.lang.IllegalThreadStateException 异常

线程创建方式——实现 Runnable 接口

  • 子类实现 Runnable 接口,覆盖接口中的 run 方法;
  • 通过 Thread 类创建线程,并将实现了 Runnable 接口的子类对象作为 Thread 类的参数;
  • 通过 Thread 类的实例对象调用 start 方法,开启线程;
public class Test {
    public static void main(String[] args)  {
        System.out.println("主线程ID:"+Thread.currentThread().getId());
        MyRunnable runnable1 = new MyRunnable("thread1");
        Thread thread1 = new Thread(runnable1);
        thread1.start();
        MyRunnable runnable2 = new MyRunnable("thread2");
        Thread thread2 = new Thread(runnable2);
        thread2.run();
    }
}

class MyRunnable implements Runnable{
    private String name;

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

    @Override
    public void run() {
        System.out.println("name:"+name+" 子线程ID:"+Thread.currentThread().getId());
    }
}

执行结果:

主线程ID:1
name:thread2 子线程ID:1
name:thread1 子线程ID:10

不管是扩展 Thread 类还是实现 Runnable 接口来实现多线程,最终还是通过 Thread 的对象的API 来控制线程的,熟悉 Thread 类的 API 是进行多线程编程的基础。

在 Java 中,这 2 种方式都可以用来创建线程去执行子任务,具体选择哪一种方式要看自己的需求。直接继承 Thread 类的话,可能比实现 Runnable 接口看起来更加简洁,但是由于 Java 只允许单继承,所以如果自定义类需要继承其他类,则只能选择实现 Runnable 接口。

线程创建方式——实现 Callable 接口

还有一种是实现 Callable 接口,并与 Future、线程池结合使用

Thread 常用方法

  • getName() 获取当前线程的名称 Thread.currenthrad().getName
继承Thread类,getName()获取当前线程名称。
实现Runnable接口,Thread.currentThread().getName();获取当前线程名称。
  • Thread.join() 让其他线程等待当前线程执行完之后再执行,比如,当前线程在另外一个线程的 run 方法中,如果不加 join 方法,那么,当前线程可能未执行完毕,其他线程就会往下执行了;
  • Thread.sleep(1000) 休眠 1 秒;
  • Thread.yield(); 让出处理器时间,「线程们」去竞争吧
  • volatile 关键字,保证了线程可以正确的读取其他线程写入的值,如下示例:

FAQ

启动一个线程是 run() 还是 start()?它们的区别?

start() 启动一个线程;

  • run 方法封装了被线程执行的代码,直接调用仅仅是普通方法的调用;
  • start 启动线程,并有 JVM 自动调用 run 方法;

Thread和Runnable的区别

如果一个类继承 Thread,则不适合资源共享。但是如果实现了 Runable 接口的话,则很容易的实现资源共享。

总结,实现 Runnable 接口比继承 Thread 类所具有的优势:

  1. 适合多个相同的程序代码的线程去处理同一个资源
  2. 可以避免 Java 中的单继承的限制
  3. 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
  4. 线程池只能放入实现 Runablecallable 类线程,不能直接放入继承 Thread 的类

提醒一下大家:main 方法其实也是一个线程。在 Java 中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到 CPU 的资源。

在 Java 中,每次程序运行至少启动 2 个线程。一个是 main 线程,一个是垃圾收集线程。因为每当使用 Java 命令执行一个类的时候,实际上都会启动一个 JVM,每一个 JVM 实际就是在操作系统中启动了一个进程。

如何正确的停止 Java 中的线程

不要使用 stop 方法,因为不清楚哪些工作已经做了、哪些还没做、清理工作还没做……戛然而止的现象!

正确的做法:设置退出旗标

使用退出标志停止线程执行的方式的好处在于:

  1. 可以使线程执行完对应的操作后,因不符合继续执行的条件而停止
  2. 我们可以做一些线程执行结束后的清理工作
  3. 使线程的结束执行看起来是有次序的,而非戛然而止

interrupt 方法不是停止线程的正确方法,原因是 interrupt() 方法初衷不是用于停止线程。interrupt() 方法不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被 Object.wait, Thread.joinThread.sleep 三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态,然后该线程还是继续运行的

参考

posted @ 2019-04-23 23:19  Michael翔  阅读(...)  评论(...编辑  收藏