【多线程】多线程基础知识

x33g5p2x  于2022-08-17 转载在 其他  
字(5.4k)|赞(0)|评价(0)|浏览(583)

1. 认识线程(Thread)

进程和线程的关系和区别:

  1. 线程是包含在进程中的,每个进程至少有一个线程存在,即主线程。
  2. 每个线程都是一个独立的执行流,可以各自执行自己的任务,多个线程之间“同时”执行多个任务。
  3. 同一个进程的每个线程之间公用同一份资源(包括虚拟地址空间+文件描述符)
    线程A创建的变量,线程B可以用到。线程B打开的文件,线程A可以用到。
  4. 不同进程之间不能共享内存空间。
  5. 从操作系统的角度可以认为,进程是系统资源分配的基本单位,线程是系统调度执行的基本单位。

线程存在的意义:

由于并发编程成为刚需,虽然多进程能够实现并发编程,但是线程比进程更加轻量,能够更加充分的利用多核 CPU 的资源:

  • 线程创建比进程创建更快
  • 线程销毁比进程销毁更快
  • 线程调度比进程调度更快

线程可以视为是轻量级进程,但随着创建和销毁频率提高,线程也就显得比较重了,进一步的解决方案如下:

  • 使用线程池
  • 使用协程,也叫做纤程

Java 的线程和操作系统线程的关系:

  • 线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户层提供了一些 API 供用户使用。
  • Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行性了进一步的抽象和封装。

2. Thread 类及常见方法

Thread 类是 JVM 用来管理线程的一个类,每个创建的 Thread 实例都是和系统中的一个线程是对应的,用来描述一个线程执行流,JVM 会将 Thread 对象组织起来,用于线性调度和线程管理。

2.1 Thread 常见构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
Tread(ThreadGroup group, Runnable target)线程可以被用来分组管理,分好的组几位线程组

Java 中实现多线程主要有以下两种方式:

  • 继承 Thread 类
  • 实现 Runnable 接口
    定义:

Runnable 接口时线程辅助类,仅定义了一个 fun() 方法。该方法中描述了要实现的线程的内容。

优点:

  • Runnbale 可以继承其它类实现对 Runnable 实现类的增强,避免了直接继承 Thread 类而无法继承其他类的问题。
  • Runnable 接口可以被多个线程共享,适用于多个进程处理一种资源的问题。

Thread 用法一:

创建一个类,继承自 Thread,重写 Thread 中的 run 方法,该 run 方法内部包含了这个线程要执行的代码,当线程跑起来时就会依次来执行这个 run 方法中的代码。

// 定义的这个类描述了要创建出的线程是啥样的,但是并未真正创建出来
class MyThread1 extends Thread{
    @Override
    public void run() {
        System.out.println("hello thread!");
    }
}

public class Demo1 {
    public static void main(String[] args) {
        // 1. 创建 Thread 实例,此处创建的 MyThread1
        MyThread1 t = new MyThread1();
        // 2. 调用 Thread 的 start 方法,这会在系统内部真正创建出线程
        t.start();
    }
}

Thread 用法二:

创建一个类,实现 Runnable 接口,重写 run 方法。创建 Thread 实例,然后把刚才的 Runnable 实例给设置进去。

class MyRunnable implements Runnable{

    @Override
    public void run() {
        // 描述这个任务具体要进行的工作
        System.out.println("hello thread2!");
    }
}

public class Demo2 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
}

Thread 用法三:

在用法一的基础上改造,采用匿名内部类。

public class Demo3 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("hello thread3!");
            }
        };
        t.start();
    }
}

Thread 用法四:

在用法二的基础上改造,采用匿名内部类。

public class Demo4 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello thread4!");
            }
        });
        t.start();
    }
}

Thread 用法五:

在用法二的基础上改造,搭配 lambda 表达式。

public class Demo5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello thread5!");
        });
        t.start();
    }
}

2.2 start 和 run 方法的区别

  • 调用 start 方法能够在操作系统的底层创建一个新的线程,新线程中会执行 run 方法。
  • 覆写 run 方法是提供线程要做的事情的指令清单,本身并不具备创建线程的能力。

如果对创建的线程对象调用 run 方法,那么并没有创建出一个新线程,并在新线程中执行指定的任务,而是在原本的线程中执行指定的任务,并只有在指定任务结束后才会执行后续的操作。

2.3 Thread 常见属性

