【多线程】初识多线程

📎前言

✨本篇文章是博主首次学习多线程相关内容而总结内容相关的博客,希望大家可以提供宝贵的意见,我定会积极采纳~ 后续我也将持续更新自己的学习记录,以此来督促我不断地学习与进步.希望大家关注支持!✨
🚀🚀系列专栏:【多线程】
📻📻本章内容:初识多线程

📌进程

进程是操作系统对一个正在运行的程序的一种抽象,换言之,可以把进程看做运行中的程序;

  • 在操作系统内部,进程又是操作系统进行资源分配的基本单位。
  • 每一个进程都有属于自己的存储空间与系统资源.

🍟进程的管理

1.先描述
使用PCB结构体表示进程内的各种属性
2.再组织
使用双向链表数据结构,把多个PCB结构体串起来

🍔PCB包含的属性

PCB指的是进程控制块(Process Control Block),它是操作系统用来管理进程的数据结构,包含了进程的各种信息,如进程状态、程序计数器、寄存器、内存分配情况、打开文件列表等。PCB包含了以下的属性以达到进程控制的目的.

  1. PID:进程的身份标识
  2. 内存指针
  3. 文件描述符表(操作文件就是操作硬盘)
  4. 进程的状态
  5. 进程的优先级
  6. 进程的上下文(中间状态存放到寄存器)
  7. 进程的记账信息(统计各进程调用多久了)

🍅并行与并发

  • 并行:同一时刻,两进程同时运行在两个CPU逻辑核心上
  • 并发:两进程在同一核心上交替着上
    由于CPU切换进程速度过快,微观上是串行的。宏观上看来这两进程就是同时进行的.因此二者通常统称为并发.

📌线程

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

📌多线程相关概念

🥪什么是多线程

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术.

🌭线程与进程的关系

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

在这里插入图片描述

🥐Java的线程与操作系统线程的关系

  • 线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
  • Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.接下来我们就要多次用到这里提到的Thread类.

📌多线程编程实现方式

🧀继承Thread类

  • 创建一个类
class MyThread extends Thread {
    @Override
    public void run() {//相当于 线程的入口方法  线程跑起来后要做什么都是run来描述的
        super.run();
        while (true) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();//创建线程.此操作会在底层调用操作系统提供的"创建线程"的API
                         //同时在操作系统内核中创建对应的PCB结构,并且加入到对应的链表中

        while(true) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

  • 基于匿名内部类
//通过匿名内部类,创建线程
public class Demo2 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                super.run();
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }
        };
        t.start();
    }
}

此处相当于是创建了一个子类,这个子类继承自Thread,但这个子类是没有名字的,这也是它叫作’匿名’的原因
另外注意,此内部类的创建是在当前的类里创建的,并使用 t 指向了该子类的实例,同时在子类中重写了run方法

🌮实现Runnable接口

  • 创建一个类
class MyRunnable implements Runnable {
        @Override
        public void run() {
            while (true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }


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

这种方法把要完成的工作放到Runnable中,再让Runnable与Thread结合.进一步的将线程本身和要执行的任务解耦合了

  • 基于匿名内部类
public class Demo4 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });//此处小括号代表Thread构造方法的结束
        t.start();
    }
}

此处相当于是创建了一个Runnable的子类(实现Runnable)
另外注意,该子类创建出实例后,把实例传给了Thread的构造方法.同时在子类中重写了run方法

🍱使用lambda表达式,表示run方法的内容(推荐使用)

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) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
    }
}
  • 引申:回调函数

lambda表达式本质上是一个"匿名函数",主要就可用来作为回调函数使用,回调函数不需要程序员自己去调用,而是在合适的时机被自动调用。
常用到回调函数的场景:
1.服务器开发:服务器收到一个请求触发一个对应回调函数
2.图形界面开发:用户某个操作触发一个对应的回调函数

🍤终止线程

  • 手动设置标志位
public class Demo8 {
    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) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();

        Thread.sleep(3000);

    	//修改标志位
        isQuit = true;
        System.out.println("线程终止");
    }
}
  • 引申:变量捕获

