多线程(一)

本文详细介绍了线程的概念,创建线程的不同方法,如继承Thread、实现Runnable接口、使用Lambda表达式和Callable,以及查看线程的工具。还探讨了线程状态、优先级、后台线程、线程结束和线程间的交互机制。
摘要由CSDN通过智能技术生成

一、概念

1. 线程是什么

一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码。

2. 为什么会出现线程

  1. 可以实现并发编程,且更轻量

    进程可以实现并发编程效果,但是太重了(消耗资源更多,速度更慢),如果需要大规模频繁地创建和销毁进程(普通用户涉及少,但是服务器上会有这种情况),创建(给进程分配资源)和销毁(释放资源)和调度的开销就会很大。为了解决这个问题,设计了线程(Thread 轻量级进程)ps.虽然是轻量级,但还是在消耗资源

    效果:创建进程的时候,只分配一个简单的PCB,而不去分配后续的这些内存硬盘资源,可以做到并发的执行任务以及提升创建/销毁的速度。线程搞出来是为了执行一些任务,执行任务有需要消耗这些硬件资源

  2. 操作(创建、销毁、调度)线程比进程更快

  3. 其他

    最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)

(1)池把一些要释放的资源先放到一个“池子里”,以备后续使用。申请资源的时候,也是先提前把要申请的资源申请好,也放到一个“池子里”,后续申请的时候也比较方便
(2)协程/纤程即“轻量级线程”,Java标准库目前还没有内置,有一些第三方库实现了。Go天然支持协程,且使用方便高效

3. 进程和线程的区别

  • 进程是包含线程的,每一个进程至少有一个进程存在,即主线程
  • 进程和进程之间不共享内存空间,同一个进程的线程共享同一个内存空间
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位

4. 线程的使用

  • 创建
    创建的还是进程,创建进程的时候,把资源(虚拟地址空间、文件描述符表)都分配好,后续创建的线程,让线程在进程内部,后续进程中的新的线程,直接复用前面进程这里创建好的资源。一个进程,至少要包含一个线程。最初创建出来的这个,可以视为是一个只包含一个线程的进程(此时创建的过程需要分配资源,此时第一个线程的创建开销可能是比较大的),但是后续再在这个进程里创建线程,就可以省略分配资源的过程(资源已经有了)

  • 描述

    • 线程同样也是通过PCB(进程控制块)来描述的
      • Windows上描述进程和描述线程的是不同的结构体,Linux开发者复用了PCB的结构体,来描述线程,所以会出现【一个PCB对应到一个线程或多个PCB对应一个进程】的情况
      • 同一个进程中的这些线程共用同一份资源(内存+硬件……),但是每个线程独立去CPU上调度,即同一个进程的多个PCB的内存指针、文件描述符表是一样的,但是其他都是不一样的。进程是操作系统进行资源分配的基本单位。线程,是操作系统进行调度执行的基本单位。
  • 缺陷

    • 随着线程数量的增加,整体的效率会再进一步的提高,但是这个提高并不是无限的提高(考虑CPU的逻辑核心数),线程太多的时候,线程调度的开销,反而会拖慢整个程序的效率
    • 线程安全问题,一旦某个线程执行过程中出现异常,并且这个异常还没有被很好的处理,此时就可能会导致整个进程直接被终止(包括进程里面的所有线程)

4. Java 的线程 和 操作系统线程 的关系

线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库),系统级别的线程,整个内存空间,任意一个线程都是可以访问的
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装

二、创建+查询线程

(1)线程本身是操作系统提供的概念,操作系统也提供了一些API供程序员来使用,java中就把操作系统的API进行了封装,提供了Thread类
(2)Java追求“跨平台”,JVM能够屏蔽不同的操作系统的差异

1. 创建线程

  • 每个线程都是一个独立的执行流,可以独立执行一段代码,即是“并发”的关系
  • 操作系统并发执行主线程和新线程
    • 系统在进行多个线程调度的时候,并没有一个非常明确的顺序,而是按着“随机”的方式进行调度,这种“随机”调度的过程,称为“抢占式执行”
  • 运行程序后,会先创建出一个java进程,这个进程中包含了至少一个线程(主线程),也就是负责执行main方法的线程。如果直接调用run方法,其实还是在主线程里,没有创建出一个新的线程

方法一:继承 Thread 类

直接把要完成的工作放到了Thread的run方法中

//Thread来自于java.lang这个包,不需要import
class MyThread extends Thread{
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread");

            //这里sleep方法会抛出一个异常
            // 不能throws,因为这里是方法重写,对于父类的run方法来说,是没有throws这个异常
            //所以重写的时候,也就不能throws该异常
            
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

public class Demo1 {
    public static void main(String[] args) throws InterruptedException{
        MyThread myThread = new MyThread();
        myThread.start();

        while(true){
            System.out.println("hello main");

            Thread.sleep(1000);
        }
    }
}

方法二:实现 Runnable 接口

把要完成的工作放到Runnable中,再让Runnable和Thread配合。成功把线程要执行的任务和线程本身,进一步解耦合了

class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("这是实现Runnable接口");
    }
}

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

        System.out.println("这里是主线程");

    }
}

