JavaEE初阶学习:多线程的初步学习

Posted Monody·

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaEE初阶学习:多线程的初步学习相关的知识,希望对你有一定的参考价值。

1.线程的定义和创建

1.线程的概念

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

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

线程是轻量级的进程,在一个进程内部可以存在一个或多个线程,进程与进程之间是不能共享内存的,进程之间的消息通信不方便,但是一个进程内部的线程之间是共享这个进程的内存空间的,线程之间通信很方便。

为了方便大家理解,我们举个例子:

一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。

如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。

此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。

2.为什么要引入线程

如以上讲的,线程之间共享内存。比如,一个文字输入软件,其内部可以有三个线程,一个用来响应鼠标、键盘的交互线程,一个用来运算,另一个用来备份。因为进程之间不共享内存,所以不能用多个进程来实现这时就用多线程可以解决。线程之间共享内存,所以从一个线程切换到另一个线程不需要陷入内核,也不需要切换上下文,线程之间的切换比进程切换快捷。

3.进程和线程的区别

  • 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

  • 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

  • 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

  • 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

  • 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

  • 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

比如之前的多进程例子中,每个客户来银行办理各自的业务,但他们之间的票据肯定是不想让别人知道的,否则钱不就被其他人取走了么。而上面我们的公司业务中,张三、李四、王五虽然是不同的执行流,但因为办理的都是一家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最大区别。

4.线程的创建

1. 继承 Thread 类

  1. 继承 Thread 来创建一个线程类
  2. 创建 MyThread 类的实例
  3. 调用 start 方法启动线程
class MyThread extends Thread 
    @Override
    public void run() 
        System.out.println("hello Thread!");
    


public class Demo1 
    public static void main(String[] args) 
        MyThread t = new MyThread();
        t.start();
    

2.实现 Runnable 接口

  1. 实现 Runnable 接口
  2. 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
  3. 调用 start 方法
class MyRunnable implements Runnable 
    @Override
    public void run() 
        System.out.println("hello Thread!");
    

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

对比上面两种方法:

  • 继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
  • 实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用Thread.currentThread()

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

public class Demo3 
    public static void main(String[] args) 
        Thread t1 = new Thread() 
            @Override
            public void run() 
                System.out.println("使用匿名类创建 Thread 子类对象");
            
        ;
        t1.start();
    

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

public class Demo4 
    public static void main(String[] args) 
        Thread t2 = new Thread(new Runnable() 
            @Override
            public void run() 
                System.out.println("使用匿名类创建 Runnable 子类对象");
            
        );
        t2.start();
    

5.lambda 表达式创建 Runnable 子类对象

public class Demo5 
    public static void main(String[] args) 
        Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
        Thread t4 = new Thread(() -> 
            System.out.println("使用匿名类创建 Thread 子类对象");
        );
        t3.start();
        t4.start();
    

2.Thread类及其方法

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

用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

1.Thread构造方法

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("为线程对象命名!");
Thread t4 = new Thread(new MyRunnable(), "为线程对象命名!");

2.Thread常见属性

  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况,下面我们会进一步说明
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行
  • 是否存活,即简单的理解,为 run 方法是否运行结束了
  • 线程的中断问题,下面我们进一步说明