有一个客观存在的事实:如果 isQuit 定义于main方法中,而lambda表达式执行时机是更靠后的,这就会导致后续开始执行lambda时,isQuit变量可能已经被销毁了。因此lambda中引入了“变量捕获”这个机制,把外部的变量复制了一份到lambda中,从而解决生命周期的问题

  • 现成的标志位

Thread类提供了现成的标志位,可以不用手动设置

public class Demo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
        //currentThread()是获取线程引用的静态方法,此处的指代的就是t
           while (!Thread.currentThread().isInterrupted()) {
               System.out.println("hello thread");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
				/*break;*/
               }
           }
        });
        t.start();

        Thread.sleep(3000);

        t.interrupt();

    }
}


但是在运行该代码后我们发现了一个问题,在线程执行的过程中报出了如下的一个异常,更奇怪的是,该线程还在继续输出内容。
在这里插入图片描述
这是因为一个线程,在一个时刻可能是在执行,也可能是在sleep,那如果在sleep过程中将线程中断,是否需要把它“唤醒”呢。

当然是需要的。因此如果线程在sleep过程中,其他线程调用了interrupt方法,就会强制使sleep抛出一个异常,sleep也就被立即唤醒。但是在被唤醒的同时,会自动清除前面的标志位。 循环也会继续执行。此时如果想让线程结束,直接在catch中加break即可。
在这里插入图片描述
此时我们发现线程就会在报出异常后中断。

🍛等待线程

多线程执行时是并发的,具体的执行过程由操作系统负责调度。因此无法确定线程执行的先后顺序,因此引入了等待线程的方法,也就是规划线程结束顺序的一种手段.用到了join()方法

public class Demo10 {
    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("hello a");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("a ending...");
        });

        Thread b = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("hello b");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("b ending...");
        });

        a.start();
        b.start();
    }
}

通过上述代码可以看出b的循环次数要少于a,所以b线程会早于a线程结束。但如果我想手动干预使a线程先结束呢?

那么只要在b线程中加入a.join()方法,此时,a线程还没有执行完,b线程就会进入’阻塞’状态,就相当于给a留下了执行的空间,a进程执行完后,b才会从阻塞状态中恢复,并继续往后执行(如果b执行到a.join()时,a已经执行完毕了那就不会进入到阻塞状态,直接往下执行)
代码如下:

public class Demo10 {
    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("hello a");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("a ending...");
        });

        Thread b = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("hello b");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            //让b进入阻塞状态
            try {
                a.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("b ending...");
        });

        a.start();
        b.start();
    }
}

  • 阻塞
    怎么理解这个阻塞呢?我们可以认为阻塞即使该线程暂时不去CPU上执行调度。
    因此sleep也可以实现线程的阻塞,但阻塞是有时间限制的。而上面用到的join()方法则是永久的等待。
    那我们就要考虑到这种“死等”的方式是否合适呢?明显是不合适的,在计算机中时间力度很强,如果无休止的等待可能会造成资源的浪费,因此我们建议限制等待的最大时间。如下方法:
    在这里插入图片描述

📌线程的状态

线程的状态是一个枚举类型,我们可以通过getState()方法来查看.

NEW安排了工作, 还未开始行动
RUNNABLE可工作的. 又可以分成正在工作中和即将开始工作
BLOCKED因为锁产生阻塞了
WAITING因为调用wait产生阻塞了
TIMED_WAITING因为sleep产生阻塞
TERMINATED工作完成了

我们通过一段代码来观察一下。

//线程的状态
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        System.out.println(t.getState());//NEW
        t.start();

        System.out.println(t.getState());//RUNNABLE

        t.join();

        System.out.println(t.getState());//TERMINATED
    }

}

三个获取状态的位置分别就代表安排了工作但未行动、正在工作与工作结束。
以下就是各状态间的相互转移与关系
在这里插入图片描述


🎉总结

🎇到这里本篇文章就结束了,感谢大家的阅读。非常希望大家可以提出宝贵的建议!!
🎇本篇文章是博主对自己学习中所学知识的个人理解。日后我也将不断更新自己学习过程以达到温故知新。
🎇希望大家可以点赞+收藏+评论支持一下噢!
🎇继续保持关注噢~

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jester.F

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值