方法三:继承Thread,基于匿名内部类

public class Demo3 {
    public static void main(String[] args) {
    //创建了一个子类实例t,继承自Thread,但是这个子类是匿名的,子类重写了run方法
    //类的创建在Demo3中
        Thread t = new Thread() {  
            @Override
            public void run() {
                System.out.println("基于匿名内部类");
            }
        };

        t.start();

        System.out.println("这是主线程");

    }
}

方法四:实现 Runnable 接口,基于匿名内部类

public class Demo4 {
    public static void main(String[] args) {
    //创建Runnable的子类,并且重写了run方法
    //然后把子类实例传给Thread的构造方法
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("实现Runnable,基于匿名内部类");
            }
        });

        t.start();
    }
}

方法五:使用lambda表达式

前面四种方法都太麻烦,java中要求“纯粹”地面向对象,即函数不能脱离类/对象而独立存在,但是对于创建线程来说,最需要的是run方法,类实际用处不大。lamdba相当于特办了一个“函数式接口”的概念。注意,本质上并没有增加新的语言特性,而是把以往能实现的功能以一种更加简洁的方式来编写。

public class Demo5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("这是lamdba表达式");
        });

        t.start();
    }
}
  • lambda 表达式本质上是一个“匿名函数”,这样的匿名函数主要可以用来作为“回调函数”来使用
    • 此处的“回调函数”是在线程创建成功之后,才真正执行的。“回调函数”不需要程序员主动调用,它会在合适的时机被自动地调用

方法六:基于Callable

Callable 是一个 interface . 相当于把线程封装了一个 “返回值”. 方便程序员借助多线程的方式计算结果

Callable 和 Runnable 相对, 都是描述一个 “任务”. Callable 描述的是带有返回值的任务,
Runnable 描述的是不带返回值的任务.
Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果(可以理解为小票). 因为
Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
FutureTask 就可以负责这个等待结果出来的工作

  • 创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
  • 重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果.
  • 把 callable 实例使用 FutureTask 包装一下.
  • 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的
  • call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
  • 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果
Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 1000; i++) {
            sum += i;
       }
        return sum;
   }
};

FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println(result);

方法七:基于线程池

2. 查看线程

(1)idea的调试器
在这里插入图片描述

(2)jconsole(jdk中提供的调试工具)
在这里插入图片描述

在这里插入图片描述

三、Thread及常用方法

1. Thread

(1) 概念
Thread 类是JVM用来管理线程的一个类,用来描述一个线程执行流,系统的一个线程就对应Java中的一个Thread对象,围绕线程的各种操作,都是通过Thread展开的。JVM会将这些Thread对象组织起来,用于线程调度、管理

(2)Thread的构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target) 使用 Runnable 对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象,并命名
【了解】Thread(ThreadGroup group,Runnable target)线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可

(3)Thread的几个常见属性

属性 获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()

ID:线程的身份标识,一个线程可以有好几个身份标识,正如一个人可以有多个称呼一样,JVM有一个,Pthread库(系统提供的操作线程的API)有一个,内核也有,这些身份标识彼此相互独立

状态:Java中的线程状态不同于操作系统中的

优先级:线程的调度主要还是由系统内核负责的,由于系统调度速度极快,所以设置/获取优先级作用并不是很大

后台进程:又名“守护线程”,后台线程不影响进程结束,前台进程会影响到进程结束(只有所有的前台进程执行完,进程才结束),如果前台进程都执行完了退出了,但是后台线程没有执行完,此时也会随着线程一起退出。

public class Demo7 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("hello thread");
            }
        });
        // 创建的线程默认是前台线程,可以通过setDaemon显式地设置成后台线程
        t.setDaemon(true);
        t.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

存活:即run方法是否运行结束了
Thread对象的生命周期和系统中的线程不完全一致。一般都是Thread对象先创建好,手动调用start后,内核才真正创建出线程。消亡的时候,可能是thread对象先没有引用指向,也可能thread对象还在,内核中的线程把run执行完了,就结束了

2.Thread 常用方法

(1)线程要执行的指令-run()

  • run只是上面的入口方法(主线程是main,其他线程是run/lambda),并没有调用系统API,也没有创建出真正的线程来
  • 线程具体跑起来之后,执行的就是run里面的代码。这里如果直接调用run方法,不会创建新的线程,仍然是在原来的主线程内部

(2)启动一个线程-start()

  • 在系统里真正创建出线程
    • 在底层调用操作系统提供的“创建线程”的API,系统内核就会在内核里创建出对应的PCB结构,并且加入到对应的链表中,此时这个新创建出来的线程就会参与到CPU的调度中,而这个线程接下来要执行的操作,就是调用重写的run方法
  • start 方法只是告诉系统创建出一个线程出来,执行是很快的,调用完start之后,代码就会继续执行start之后的逻辑

