并发编程1:多线程的基本概念

一、进程、线程、用户线程&原生线程、优先级、守护线程

什么是进程

  • 是程序一次执行的过程,是系统运行程序,和向操作系统申请资源的的基本单位。
  • 系统运行一次程序,就是一个进程从创建到关闭的过程。
  • Java 项目从 main 方法启动,就是启动了一个 JVM 进程,而 main 函数就是由进程中的一个线程负责执行,这个线程称为主线程。

什么是线程

  • 线程和进程相似,但是是一个比线程更小的单位,线程间的切换比进程的切换负担小得多,所以线程也称为轻量级进程。
  • 一个进程执行时可以有多个线程,JAVA 线程之间可以共享堆和方法区资源,但是每个线程有自己的程序计数器、虚拟机栈、和本地方法栈
  • 举例:一个进程是一个对外营业的饭店,而线程是其中工作的厨师、服务员等,饭店对外提供服务,厨师和服务员各自工作相互配合。

什么是用户线程&原生线程

  • 用户线程:JDK1.2 之前 JVM 使用的是绿色线程自己模拟的多线程,这就是用户线程。用户线程对比原生线程使用起来有一些限制。用户线程由用户空间程序管理和调度,运行在用户空间。JDK1.2 之后,JAVA 线程,就是用的是内核线程了。
  • 内核线程:由操作系统内核管理和调度的线程,运行在内核空间,可以操作系统提供的异步 I/O

什么是守护线程&非守护线程

  • JAVA 中可以在线程 start 前使用 setDaemon() 方法指定其为守护线程,守护线程和非守护线程最大的区别在于他们如何影响程序的结束。
  • 非守护线程执行完毕时,程序就会结束执行。不论是否有守护线程仍在执行。

线程优先级

可以对线程设置优先级,默认优先级为 Thread.NORM_PRIORITY(5),优先级从 1~10 优先级越大优先执行的概率越高但并不能保证执行顺序。

多线程和上下文切换

  • 线程在执行过程中,会有自己的运行条件和状态这就是线程的上下文。例如 Java 线程私有的程序计数器,虚拟机栈、本地方法栈。
  • 线程在执行过程中可能出现阻塞、等待、或者 CPU 时间片执行完毕,的情况要让出 CPU 等待下一次执行。这种情况下就会发生线程切换,下一次执行时又要恢复线程的数据,这就是上下文切换
  • 因为需要上下文切换,如果是 CPU 密集型任务,线程越多反而有更多的性能损耗;

二、线程的状态和切换

线程的状态定义在 Thread.states 枚举中

线程状态介绍

2023_12_5.jpg

  • NEW:Thread对象已经创建,但是还没有开始执行,线程只能被执行一次所以一个线程只可能有一次处于该状态。
  • RUNNABLE:对应原生线程的两种状态,RUNNING 和 READY,前者属于正在运行状态,后者可能是等待 cpu 调度,分配时间片后执行。READY 状态也称为活跃线程,线程执行中调用 yelid() 让出时间片有可能从 RUNNING 进入 READY 状态。
  • BLOCKED : 线程等待阻塞式的 I/O 操作,或者申请一个加锁的资源时线程会处于该状态。该状态的线程不会占用处理器资源,阻塞 I/O 结束后,线程可以再次回到 RUNNABLE 状态。
  • WAITING:线程满足执行条件执行某些特定方法后,就会进入该状态。例如 Object.wait()、Thread.join()、LockSupport.park(Object)。执行其他方法可以从 WAITING 中返回 RUNNABLE 状态,例如 Object.notify()、Object.notifyAll()、LockSupport.unpark(Object);
  • TIME_WAITING:和 WAITING 类似,但是可以在一定时间内自动转换为 RUNNABLE 状态。。
  • TERMINATED:线程正常运行结束、或者抛出异常结束。

Java 的线程状态没有区分 操作系统层面的 Running 和 Ready 状态,而是只有 RUNNABLE 状态的原因

  • 因为 现在的 CPU 都讲 CPU 资源进行分片循环调度,由于时间片非常小一般是 10~20ms,线程切换的非常快所以区分 Ready 和 Running 意义不大

线程的终止方式

  1. 调用 stop 方法强行终止,但是是强行终止线程可能任意时刻被中指导致任务不完整,资源未释放等问题。
  2. 线程中保存 volite 的退出标识,线程中循环判断该标识,是否需要终止线程,和 interrupt 很类似。
  3. 使用线程的中断标识,interrupt,可以通过isInterrupted()和 interrupted() 来感知外界传入的中断状态,决定是否中断线程。interrupt 碰上 sleep 或者 wait 状态 则一定会抛出异常
