10.1 相关概念的介绍

10.1.1 程序、进程与线程

程序:用来完成某些功能的一段静态代码,一组指令的集合。
进程:正在运行的一段程序。它是系统分配资源的基本单位,是一个动态过程,有它自己的声明周期。
线程:进程中的一个执行单元,它是系统执行与调度的基本单位

10.1.2 单线程和多线程

单线程:每个正在运行的程序(即进程),至少包括一个线程,这个线程叫主线程。主线程在程序启动时被创建,用于执行main函数。只有一个主线程的程序,称作单线程程序,主线程负责执行程序的所有代码(UI展现以及刷新,网络请求,本地存储等等)。这些代码只能顺序执行,无法并发执行。

多线程:拥有多个线程的程序,称作多线程程序。一个进程可以创建一个或多个线程,线程还可以创建子线程。

10.1.3 并发与并行

并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。

比如说对于一个单核cpu来说,因为cpu一次只能处理单条指令,所以即便是单核多线程cpu,也只能做到并发,而非并行。并行只能由多核cpu进行处理。

10.2 线程的基本使用

10.2.1 创建线程的两种方式

  • 继承Thread类:Thread类实现了Runnable接口
  • 实现Runnable接口

10.2.2 Thread类使用案例

  1. public class Thread01 extends Thread{
  2. public static void main(String[] args) throws InterruptedException {
  3. Cat cat = new Cat();
  4. cat.start();
  5. // 主线程继续
  6. System.out.println("主线程继续执行" + Thread.currentThread().getName());
  7. for (int i = 0; i < 60; i++) {
  8. System.out.println("主线程 i=" + i);
  9. Thread.sleep(1000);
  10. }
  11. }
  12. }
  13. class Cat extends Thread{
  14. @Override
  15. public void run() {
  16. int times = 0;
  17. while (times < 80) {
  18. System.out.println("喵喵,我是小猫咪");
  19. times++;
  20. System.out.println(times);
  21. try {
  22. Thread.sleep(1000);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }
  28. }
  29. 代码分析: 这段代码可以演示主线程和子线程并行执行的过程。main方法相当于起了一个主线程,有主线程中实例化了Cat对象,由于Cat继承了Thread类并重写run方法,所以可以通过该类对象的start方法启动一个线程。

start方法是Thread类中的一个成员方法,它调用了本地方法native start0用来启动线程(注意此时只是启动,而非线程能够立马执行,线程只是状态切换到了就绪态,但是依然要等系统时间片轮转到它时才会执行)。

run方法是Runnable接口中的一个抽象方法,由实现它的类来定义其功能,内容为线程具体要做的事情。当线程开始执行时,就开始执行run方法里的代码,当run方法体执行完毕后,线程就正常结束了。

10.2.3 Runnable接口使用案例

由于Java是单继承,当一个类已经有直接继承的父类时,就不能再继承Thread类了,所以还另外提供了实现Runnable接口的方式。

  1. public class Thread02 {
  2. public static void main(String[] args) throws InterruptedException {
  3. Dog dog = new Dog();
  4. Thread t1 = new Thread(new Dog());
  5. t1.start();
  6. System.out.println("what the fuck......");
  7. for (int i = 0; i < 10; i++) {
  8. System.out.println(i);
  9. Thread.sleep(1000);
  10. }
  11. }
  12. }
  13. class Dog implements Runnable {
  14. @Override
  15. public void run() {
  16. int times = 0;
  17. while (times++ < 10) {
  18. System.out.println("hi");
  19. try {
  20. Thread.sleep(1000);
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. }
  26. }
  27. 代码分析:由于Runnable接口只有run方法,start方法定义在Thread类中,所以还是需要Thread类的支持。需要去实例化一个Thread对象,在构造Thread时,需要传入一个实现了Runnable接口的实现类的对象。

10.2.4 两种方式的比较

推荐使用实现Runnable接口的方式来实现多线程,理由如下:

  1. 可以避免单继承的限制
  2. 使用继承接口的方式可以更好地共享资源

关于第二点使用一个案例来解释:

  1. // 模拟三个售票窗口出售100张票
  2. public class SellTicket {
  3. public static void main(String[] args) {
  4. // 1. 使用继承Thread类实现
  5. Window_1 w1 = new Window_1();
  6. Window_1 w2 = new Window_1();
  7. Window_1 w3 = new Window_1();
  8. w1.start();
  9. w2.start();
  10. w3.start();
  11. // 2. 使用实现Runnable接口的方式实现
  12. Window_1 w1 = new Window_1();
  13. Thread t1 = new Thread(w1);
  14. Thread t2 = new Thread(w1);
  15. Thread t3 = new Thread(w1);
  16. t1.start();
  17. t2.start();
  18. t3.start();
  19. }
  20. }
  21. class Window_1 extends Thread {
  22. // 1. 使用继承Thread类实现
  23. private static int ticket = 100; // 共同资源使用static修饰
  24. @Override
  25. public void run() {
  26. while (ticket > 0) {
  27. System.out.println(Thread.currentThread().getName() + ":" + ticket);
  28. ticket--;
  29. }
  30. }
  31. }
  32. class Window_1 implements Runnable {
  33. // 2. 使用实现Runnable接口的方式实现
  34. private int ticket = 100;
  35. @Override
  36. public void run() {
  37. while (ticket > 0) {
  38. System.out.println(Thread.currentThread().getName() + ":" + ticket);
  39. ticket--;
  40. }
  41. }
  42. }

代码分析:可以看出一个明显的区别是,当使用继承方式实现多线程时,实际上创建了多个类对象,由每个对象去调用start()开启一个线程,即7-12行所示;而当使用实现接口方式时,只创建了一个类对象,通过将该对象分别放入三个构造器,开启了多线程。这直接导致了两种方式对于ticket的修饰产生了差别,前者由于是多个对象共享ticket,所以使用static进行修饰;而后者始终在一个对象上操作,所以无需static修饰。也正因为后者在一个对象上操作,所以说后者更适合资源的共享。

10.3 线程终止

run()正常结束时,线程应该正常结束;但是还可以通过通知的方式来结束线程,以下案例展示了在主线程中通过设置loop的方式来通知子线程结束。

public class ThreadExit_ {

    public static void main(String[] args) {
        Person p = new Person();
        Thread t = new Thread(p);
        t.start();
        // 主线程休眠10s后通知子线程结束
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        p.setLoop(false);
    }

}

class Person implements Runnable {

    private boolean loop = true;
    int i = 0;

    @Override
    public void run() {
        while (loop) {
            System.out.println("i=" + i);
            i++;
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void setLoop(boolean loop) {
        this.loop = loop;
    }
}

10.4 线程的常用方法

- setName(String name): 设置线程名称
- getName(): 获取线程名称
- start(): 使线程处于就绪状态,底层调用start0
- run(): 线程实际执行的任务
- setPriority(): 设置线程优先级
- getPriority(): 获取线程优先级
- sleep(millisec): 线程休眠
- interrupt(): 中断线程(并不会终止线程,只是标明线程的状态为中断态)
- yield(): 当前线程让出cpu给别的线程执行
- join(): 等待当前线程结束

joinyield

10.4.1 用户线程和守护线程

用户线程:也称为工作线程,当线程任务执行完或通知方式结束;
守护线程:一般是为工作线程服务的,当所有工作线层结束后,守护线程结束,比如GC

10.5 线程的生命周期

10.5.1 线程的几种状态

线程的状态定义在Thread类中的State枚举类中,共有6种状态:

  1. NEW:线程未启动时,处于该状态
  2. RUNNABLE:对于一个可运行的线程,就处于这个状态
  3. BLOCKED:等待monitor lock的线程处于该状态
  4. WAITING:处于等待状态的线程
  5. TIMED_WAITING:等待另一个线程执行动作达到指定等待时间
  6. TERMINATED:已结束的线程处于该状态

10.5.2 状态转换图

图片.png

10.6 线程的同步

10.6.1 线程同步机制

在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就需要使用同步访问技术,保证数据再任一时刻,最多有一个线程访问,以保证数据的完整性

10.6.2 同步具体方法-Synchronized

实现同步有很多方法,这里仅介绍同步方法和同步代码块。

同步方法即使用Synchronized关键字修饰想要同步的方法;同步代码块同理,即将想要同步的代码块放在Synchronized包含的子块中。

// 同步方法
public Synchronized void func(param...) {
    ......
}

// 同步代码块
public void func(param...) {

    Synchronized ([object/class]){
        // 要同步的代码
    }
}

10.6.3 线程同步的原理-互斥锁

当一段代码或一个方法使用Synchronized修饰时,当多个线程对方法或代码块进行访问时,它们首先需要获取一把锁,这个锁是在对象上的,用对象的一个位来表示的。当一个线程获取该对象锁后,就改变这个位的状态,此时其他对象就无法再获取该锁。

对于同步代码块,它的锁对象可以是当前对象,也可以是其他任何对象,但是需要保证多个线程操作的是同一把锁。这里又区分两种情况:

  1. 如果是静态方法,那么锁就是该类本身(Classname.class
  2. 如果是成员方法,那么锁可以是该对象(this),也可以是其他任何对象

对于代码块也是同理,看代码块是位于静态方法中还是非静态方法中。
图片.png

10.7 死锁与释放锁

10.7.1 死锁

操作系统讲的很详细,这里不做介绍了

10.7.2 释放锁

下面的行为会释放锁:

  • 同步方法或同步代码块执行完了
  • 当前线程在同步方法或同步代码块中遇到了breakreturn
  • 当前线程在同步方法或同步代码块中遇到了未处理的ErrorException
  • 当前线程在同步方法或同步代码块中执行了线程对象的wait()

下面的行为不会释放锁:

  • 线程执行同步方法或同步代码块时,程序调用了Thread.sleep()Thread.yield()
  • 线程执行同步代码块时,其他线程调用了该线程的suspend(),即当前线程被挂起