(3)终止一个线程

方法一:手动设置标志位

public class Demo3 {
    // 写作成员变量就不是触发变量捕获的逻辑了. 而是 "内部类访问外部类的成员" , 本身就是 ok 的
    public static boolean isQuit = false;

    public static void main(String[] args) throws InterruptedException {

        Thread t = new Thread(() -> {
            while (!isQuit) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();

        // 主线程这里执行一些其他逻辑之后, 要让 t 线程结束.
        Thread.sleep(3000);

        // 这个代码就是在修改前面设定的标志位.
        isQuit = true;
        System.out.println("把 t 线程终止");
    }
}

blog.csdnimg.cn/76d75227630a4ab8a01e93826ce0e05c.png)

解析
(1)因为lambda的执行时间是比较靠后的,所以可能当真正执行lamdba的时候,局部变量isQuit已经被销毁了。

(2)为了解决该问题,lambda引入了“变量捕获”这样的机制,即lambda内部看起来是在直接访问外部的变量,本质上是把外部的变量复制了一份,并放到lambda中。变量捕获有个限制:要求捕获的变量得是final,或者说没有修改的状态。因为Java是通过复制的方式来实现“变量捕获”,如果外面的代码修改了这个变量,那么就会出现【外面变量变了,里面没变】的歧义情况

(3)有的语言“变量捕获”是通过直接改变外部变量的生命周期来实现的,保证了lambda在执行的时候肯定能访问到外部的变量,此时该变量捕获就没有final的限制了

方法二:使用Thread提供的现成的标志位

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            // Thread.currentThread 能获取到当前线程的对象(t)
            // Thread对象内部提供了一个标志位(boolean),线程应该要结束(true),线程先不必结束(false)
            //lambda 表达式是在构造 t 之前就定义好的. 编译器看到的 lambda 里的 t 就会
            // 认为这是一个还没初始化的对象.所以不能用t.isInterrupted()
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();    //打印出了报错信息
                    break;
                }
            }
        });

        t.start();
        Thread.sleep(3000);
        
        // 把上述的标志位给设置成 true
        t.interrupt();
    }
}

(1)方法

现成标志位:Thread.interrupted() 或者 Thread.currentThread().isInterrupted(),使用这两个方法通知线程结束,然后通过run()里的标志位,让线程尽快结束

方法说明
public void interrupt()中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
public static boolean interrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位

(2)thread 收到通知的方式有两种:

  1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志((万一出现了标志位已经被删除的情况会很麻烦,给程序员留下更多的操作空间))
    • 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程(使用break)
    • Java做法是比较温和的(可以由本线程自己决定是否要终止)。激进一点的比如C++,刚提出请求就立即结束,完全不考虑本线程的实际情况,可能会产生负面影响
    • 手动设置的标志位,是没法唤醒阻塞状态的
  2. 否则,只是内部的一个中断标志被设置,thread 可以通过

(4)等待一个线程-join()

是一种规划【线程结束】顺序的手段,让原本由系统调度线程,无法确定【线程执行的顺序】变为确定的。

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

(1)A,B两个线程,如果希望B比A先结束,就可以让A线程中调用B.join()的方法,此时B线程还没执行完,A线程就会进入“阻塞”状态,相当于给B留下了执行的时间,B执行完毕之后,A再恢复过来,并且继续往后执行

(2)如果A执行到B.join的时候,B已经执行完了,A就不必阻塞了,直接往下执行就可以了

(5)获取当前线程引用

方法说明
public static Thread currentThread(); 返回当前线程对象的引用

(6)休眠当前线程-sleep()

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

    • Windows版本的休眠Sleep(ms),在Windows.h里。Windows上的头文件,是windows的系统函数

    • Linux版本的休眠sleep(s),unistd.h,Linux上的头文件,Linux的系统函数

    • Thread.sleep:把上述的系统函数进行封装了,如果是在Windows版本的jvm上运行,底层调用的是Windows的Sleep,如果是在Linux版本的jvm运行,底层就是调用Linux的sleep

  • 使用效果

    • 这俩线程在进行sleep之后,就会进入阻塞状态,当时间到了,系统就会唤醒这两个线程,并且恢复对这两个线程的调度,当这两个线程都唤醒了之后,谁先调度,谁后调度,可以认为是“随机的”,没什么规律,不一定保证概率均等
    • 本身存在一定误差:sleep(1000)并不是是精确的休眠1000ms,因为线程的调度也是需要时间的,所以sleep(1000):线程在1000ms之后,恢复为“就绪状态”
    • sleep(0):不改变线程的状态, 但是会重新去排队(让当前线程放弃CPU,直接准备下一轮的调度),相当于yield();
  • 24
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值