class Task implements Runnable {
    @Override
    public void run() {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("子线程正在运行...");
                Thread.sleep(1000); // 让子线程休眠1秒
            }
        } catch (InterruptedException e) {
            System.out.println("子线程被中断");
        } finally {
            System.out.println("子线程结束");
        }
    }
}

三、线程的创建 Runnable 接口 和 Thread 类

线程可以直接集成 Thread 类,也可以实现 Runnable 接口将 Runnable 对象作为参数创建 Thread

Runnable 接口

  • Runnable 接口只有一个 run 方法,用来记录线程需要执行的任务内容

Thread 类

  • 获取和设置 Thread 对象信息
    • getId():该方法返回Thread对象的标识符。该标识符是在钱程创建时分配的一个正整数。在线程的整个生命周期中是唯一且无法改变的。
    • getName()/setName():这两种方法允许你获取或设置Thread对象的名称。这个名称是一个String对象,也可以在Thread类的构造函数中建立
    • getPriority()/setPriority():你可以使用这两种方法来获取或设置Thread对象的优先级。
    • isDaemon()/setDaemon():这两种方法允许你获取或建立Thread对象的守护条件
    • getState():该方法返回Thread对象的状态。
  • interrupt():中断目标线程,给目标线程发送一个中断信号,线程被打上中断标记。
  • interrupted():判断目标线程是否被中断,但是将清除线程的中断标记。
  • **isinterrupted():**判断目标线程是否被中断,不会清除中断标记。
  • **sleep(long ms):**该方法将线程的执行暂停ms时间。
  • **join():**暂停线程的执行,直到调用该方法的线程执行结束为止。可以使用该方法等待另一个Thread对象结束。举例 : A 线程中执行 b.join() 则执行流程为 A->暂停->b 开始执行->b 执行完毕->A 继续
  • setUncaughtExceptionHandler() 当线程执行出现未校验异常时,该方法用于建立未校验异常的控制器。
  • currentThread():Thread类的静态方法,返回实际执行该代码的Thread对象。
  • suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
  • yield() 方法使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。

线程的启动

  1. 调用 Thread 对象的 start 方法来启动线程
  2. 线程属于一次性用品,一个 Thread 对象的 start 方法只能调用一次

四、 Callable 接口和 Runnable 接口

Callable 接口是一个和 Runnable 接口非常相似的接口

  • Callable 接口内部是带有返回值的 call()方法,而 Runnable 接口是没有返回值的 run()方法。
  • Callable 接口一般配合 Future 来一起使用,使用 Future 来包装 Callable 对象,实现 callable 的异步执行和后续的结果获取。

public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable myCallable = new MyCallable();
    //future接口不能被异步执行,FutureTask实现了RunnableFuture接口,间接实现了Runnable接口
    FutureTask<String> stringFuture = new FutureTask<String>(myCallable);
    //使用Thread执行或者提交给线程池执行
    Thread thread = new Thread(stringFuture);
    System.out.println("外部调用任务开始" + new Date());
    thread.start();
    //这里阻塞获取结果,如果出结果了可以直接获取,否则需要等待任务执行完毕
    System.out.println("获取结果:" + stringFuture.get());

}

/**
* 这是自定义的future类
*
* @author liuyp
* @date 2023/11/19
*/
static class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println("任务执行开始,预计耗时5s");
        Thread.sleep(5000);
        return "我是任务执行的结果";
    }
}

五、多线程的优势和风险

优势

  1. 提高系统吞吐量:一个系统可以多个并发操作,不会因为某个流程阻塞而整个程序等待。
  2. 提高响应速度:多个操作可以使用多个线程执行,不会因为某个任务速度慢而阻塞其他操作。
  3. 利用多核处理器资源:多线程有利于充分利用设备多核的处理器性能。

风险

  1. 线程安全问题:对共享数据需要做并发控制,避免出现脏读、和丢失更新或其他一致性问题。
  2. 现成活性问题:线程可能陷入死锁(互相持有对方需要的资源陷入永久等待)、活锁(某种巧合导致线程永远尝试某个操作但无法成功)、线程饥饿(线程永远等不到执行机会)等问题;
  3. 上下文切换:处理器从一个线程转向执行另一个线程,需要切换到对应的线程执行状态需要切换上下文这也会带来更多的系统消耗
  4. 可靠性:,某个线程的错误崩溃可能导致进程的意外终止。
posted @ 2023-12-03 23:29  青花石  阅读(21)  评论(0)    收藏  举报  来源