03.JAVAEE之线程1

1. 认识线程(Thread)

1.1 概念

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

1.2 引入线程的原因 

引入多个进程,初心?

实现并发编程 => 多核 CPU 的时代=>可以同时执行多个任务

进程可以满足并发编程,但是效率很低,这时候我们引入了线程。

【进程太重量,效率不高.

创建一个进程,消耗时间比较多

【消耗在申请资源上的.进程是资源分配的基本单位.

分配内存操作,就是一个大活~

操作系统内部有一定的数据结构,把空闲的内存分块管理好,当我们去进行申请内存的时候,系统就会从这样的数据结构中找到一个大小合适的空闲内存,返回给对应的进程

这里虽然通过此处的数据结构,可以一定程度提高效率,整体来说,管理的空间比较多,相比之下还是一个耗时操作.
销毁一个进程,消耗时间也比较多
调度一个进程消耗时间也比较多】

如果需要频繁的创建/销毁进程,这个时候,开销就不能忽视

为了解决上述问题,就引入了"线程"(Thread)
线程也叫做"轻量级进程”
创建线程,比创建进程,更快;
销毁线程,比销毁进程, 更快,
调度线程,比调度进程, 更快

【线程不能独立存在,而是要依附于进程,(进程包含线程)
进程可以包含一个线程,也可以包含多个线程(一个进程,最开始的时候,至少要有一个线程这个线程负责完成执行代码的工作.也可以根据需要, 创建出更多的线程,从而使当前实现"并发编程"的效果)】

【结构】一个进程,使用 PCB 表示,一个进程可能使用一个 PCB 表示,也可能使用多个 PCB 表示每个 PCB 对应到一个线程上(状态,优先级,上下文,记账信息....每个线程都有这些信息,辅助调度)(除此之外, 前面谈到的 pid,是相同的.内存指针, 文件描述符表,也是共用同一份的)

上述结构,决定了,线程的特点:
1.每个线程都可以独立的去 CPU 上调度执行,
2.同一个进程的多个线程之间,共用同一份内存空间,和文件资源...

创建线程的时候,不需要重新申请资源了直接复用之前已经分配给进程的资源.省去了资源分配的开销,于是创建效率就更高了

进程是资源分配的基本单位

线程是调度执行的基本单位 

一个系统中,可以有很多进程
每个进程, 都有自己的资源.
一个进程中,可以有很多线程

每个线程都能独立调度,共享内存/硬盘资源 

总结:

1.首先, "并发编程" 成为 "刚需".

  • 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源. 
  • 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程. 

2.其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量. 

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

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

1.3 进程和线程的区别)(总结)

  • 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。进程同时可以有多个线程。
  • 进程和线程, 都是用来实现 并发编程 场景的,但是线程比进程更轻量,更高效
  • 同一个进程的线程之间,共用同一份的资源(内存+硬盘),省去了申请资源的开销
  • 进程和进程之间,是具有独立性的,一个进程挂了,不会影响到别人线程和线程之间(前提是同一个进程内),是可能会相互影响的.(线程安全问题 + 线程出现异常)
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。

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

线程是操作系统中的概念.

操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库). 

Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.  

2.多线程程序

感受多线程程序和普通程序的区别:
每个线程都是一个独立的执行流
多个线程之间是 "并发" 执行的. 

  • java.lang下的类不用import 
  • run是线程的入口方法

每个线程都是一个独立的执行流~~
每个线程都可以执行一系列的逻辑(代码)
一个线程跑起来,从哪个代码开始执行?

就是从它的入口方法入口

一个 Java 程序的 入囗是 main 方法一样~~
【运行 Java 程序,就是跑起来一个 java 进程,这个进程里面至少会有一个线程,主线程,主线程的入口方法就是main 方法. 】

//Thread.sleep

//sleep是Thread的静态方法 

 

  • 把上述代码改成 while (true)可以看到, 这两个 while 循环在"同时执行看到的结果,是两边的日志都在交替打印
  • 每个线程,都是一个独立执行的逻辑.(独立的执行流) 
  • 兵分两路,并发执行(并行+并发)->达到并发编程的效果->更好的利用多核
  • 这俩线程都是休眠 1000ms, 当时间到了之后,这俩线程谁先执行, 谁后执行,不一定!!
    这个过程可以视为是"随机"的.
    操作系统,对于多个线程的调度顺序,是不确定的,"随机"的.(此处的随机,不是数学上"概率均等"这种随机,取决于 操作系统 对于线程调度的模块 (调度器)具体实现)