public class Demo6 
    public static void main(String[] args) 
        Thread thread = new Thread(() -> 
            for (int i = 0; i < 10; i++) 
                try 
                    System.out.println(Thread.currentThread().getName() + ": 存在");
                            Thread.sleep(1 * 1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
            System.out.println(Thread.currentThread().getName() + ": 消失");
        );
        System.out.println(Thread.currentThread().getName()
                + ": ID: " + thread.getId());
        System.out.println(Thread.currentThread().getName()
                + ": 名称: " + thread.getName());
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
        System.out.println(Thread.currentThread().getName()
                + ": 优先级: " + thread.getPriority());
        System.out.println(Thread.currentThread().getName()
                + ": 后台线程: " + thread.isDaemon());
        System.out.println(Thread.currentThread().getName()
                + ": 活着: " + thread.isAlive());
        System.out.println(Thread.currentThread().getName()
                + ": 被中断: " + thread.isInterrupted());
        thread.start();
        while (thread.isAlive()) 
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
    

3.线程的启动

之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。

  • 覆写 run 方法是提供给线程要做的事情的指令清单
  • 线程对象可以认为是把 李四、王五叫过来了
  • 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。

调用 start 方法, 才真的在操作系统的底层创建出一个线程.

4.线程的中断

大哥一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那大哥该如何通知二弟停止呢?这就涉及到我们的停止线程的方式了。

目前常见的有以下两种方式:

  1. 通过共享的标记来进行沟通
  2. 调用 interrupt() 方法来通知

1.使用自定义的变量来作为标志位

  • 需要给标志位上加 volatile 关键字
ublic class Demo7 
    private static class MyRunnable implements Runnable 
        public volatile boolean isQuit = false;
        @Override
        public void run() 
            while (!isQuit) 
                System.out.println(Thread.currentThread().getName()
                        + ": 正在转账!");
                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
            System.out.println(Thread.currentThread().getName()
                    + ": 停止转账");
        
    
    public static void main(String[] args) throws InterruptedException 
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "二弟");
        System.out.println(Thread.currentThread().getName()
                + ": 让二弟开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 老板来电话了,通知二弟对方是个骗子!");
        target.isQuit = true;
    

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

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

  • 使用 thread 对象的 interrupted() 方法通知线程结束.
public class Demo8 
    private static class MyRunnable implements Runnable 
        @Override
        public void run() 
            // 两种方法均可以
            while (!Thread.interrupted()) 
                //while (!Thread.currentThread().isInterrupted()) 
                System.out.println(Thread.currentThread().getName()
                        + ": 正在转账!");
                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName()
                            + ": 有内鬼,终止交易!");
                    // 注意此处的 break
                    break;
                
            
            System.out.println(Thread.currentThread().getName()
                    + ": 停止转账!");
        
    
    public static void main(String[] args) throws InterruptedException 
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "二弟");
        System.out.println(Thread.currentThread().getName()
                + ": 让二弟开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 老板来电话了,通知二弟对方是个骗子!");
        thread.interrupt();
    


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

  1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志
    (1) 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.
  2. 否则,只是内部的一个中断标志被设置,thread 可以通过
    (1)Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
    (2)Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。

3.观察标志位是否清除

标志位是否清除, 就类似于一个开关.

Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 “清除标志位”

Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为
“不清除标志位”.

  • 使用 Thread.isInterrupted() , 线程中断会清除标志位.
public class Demo9 
    private static class MyRunnable implements Runnable 
        @Override
        public void run() 
            for (int i = 0; i < 10; i++) 
                System.out.println(Thread.interrupted());
            
        
    
    public static void main(String[] args) throws InterruptedException 
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "二弟");
        thread.start();
        thread.interrupt();
    


只有一开始是 true,后边都是 false,因为标志位被清除

  • 使用 Thread.currentThread().isInterrupted() , 线程中断标记位不会清除.
public class Demo10 
    private static class MyRunnable implements Runnable 
        @Override
        public void run() 
            for (int i = 0; i < 10; i++) 
                System.out.println(Thread.currentThread().isInterrupted());
            
        
    
    public static void main(String[] args) throws InterruptedException 
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "二弟");
        thread.start();
        thread.interrupt();
    


全部是 true,因为标志位没有被清除

5.线程的等待

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,二弟只有等三弟转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。