属性补充获取方法
IDID 是线程唯一标识getId()
名称名称是便于调试的,JVM 会默认指定名称getName()
状态状态表示线程当前所处的一个情况getState()
优先级理论上优先级越高越容易被调度到getPriority()
是否后台线程JVM 会在一个进程的所有非后台线程结束后,才会结束运行isDaemon()
是否存活表示 run 方法是否结束运行,NEW 和 TERMINATED 状态是未存活。isAlive()
是否被中断isInterrupted()
返回当前线程对象的引用以上属性在被调用时,要先通过 Thread 类的 currentThread() 方法获取到正在被执行的线程引用才能够调用。currentThread()

补充:如果是通过继承 Thread 来创建的线程,此时直接在 run 方法中通过 this,就能够拿到这个线程的实例。

2.4 中断线程

线程只有执行完 run 方法中的内容才会结束,但如果该线程执行的内容是一个死循环,并且涉及到风险,那么就要能够手动停止线程了。

目前常见的中断线程的方式如下:

  • 手动设置标志位,来作为循环结束的判定条件(需要给标志位加上 volatile 关键字,该方法存在缺陷)。
    volatile 的作用如下:

  • 保证变量的内存可见性

  • 禁止指令重排序

public class Demo6 {
    // 通过这个变量来控制线程是否结束
    public volatile static boolean isQuit = true;

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(isQuit) {
                System.out.println("hello thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isQuit = false;
        System.out.println("hello main!");
    }
}

  1. 借助 Thread 实例中自己提供的一个标志位,来作为循环结束的判定条件,方式如下:
方法说明
public void interrupt()中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位为 true。
public static boolean interrupted()判断当前线程的中断标志位是否设置,调用后清楚标志位。
public boolean isInterrupted()判断当前线程的中断标志位是否设置,调用后不清楚标志位。

对于以上方法设置标志位,Thread 收到通的方式有两种:

  1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,至于要不要结束线程或者有其它操作,则取决于 catch 中的代码内容,如 break 跳出循环结束线程。
  2. 否则,只是内部的一个中断标志被设置为 true。
public class Demo7 {

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });

        t.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
        System.out.println("hello main!");
    }
}

2.5 等待线程

有时我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。通过 join 方法,就能够起到阻塞线程的作用,从而达到手动控制线程执行顺序的一种办法(join 是控制结束顺序)。

方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,最多等待 millis 毫秒
public void join(long millis, int nanos)等待线程结束,最多等待 millis 毫秒,精度更高

注意:在 main 线程中调用 t.join() 方法,是让 main 线程来等待 t 线程执行完毕。

应用:使用 Scanner 进行输入操作时,就运用了阻塞等待。

2.6 休眠线程

通过 sleep 方法就能够使调用 sleep 方法的线程进入阻塞等待的状态,至于阻塞的时间则取决于 sleep 方法中设置的时间参数。

方法说明
public static void sleep(long millis) throws InterruptedException休眠当前线程 millis 毫秒
public static void sleep(long millis, int nanos) throws InterruptedException休眠当前线程 millis 毫秒,精度更高

注意:因为线程的调度是不可控的,所以这个方法只能够保证实际休眠时间是大于等于参数设置的休眠时间的。

一个进程中有多个线程,每个线程对应一个 PCB,即一个进程对应一组 PCB。操作系统调度 PCB 的时候,会从就绪队列中挑出一个 PCB 去 CPU 上执行,而处于阻塞队列中的 PCB 是不会被系统调度到 CPU 上执行的。而调用了 sleep 方法后,该线程就会被移动到阻塞队列中,只有到达了 sleep 的睡眠时间,该 PCB 才会被移动回就绪队列,但此时并不意味着该线程就能够在 CPU 上执行了,因为线程调度是不可控的。

3. 线程的状态

3.1 线程的所有状态

线程的状态是一个枚举类型 Thread.State,具体状态如下:

状态说明补充
NEW安排了工作,还未开始行动Thread 创建出了对象,但是内核里面的线程还没创建,即未调用 start 方法。
RUNNABLE可以工作的,又可以分成正在工作中和即将开始工作就绪状态
BLOCKED表示排队等着其它事情阻塞状态,等待锁的时候产生的状态。
WAITING表示排队等着其它事情阻塞状态,通过 wait 方法产生的状态。
TIMED_WAITING表示排队等着其它事情阻塞状态,通过 sleep 方法产生的状态。
TERMINATED工作完成了内核里的线程已经结束了,然后 Thread 创建的对象还在。

通过下面这段代码,就能够将 Java 线程的所有状态打印出来:

public class Demo8 {
    public static void main(String[] args) {
        for(Thread.State state: Thread.State.values()){
            System.out.println(state);
        }
    }
}

3.2 线程状态转移图

|

相关文章