把 t.start 改成 t.run此时,代码中不会创建出新的线程,只有一个 主线程.这个主线程里面只能依次执行循环执行完一个循环再执行另一个 

t.start兵分两路,一部分往下(main自动创建的线程,与别的线程相比没有什么特殊的,一个Java进程至少有一个main线程),一部分创建新的线程

多线程程序运行的时候,可以使用 IDEA 或者jconsole(jdk带有的程序) 来观察到该进程里的多线程情况 

找到jdk所在路径

  • 在jconsole,可以看到一个 Java 进程即使是最简单的,里面也包含了很多的线程
  • 只有Thread-0是Thread t = new MyThread();自己手动创建的,其他的线程都是 JVM 自动创建的
  • 一个 Java 进程启动之后,NM 会在后面,默默的帮咱们做很多的事情(比如,垃圾回收,资源统计, 远程方法调用...)
  • 线程的详细信息
  • 未来写一些多线程程序的时候,就可以借助这个功能能看到该线程实时的运行情况比如,写的程序"卡死了

2.1 Thread 类的其他用法

创建线程,其他的写法

1.继承 Thread, 重写 run


2.实现 Runnable, 重写 run (Runnable是接口不是类)

1) 实现 Runnable 接口

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
   }
}

2) 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数. 

Thread t = new Thread(new MyRunnable()); 

3) 调用 start 方法

t.start(); // 线程开始运行 

class MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

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

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Runnable 表示的是一个"可以运行的任务这个任务是交给线程负责执行,还是交给其他的实体来执行...... Runnable 本身并不关心~~

使用 Runnable 的写法, 和 直接继承 Thread 之间的区别, 主要就是解耦合

创建一个线程,需要进行两个关键操作:
1.明确线程要执行的任务

任务本身,不一定和线程概念强相关的这个任务只是单纯的执行一段代码,这个任务是使用单个线程执行,还是多个线程执行,还是通过其他的方式(信号处理函数/协程/线程池.....)都没啥区别~~(可以把任务本身给提取出来~此时就可以随时把代码改成使用其他方式来执行这个任务)
2. 调用系统 api 创建出线程 

 3.匿名内部类创建 Thread 子类对象

匿名内部类创建 Thread 子类对象

// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Thread 子类对象");
   }
};

4.匿名内部类创建 Runnable 子类对象

// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Runnable 子类对象");
   }
});

5.lambda 表达式创建 Runnable 子类对象(lambda自身就是run方法,所以不用重写run)

// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
    System.out.println("使用匿名类创建 Thread 子类对象");
});

lambda 表达式,本质上是一个匿名函数,主要用来实现回调函数”的效果

Java 中不允许函数独立存在的(其他语言叫函数 function, Java 这里叫方法 method)
lambda 本质函数式接口.(本质上还是没有脱离类)

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

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 2. Thread 类及常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。

2.1 Thread 的常见构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名

创建线程的时候,可以去指定 namename 不影响线程的执行,

只是给线程起个名字后续在调试的时候,比较方便区分 

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字"); 

2.2 Thread 的几个常见属性 

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

  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。(后台线程是否结束,不影响整个进程的接收,前台线程会影响进程结束)(默认情况下一个线程是前台线程)(t.setDaemon(true)即就设置为后台线程了)
  • 是否存活,即简单的理解,为 run 方法是否运行结束了(Thread 对象的生命周期,要比系统内核中的线程更长一些~~Thread 对象还在,内核中的线程已经销毁了这样的情况~~)
  • 线程的中断问题

2.3 启动一个线程(start)

start 方法,start 方法内部,是会调用到系统 api,来在系统内核中创建出线程,

run 方法,就只是单纯的描述了该线程要执行啥内容.(会在 start 创建好线程之后自动被调用的)

二者之间的差别就是是否创建了新的线程

2.4 中断一个线程(interrupt)

常见的有以下两种方式:

1. 通过共享的标记来进行沟通

// 线程的打断
public class Demo8 {
    private static boolean isQuit = false;