public class Demo1 
    public static void main(String[] args) throws InterruptedException 
        Runnable target = () -> 
            for (int i = 0; i < 3; i++) 
                try 
                    System.out.println(Thread前言
  • 线程是什么?
  • Java中执行多线程编程
  • Thread 类创建线程的写法
  • 多线程的优势
  • Thread 类常见方法
  • 线程等待
  • 获取当前线程引用
  • 线程休眠
  • 线程的状态
  • 线程安全问题 - 重中之重
  • 重点解析 - synchronized 关键字 - 监视器锁 monitor lock
  • Java标准库中的线程安全类
  • volatile 关键字
  • wait 和 notify :等待 和 通知
  • 小结
  • 关于多线程的案例
  • 本文结束
  • 前言

    建议看一下上篇博客开头篇:计算机是如何工作的中 关于PCB【Process Control block - 进程控制块】 属性的那一部分。

    线程是什么?

    谈到线程,就不得不说一下进程【进程包含线程】。
    思考一个问题:为什么要有进程?
    这是因为我们的操作系统支持多任务。程序员也就需要“并发编程”。【这里并发是宏观的,包含了并发和并行】
    通过多进程,是完全可以实现并发编程的。
    但是存在问题:如果需要频繁的 创建 / 销毁 进程,这个事情成本是比较高的!同时如果频繁的调度进程,这个事情的成本也是比较高的!

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


    Java中执行多线程编程

    在 Java 标准库中,就提供了一个 Thread 类,来 表示 / 操作 线程。
    Thread 类也可以视为是 Java 标准库提供的 API(API:Thread 类提供的方法 和 类)。

    当我们创建好 Thread 实例 / 对象,其实和 操作系统中的线程是一一对应的关系。
    换句话来说:
    如果我们想要创建一个线程,就得先创建出一个 Thread 类 的 对象。
    创建10个线程,就需要创建出 10 个 Thread 类的对象。

    进一步来说:操作系统提供了一组关于线程的API【C语言风格】。
    Java对于这组API 进一步封装了一下,就成了 Thread 类。


    下面我们在 idea 中实践

    准备工作 : 在 idea中 创建一个 Java 项目


    Thread 类的基本方法

    通过 Thread 类创建线程,写法 有很多中。
    其中最简单的做法,创建子类,继承自 Thread,并且重写 run 方法。


    另外,这里创建的线程,都是在同一个进程内部创建的。

    其实很好理解,假设进程是一个厂,线程是一条流水线。
    我想新增一条流水线,不可能建在别人厂里,人家又不傻。
    肯定是建在自己的厂里。
    而且由于独立性,进程之间是不能相互干扰的。

    另外,其实我们这个例子打印出效果是不太理想的。
    因为 线程之间 是 并发执行的,而我们从上面示例是看不出来的。
    下面我们重新创建一个 Class 类 来表达 线程之间的并发执行。
    知识点:异常


    拓展

    有的人可能会有疑问:为什么 Thread 没有导入包 也能用。
    这是因为:但凡是 java.lang 包里的类,都不用手动导入包,类似还有String类。

    sleep 方法 参数的单位是 ms的情况,时间的精确度就没有那么高了。
    也就是说:sleep(1000) 并不是正好在 1000ms 之后就上CPU,可能是 998,又或者是 1003上CPU执行的。
    又或者说CPU正处理其它事情,没空搭理你,导致拖延一些时间。


    Thread 类创建线程的写法

    1、最基本的创建线程的办法

    这个写法就是上面举个例子。【创建子类,继承自 Thread,并且重写 run 方法。】

    // 最基本的创建线程的办法
    
    class MyThread extends Thread
        @Override
        public void run() 
            System.out.println("hello thread");
        
    
    
    public class Test 
        public static void main(String[] args) 
    
            Thread t = new MyThread();
            t.start();
        
    
    

    2、创建 一个类 实现 Runnable 接口,再创建Runnable实例 传给 Thread 实例

    通过 Runnable 来描述任务的内容
    进一步的再把描述好的任务交给 Thread 实例

    // 这里面的 Runnable 就是在描述一个 “任务”
    class MyRunnable implements Runnable
        @Override
        public void run() 
            System.out.println("hello");
        
    
    
    public class Test2 
        public static void main(String[] args) 
            // 需要注意的是 这里 我们实例化是 Thread 类本身,
            // 只不过构造方法里给指定了 MyRunnable 实例
            Thread t = new Thread(new MyRunnable());
            t.start();
        
    
    


    写法 3 和 写法 4 :就是上面两种写法的翻版 - 使用了匿名内部类。

    知识点:内部类

    写法3

    创建了一个匿名内部类,继承自 Thread 类。
    同时重写 run 方法 和 new 了
    同时再 new 了 个 匿名内部类的实例。
    【也就是 t 所指向的实例】

    
    public class Test3 
        public static void main(String[] args) 
            Thread t = new Thread()
                @Override
                public void run() 
                    System.out.println("hello thread");
                
            ;
            t.start();// 此处仍然是调用 start 来开启线程
        
    
    


    写法4

    这一次,我们是针对 Runnable 接口 创建的 匿名内部类(这个匿名内部类实现了Runnable 接口)。
    同时 将创建出的实例 作为 参数 传给了 Thread 的构造方法

    public class Test4 
        public static void main(String[] args) 
            Thread t = new Thread(new Runnable() 
                @Override
                public void run() 
                    System.out.println("hello thread");
                
            );
            t.start();
        
    
    


    小结

    通过上面的四种写法,我们认识 Thread 方法 和 Runnable 方法。
    那么这两种方法,哪一个更好?
    通常 认为 Runnable 这种方法更好一点!
    它能够做到让 线程 和 线程 执行的任务,更好的进行解耦(解除耦合)
    我们写代码一般希望:高内聚(同一类功能的代码放在一起),低耦合(不同的功能模块之间,没有关联关系)。

    其实,我们在使用 Runable 方式 来创建线程的时候,就把当前的线程要执行的任务 与 整个线程的概念给分开了。

    换句话说: Runnable 只是单纯的描述了一个任务,至于这个任务是要通过一个进程来执行,还是线程来执行,还是线程池来执行,还是协程来执行,都无所谓!
    Runnable 本身并不关心,Runnable 里面的代码也不关心。

    就好像通缉令悬赏一个罪犯,是谁抓住的不重要,重要的是内容是否 被 完成 / 执行。


    写法五

    相当于 第 4 种写法的延伸 》》 进一步简化 - lambda表达式
    说白了:使用 lambda 表达式 代替 Runnable。

    public class Test5 
        public static void main(String[] args) 
        //() 表示无参数的run 方法(Runnable 的 run 方法)
        // -> 表示 这是一个lambda 表达式
        // lambda 表达式里面 具体内容  
            Thread t = new Thread(()-> 
                System.out.println("hello thread");
            );
            t.start();
        
    
    


    多线程的优势

    多线程能够提高任务完成的效率。
    为了证明 多线程的完成任务的效率。


    我们下面一起来实践一下

    假设:
    现有两个整数变量,分别要对这辆变量,进行自增 10 亿次。
    分别使用一个线程 和 两个线程。
    我们通过这种方式来体现多线程的效率


    总程序

    public class Test6 
        private static  final  long count =10_0000_0000;
        public  static void serial()
            // 记录程序自增开始的时间
            long begin = System.currentTimeMillis();
            long a = 0;
            for(long i = 0;i < count;i++)
                a++;
            
            long b = 0;
            for (long i = 0; i < count; i++) 
                b++;
            
            // 记录自增程序结束时间。
            long end = System.currentTimeMillis();
            System.out.println("花费的时间:"+ (end - begin) + "ms");
        
    
        public static void concurrency() throws InterruptedException 
            long begin = System.currentTimeMillis();
            Thread t1 = new Thread(()->
               long a = 0;
                for (long i = 0; i < count; i++) 
                    a++;
                
            );
            t1.start();
            Thread t2 = new Thread(()->
               long b = 0;
               for(long i = 0;i < count;i++)
                   b++;
               
            );
            t2.start();
    
            // join 效果 就是等待线程结束。
            t1.join();// 让 main线程 等待 t1 线程执行结束
            t2.join();// 让 main线程 等待 t2 线程执行结束
            long end = System.currentTimeMillis();
            System.out.println("花费时间:" + (end - begin));
        
    
        public static void main(String[] args) throws InterruptedException 
            serial();
            concurrency();
        
    
    
    

    很明显 多线程 比 单线程 效率 大概高出了 3 分之 1.【数据量更大的话,效果更明显】
    多线程 与 单线程 的效率差距 还是特别明显的!
    但是! t1 和 t2 在底层中,是 并发执行, 还是并行执行。是不确定的!
    多线程在真正并行执行的时候,效率才会有显著的提升!
    另外,多线程在数据量庞大的情况下,效率的提升才是最明显!反而数据量很少的情况下,效率还会有所降低。因为创建线程也是有开销的。

    所以讲到这,大家一定要明白一件事。
    就是 多线程,它不是万能良药,不是说使用了多线程,代码的执行效率就一定能提高!还需要看使用场景!!!!

    多线程特适合那种 CPU 密集型的程序:程序需要进行大量的计算,使用多线程就可以更充分的CPU的多核资源。

    换句话来说,
    我们使用多线程来提升程序的效率的前提是:这个任务是由CPU来完成的,并且我们需要进行大量的计算,让计算机的所有核心都工作起来。


    Thread 类常见方法

    Thread 的常见构造方法

    z只讲解一些主要的方法

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

    Thread(String name) - 创建线程对象,并命名

    这个构造方法是给线程(thread 对象)起一个名字。
    需要注意的是:起一个什么样子的名字,不影响线程的本身的执行。
    取得名字要贴合使用场景,不能瞎取名字。
    因为乱取线程名字,会影响到 程序员 对 代码 的 后续调试。
    因为程序员在调试的时候,可以借助一些工具看到每个线程以及名字,很容易在调式中对线程做出区分。


    Thread 的几个常见属性

    属性获取方法
    ID(身份标识)getId()
    名称(就是我们上面构成方法给新城指定的名字)getName()
    状态(线程程的状态)getState()
    优先级(线程的优先级) getPriority()
    是否后台线程isDaemon()
    是否存活isAlive()
    是否被中断isInterrupted()

    是否后台线程 - isDaemon()

    如果线程是后台线程,就不影响进程退出;
    如果线程是前台线程,就会影响到进程退出。
    【我们刚才在程序中创建的t1 和 t2 就是前台线程】
    即使main 方法执行完,进程也不能退出,得等 t1 和 t2 都执行完。
    整个程序才能退出!!!!

    如果 t1 和 t2 是 后台线程,此时如果main执行完毕,整个进程就直接退出,
    t1 和 t2 就被强行终止。


    是否存活 - isAlive()

    判断 操作系统中对应的编程是否正常运行。
    Thread t 对象的生命周期 和 内核中对应的线程,生命周期并不完全一至。
    因为创建出 t 对象之后,在调用 start之前,系统中是是没有对应线程的。
    进一步来说,在 run 方法执行完了之后,系统中的线程就销毁了,但是 t 这个 对象 可能 还存在。

    所以我们就可以通过 isAlive 来 判断 当前系统的线程的运行情况。
    如果 调用 start 之后,run执行完之前,isAlive 就返回 true
    入股 调用 start 之前,或者run执行完之后,isAlive 就返回 false


    Thread 中的一些重要方法

    start 方法 - 启动线程

    start 决定了系统中是不是真的创建出线程。


    经典面试题: start 与 run方法 的区别

    start 操作就是在创建新的线程,run 就是一个普通方法 描述一个任务的内容。


    中断一个线程

    一般通过 Tread 来创建的线程,想让一个线程停下来的关键,就是要让线程对应 run 方法 执行完。【这是中断线程的关键】

    还有一个特殊的,就是main这个线程。
    对于 main线程 来说,必须要等到 main 方法执行完,线程才能结束。


    让线程结束有以下几个方法:

    1、可以手动的设置一个标志位(自己创建一个变量,boolean 和 int 类型都行),来控制线程是否要执行的结果。


    结论:在其它线程中控制某个标志位,就能影响对应线程的运行的状态(提前中止该线程的运行)。

    另外,此处因为多个线程共用一个虚拟地址空间!
    因此,main 线程 修改的 isQuit 和 t 线程判定 的 isQuit 是同一个值。

    但是,如果是在进程的那种情况下,在不同的虚拟地址的情况下,这种写法就会失效。


    2、使用 Thread 中内置的一个标志位来进行判断来进行判定(比第一种方法更好)

    上面那种写法其实还存在着一个问题:标志位的写法,还不够严谨,存在某些问题。

    这样写,只能保证 在上面的程序中运行,可能在其他程序中就没有效果了。

    这时候,我们就需要使用 第二种方法:使用 Thread 中内置的一个标志位来进行判断来进行判定:
    1、Thread.interrupted(); 【这是一个静态方法】
    2、Thread currentThread().isInterrupted() 【这是一个实例方法,其中 currentThread 能够获取当前线程的实例】
    推荐使用第二种方法!!

    public class Test10 
        public static void main(String[] args) 
            Thread  t = new Thread(()->
                // Thread.currentThread() 获取目前所在线程 t
                // isInterrupted() 判断 线程 t 是否中断
                // 中断返回 true,再根据 !取反,为 false,跳出循环,从而结束 run任务,致使线程t中断结束执行
                // 执行中返回 false,,再根据 !取反,为 true,执行 run 的 输出语句。
                while(!Thread.currentThread().isInterrupted())
                    System.out.println("hello thread");
                    try 
                        Thread.sleep(1000);
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                
            );
            t.start();
    
            try 
                Thread.sleep(5000);// 在main线程中,5s之后,执行下面 代码t.interrupt()
             catch (InterruptedException e) 
                e.printStackTrace();
            
            // 在主线程中,调用 interrupt 方块,来中断这个线程
            // t.interrupt 的 意思是: t线程被中断
            t.interrupt();
        
    
    

    但是呢!运行的结果 与我们想象的不同!
    期望:在 5s之后,线程 t 被中断
    实际:5s之后,编译器报出一个异常,线程 t 继续执行,线程t还没有终止。

    也就是说: t.interrupt() 不仅仅是针对 while循环的条件(标记位) 进行操作,它还可能触发一个异常。

    需要注意的是:在使用 interrupt 方法的时候,我们的interrupt可能会有两种情况,而这两种情况都是需要考虑到的。
    如果我们线程中没有什么代码导致线程进入阻塞状态的操作,直接一个循环判断就是够了。
    如果有,我们就需要借助 catch代码块(处理异常),在里面进行添加相应的操作。

    Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记.:
    public boolean isInterrupted() :判断对象关联的线程的标志位是否设置,调用后不清除标志位.【第三个方法,这个实际开发中 常用的 写法】
    【我们上面while循环调用 interrupted方法是第三个,线程 t. interrupt 是 第一个】
    毕竟一个代码中的线程有很多个,随时哪个线程都可能会终止
    Thread.interrupted() 这个方法判定的标志位置是 Thread 的 static 成员。
    又因为 一个程序中只有一个标志位,很显然这么多的线程,一个标志位怎么够用。

    Thread.currentThread().isinterrupted() 这个方法判定的标志位 是 Thread 的 普通成员,每个示例都有自己的标志位。【一般无脑用这个方法即可】


    线程等待

    前面说到:多个线程之间,调度的顺序是不确定的。(顺序取决于系统)
    但是这样的不确定性 并不好。有的时候,我们是需要让线程有明确顺序的。

    换个说法:
    线程之间的执行是按照调度器来安排的,这个过程可以视为是“无序,随机”。
    这样不太好,有时候,我们需要能够控制线程之间的顺序。

    线程等待就是其中一种,控制线程执行顺序的手段
    此处的线程等待,主要是控制线程结束的先后顺序。

    其实 join 也是其中的一种
    调用 join 的时候,哪个线程调用的 join,那个线程就会阻塞等待。
    等到对应的线程执行完毕为止(对应线程的 run 执行完)


    获取当前线程引用

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

    哪个线程调用的这个currentThread,就获取到哪个线程的实例。


    线程休眠

    就是 sleep,前面也用了很多。
    这里,我们进一步的解析它。
    所谓的 线程休眠的具体作用是什么?

    回顾
    进程是通过 PCB 来描述。
    进程是通过 双向链表来组织的。
    前面的说法是针对只有一个线程的进程,是这种情况。
    但如果是一个进程有多个线程,此时每个线程都有一个PCB。
    也就是过更为准确的说:一个进程 对应的就是 这一组PCB了。
    然后 PCB 上有一个字段 tgroupld,这个 id 其实就相当于 进程的 id,同一个进程中的若干个线程的 tgroupld 是相同。

    那么 PCB - process control block 进程控制块 和 线程有什么关系?
    其实在linux系统中 内核是不缺分进程和线程。
    只是程序员在写应用程序代码的时候,弄出来的词。
    实际上 linux 内黑 指认 PCB!!!
    在内核里 linux 把 线程 称为 轻量级进程。


    线程的状态

    主要就介绍两个状态:
    1、就绪
    2、阻塞

    前面所讲的进程状态,其实都是指的是系统按照“什么样子的态度”来调度这个进程
    但是!这种说法并不是很严谨!
    上面的说法时针对一个进程中只有一个线程的情况。

    更常见的情况:一个进程包含多个线程。
    所谓的状态其实是绑定在线程上。【前面讲的例子都在透露这个信息】
    与线程休眠想表达出的意思一样:Linux 中认为 PCB 和 线程一一对应,一个进程对应一组PCB。

    状态本来就是 PCB 的 一个属性,现在正好每个线程都有着各自的PCB,也就说每个线程都有属于自己的状态。
    因此,我们系统在调用线程的时候,就可以根据每个线程不同的状态,来确定 是否调度该线程。
    并且我们还可以通过状态来更好的区分线程。

    上面说的 “就绪” 和 “阻塞” 都是针对系统层面上的线程的状态【PCB的状态】。
    在Java中,尤其是在 thread 类中,又对线程的状态进行了进一步的细化。


    1、NEW: 安排了工作, 还未开始行动

    把 Thread 对象创建好了,但是没有调用start方法。


    2、TERMINATED: 工作完成了.

    操作系统中的线程已经执行完毕,销毁了。
    但是 Thread 对象还在,此时获取的状态就是 terminated。


    小结


    3、RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.

    RUNNABLE 就是 我们常说的就绪状态。
    处于这个状态的线程,就是在 就绪队列中。
    随时可以被被调度到 CPU 上。
    对 就绪状态 的线程,有两种情况:
    1、正在被执行
    2、还没有执行,但是随时可以调度它。

    如果

    以上是关于JavaEE初阶学习:多线程的初步学习的主要内容,如果未能解决你的问题,请参考以下文章

    JavaEE初阶多线程 _ 基础篇 _ Thread类的使用线程的几个重要操作和状态

    JavaEE之多线程01

    多线程初阶

    初步学习多线程2

    初步学习多线程3

    python学习对多线程的初步了解

    (c)2006-2024 SYSTEM All Rights Reserved IT常识