线程的概念及如何用java创建线程

一. 线程

并发执行, 是指为了利用多核CPU, 我们要把要完成的任务, 拆解成多个部分, 并且分别让他们在不同的CPU上运行, 那么此时进程会被调度到不同的CPU上执行
此时, 每个客户端连上服务器, 服务器都会创建一个进程, 客户端断开, 再把进程释放掉, 那么如果这个服务器, 频繁地有客户来来去去, 服务器也需要频繁地创建和销毁进程, 此时就会带来新问题, 创建和销毁进程的花销太大了!!!
此时就引用了线程(thread), 线程创建和销毁的开销更小
可以理解为线程是"进程的一部分", 一个进程, 可以包含一个线程, 或者多个线程

事实上吗更严格的来说, 一个PCB其实是描述一个线程的

  1. pid 每个线程都不一样
  2. 内存指针 同一个进程的若干个线程, 这里的内存指针其实是同一个
  3. 文件描述表 同一个进程的若干个线程, 这里的文件描述表其实是同一个
  4. 状态, 优先级, 上下文, 记账信息, 每个线程都有自己的属性
  5. tgid 同一个进程的tgid是同一个

同一个进程中的若干个线程之间, 是公用内存资源和文件资源的(线程1中new了一个对象, 线程2重视可以直接访问到的; 线程1中打开了一个文件, 线程2也是可以直接使用的), 但是每个线程都是独立在CPU上调度执行的
进程是系统资源分配的基本单位
线程是系统调度执行的基本单位

为什么说, 线程比进程更轻量?为啥说线程创建/销毁的开销, 比进程小?
核心就在于, 创建进程, 可能要包含多个线程, 这个过程, 涉及到资源分配/资源释放
而创建线程, 相当于资源已经有了, 省去了资源分配/释放的过程了

多线程编程值得关注的难点: 一个线程抛出异常, 没有很好的捕获处理好, 就会使整个进程退出(其他线程也就没了), 而相比之下, 独立性更好, 一个进程异常, 不会影响到其他进程

面试题: 线程和进程的区别?

二. 多线程代码

线程, 本身是操作系统提供的, 操作系统提供了api, 让我们可以操作线程, JVM就对操作系统进行了封装, 提供了Thread类

  1. 创建线程
    在这里插入图片描述
  2. 重写run方法
    run方法的作用, 就是描述了线程具体干什么活
    在这里插入图片描述
  3. 启动线程
    调用start方法在这里插入图片描述

此时运行的结果为:
在这里插入图片描述

上述代码中其实有两个线程

  • thread线程
  • main方法所在的线程(主线程)
    jvm进程启动的时候, 自己创建的线程

我们把代码修改一下:
在这里插入图片描述
在run中和main中分别加入两个死循环, 运行程序, 让进程一直执行下去
我们通过java提供的工具, 能更清楚的看到代码中的线程
在这里插入图片描述
选择我们创建的进程, 并连接
在这里插入图片描述
选择线程
在这里插入图片描述
我们就可以看到:
在这里插入图片描述
Thread线程是我们手动创建的
main是执行main方法的线程
剩下的都是jvm帮我们做的一些其他工作涉及到的线程, 有的负责垃圾回收, 有的负责记录调试信息…

运行上述代码, 消耗了大量的CPU资源, 主要是因为while循环太快了!

sleep

在这里插入图片描述
这个方法, 可以让线程主动进入阻塞状态, 主动放弃CPU上执行
括号中的单位为毫秒 1000ms => 1s
时间到了之后, 线程才会解除阻塞状态, 重新被调度到CPU上执行

把鼠标放在sleep上发现会抛出受查异常, 需要我们try-catch一下
在这里插入图片描述
在这里插入图片描述
由于打印操作消耗的时间非常短, 相比于sleep1s, 可以忽略不计了, 因此加上sleep可以使CPU消耗的资源大幅度降低了

未来实际开发中, 发现你负责的服务器程序, 消耗的CPU资源超出预期, 你如何排查这个问题?
  1. 首先需要确认, 是哪个线程消耗的CPU比较高(未来会涉及到一些第三方工具, 可以看到每个线程的CPU消耗情况)
  1. 确定了之后, 进一步排查, 线程中是否有类似"非常快速"的循环
  1. 确认清楚, 这里的循环是否应该这么快?
    如果应该, 说明你们需要升级更好的CPU
    如果不应该, 说明需要再循环中引入一些"等待"操作(不一定是sleep)