    public static void main(String[] args) throws InterruptedException {
        // boolean isQuit = false;

        Thread t = new Thread(() -> {
            while (!isQuit) {
                // 此处的打印可以替换成任意的逻辑来表示线程的实际工作内容
                System.out.println("线程工作中");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程工作完毕!");
        });

        t.start();
        Thread.sleep(5000);

        isQuit = true;
        System.out.println("设置 isQuit 为 true");
    }
}

当前咱们这个代码,是使用了一个 成员变量 isQuit,来作为标志位如果把 isQuit 改成 main 方法中的局部变量,是否可以呢?? 

不可以

lambda 表达式,有一个语法规则,变量捕获,lambda 表达式里面的代码,是可以自动捕获到上层作用域中涉及到的局部变量的~~

所谓的变量捕获, 其实就是让 lambda 表达式把当前作用域中的变量在 lambda 内部复制了一份!!(此时,外面是否销毁, 就无所谓了)

变量捕获

Java 中,变量捕获语法, 还有一个前提限制,就是必须只能,捕获一个 final 或者是实际上是 final 的变量

变量虽然没有使用 final,但是却没有修改内容就是"事实上的 final'。

上述方案,不够优雅.
1.需要手动创建变量.
2.当线程内部在 sleep 的时候, 主线程修改变量,新线程内部不能及时响应. 

2. 调用 interrupt() 方法来通知

// 线程终止
public class Demo9 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            // Thread 类内部, 有一个现成的标志位, 可以用来判定当前的循环是否要结束.
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("线程工作中");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // 1. 假装没听见, 循环继续正常执行.
                    e.printStackTrace();
                    // 2. 加上一个 break, 表示让线程立即结束.
                    // break;
                    // 3. 做一些其他工作, 完成之后再结束.
                    // 其他工作的代码放到这里.
                    break;
                }
            }
        });
        t.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("让 t 线程终止. ");
        t.interrupt();
    }
}

使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位. 

 Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记

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

t.interrupt();//这个操作,就是把上述 Thread 对象内部的标志位设置为 true 了.

即使线程内部的逻辑出现阻塞(sleep)也是可以使用这个方法唤醒的~~

正常来说,sleep 会休眠到时间到, 才能唤醒,此处给出的 interrupt 就可以使 sleep 内部触发一个异常,从而提前被唤醒,(这是手动设置标志位无法实现的)

//但存在一个问题

异常确实是出现了.sleep 确实是唤醒了但是上述 t仍然在继续工作!! 并没有真的结束!!
interrupt 唤醒线程之后,此时sleep 方法抛出异常,同时会自动清除刚才设置的 标志位这样就使"设置标志位"这样的效果就好像没有生效一样~~

【这么设定的原因】
Java 是期望, 当线程收到"要中断"这样的信号的时候,他能够自由决定,接下来怎么处理~~

就可以让咱们有更多的"可操作性空间
可操作性空间的前提,是通过"异常"的方式唤醒的
如果没有 sleep,则没有上述操作空间,(此时没有异常,目的就非常明确,如果有 异常,就需要在出现异常之后,再确认一下)

2.5 线程等待join()

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。

让一个线程,等待另一个线程执行结束,再继续执行.本质上就是控制线程结束的顺序

join 实现线程等待效果,

主线程中,调用 t.join()

1.此时就是主线程等待 t线程先结束

package thread;

public class Demo10 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("t 线程工作中!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();

        // 让主线程来等待 t 线程执行结束.
        // 一旦调用 join, 主线程就会触发阻塞. 此时 t 线程就可以趁机完成后续的工作.
        // 一直阻塞到 t 执行完毕了, join 才会解除阻塞, 才能继续执行
        System.out.println("join 等待开始");
        t.join();
        System.out.println("join 等待结束");
    }
}

t.join 工作过程:
1)如果t线程正在运行中,此时调用 join 的线程就会阻塞,一直阻塞到t线程执行结束为止

2)如果t线程已经执行结束了,此时调用 join 线程, 就直接返回了.不会涉及到阻塞~~ 

3)可以设置超时时间(一般来说不建议死等)

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

2.6 获取当前线程引用 

public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
   }
}

2.7 休眠当前线程

也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的

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



 

  • 28
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值