在这里插入图片描述
运行之后我们发现, 每秒钟会打印一次hello main 和 hello thread
两两一组我们发现, 打印的顺序不是固定的, 有可能main在前面有可能thread在前面

多个线程的调度顺序, 是"无序"的, 在操作系统内部称为"抢占式运行"
任何一个线程, 在执行到任何一个代码的过程中, 都可能被其他线程抢占掉他的资源, 于是CPU就给别的线程执行了
这样的抢占式执行, 充满了随机性, 使多线程的程序的执行结果也难以预测, 甚至可能会引入bug
现在主流的操作系统(windows, linux…)都是抢占式执行
也有一些小众的系统(实时操作系统), 通过"协商式"进行调度, 如vxworks(风河 windriver公司)比较知名的实时操作系统

start

start是Thread里面自带的方法, 它的主要任务是:
调用操作系统提供的"创建线程"的api
在内核中创建对应的pcb, 并把pcb加入到链表中
当系统调度这个线程后, 就会执行上述run方法中的逻辑

注意: run不是start调用的, run是start创建的线程, 在线程里被调用的

如果我们调用run方法执行:
在这里插入图片描述
此时, 并没有创建新的线程, 就是在主线程中执行上述run中的循环, 此时执行的结果:
在这里插入图片描述
此时run和主线程的循环, 是串行执行的, 不是并发执行, 所以只会打印hello thread, 只有当run中循环结束, 才会执行main循环

像run方法这样, 只是定义好, 而不去手动调用, 把这个方法的调用, 交给 系统 / 其他的库 / 其他框架(别人) , 这样的方法称为"回调函数"(callback function)

创建线程的写法

1. 创建Thread子类, 重写run方法

就是上述我们用的方法

class MyThread extends Thread{
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class threadDemo1 {
    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();
        while(true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

2. 通过实现Runnable接口创建线程

创建NyRunnable类实现Runnable接口, 重写run方法
将MyRunnable对象作为参数传给Thread对象

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 threadDemo2 {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}

Runnable的任务是描述一个任务, run是要执行的任务本身

刚刚的第一种写法, 是线程Thread自己记录任务
而第二种写法, Thread本身没有记录任务, 由别人Runnable来记录的
引入Runnable的目的, 其实就是为了解耦合!
把任务内容和线程拆分开, 这样的任务, 就可以给其他地方执行, 当前是通过多线程的方式执行的, 未来也可以方便改成基于线程池方式执行, 也可以改成基于虚拟线程的方式执行(改动过程很简单)

3. 对于Thread, 使用匿名内部类实现

匿名内部类是java中的特殊语法, 这个类没有名字, 是在别的类中定义的
在这里插入图片描述
这里并不是new Thread, 此处是几个操作合并在一起了

  1. 创建了一个Thread子类(不知道名字, 匿名)
  2. 同时创建了一个该子类的实例,
    对于匿名内部类来说, 只能创建这一个实例, 这个实例创建完了之后, 再也拿不到这个匿名内部类了
  3. 此处的子类内部重写了run方法
public class threadDemo3 {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        thread.start();
    }
}

4. 对于Runnable, 使用匿名内部类实现

在这里插入图片描述
这里并不是new Runnable, 此处是几个操作合并在一起了

  1. 创建了一个Runnable子类(不知道名字, 匿名)
  2. 同时创建了一个该子类的实例
  3. 重写了run方法
public class threadDemo4 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }

            }
        });
        thread.start();
    }
}

5. 使用lambda表达式

lambda表达式介绍
lambda本质上就是针对匿名内部类的平替
在这里插入图片描述
这里的lambda就是代替需要重写的run方法

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

面试题: java中创建线程都有哪些写法?

补充:
1.catch语句中的代码怎么写?
在这里插入图片描述
这是编译器自动生成的, 继续刨除新的异常
但是在实际开发中, 是要自定义的,可能会打印一些日志, 把出现的异常的详情都记录到日志文件中; 可能触发一些重试类操作; 可能触发一些"回滚"类操作…

2.在main方法中, 处理sleep有两种选择1)throws 2)try-catch
但是在线程run中只有一种选择try-catch, 为什么?

其实throws也是方法签名的一部分
方法签名包括:
1)方法名字
2) 方法的参数列表
3) 声明抛出的异常
不包括返回值和public…

那么在方法重写时, 就要求方法签名得是一样的
而父类的run方法中, 也就是Thread类中run是没有抛出异常的, 我们也改不了, 所以子类也不能抛出异常

  • 19
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值