Java线程(一):入门解析,从创建到优势解析

前言❤️❤️

hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
在Java中,线程的知识非常多,博主预计写九篇博客左右,来解析线程知识,这篇博客是线程部分的第一篇,这里建议铁汁们过本栏目中上篇解析进程的博客,能更好的理解线程,这篇博客会解析什么是线程,线程的优势,以及线程的创建方法
在这里插入图片描述🎆个人主页:洋不写bug的博客
🎆所属专栏:JavaEE学习
🎆铁汁们对于JavaEE的各种常用核心语法,都可以在上面的前端专栏学习,专栏正在持续更新中🏀,有问题可以写在评论区或者私信我哦~

1,线程简介

上篇博客解析了计算机中的进程,以及进程的属性,在任务管理器中,会发现一个叫线程的东西,数量比进程数要多得多(如下图)

在这里插入图片描述

线程可以称为是“轻量化进程”
进程有个特点,那就是在创建和销毁的时候消耗的时间和资源都是比较多的
在进程数量多,这个特点表现的更加明显
而服务器开发,就比较容易出现多进程的情况

说到这里,就要解析下服务器和客户端了

就好比我们去饭馆点餐,那我们就是客户端,老板就是服务器
服务器:被动接受通信的一方
客户端:主动发起通信的一方
服务器是处于一种被动的状态的,它不知道客户端什么时候来,也不知道一次来多少个客户端
因此,服务器就只能7*24小时的运转,应对到来的客户端,自然进程数量就多



为了提升效率,就用到了线程,进程是包含线程的
一个进程中至少有一个线程,也可以有多个线程
如果一个进程有多个线程,那这多个线程共用这个进程的资源(内存资源,硬盘资源…)
每个线程各自独立的运行一段逻辑,并且独立的在cpu上进行调度

因此,说“进程调度”其实是不严谨的,应该是线程“调度”

进程是操作资源分配的基本单位,线程是操作系统调度的基本单位



那为什么线程的使用能提高效率呢?

创建进程时需要申请资源(内存,硬盘),这是一个比较麻烦的过程(就好比向老板申请经费,要准备各种材料,经过老板同意,再从财务那里拿钱)
而对于线程来说,只有第一个线程(也就是随着进程创建的那个线程)在创建的时候需要申请资源,后面创建的线程,是不需要申请资源的,这就节省了开销,提高了效率

2,线程优势解析

线程的优势和注意事项会通过下面的图画来为大家解析

在一个屋子中,有一个桌子,桌子上有100只烤鸡,让滑稽一个人来吃,那速度就比较慢

在这里插入图片描述



要提高速度呢,就想了个办法,那就是把100只烤鸡分到两个屋子中,让两个滑稽来吃,一个滑稽吃50只,这样效率确实会提高,但是这种方法需要多搞一个屋子,一张桌子,会多耗费资源

这也就是多进程模式
在这里插入图片描述



那怎么减少资源的消耗呢,又有一个办法,就是让多个滑稽到一个屋子里吃这100只烤鸡,这样做省下了节省资源的开销
这种模式就是多线程模式

在这里插入图片描述


那是否是线程越多越好呢?
当线程比较少时,确实是这样,但是桌子的大小是固定的(例如同时只能有8个滑稽围着桌子吃鸡),随着滑稽数目的不断增加,靠桌子的滑稽没吃两口,就会被外面的滑稽挤出去,挤出去之后他还会想办法再挤进去
这样挤来挤去,效率反而会降低,因此并不是线程数目越多越好
前提是有充足的cpu资源,线程数目的增加,才会提高效率

在这里插入图片描述

其实,性能优化的终极答案就是升级cpu和内存,如果都到顶了还想升,那就加机器🐵



那如果线程数目不多,是不是意味着就不会出问题了呢

在这里插入图片描述其实不然,还是会存在线程安全问题:

  1. 两个滑稽同时看上了一只烤鸡,两个人就会抢
    当两个线程,尝试操作一个共享资源的时候,就可能会发生冲突,进而引起bug
  2. 某个滑稽吃到一半,心情不好,想掀桌子,其他滑稽劝他,如果劝住了,那就没事,如果没劝住,那大家就都别吃了😅
    如果某个线程抛出了异常,其他的线程没有catch到这个异常,那这样的异常就会导致整个程序运行的终止

在这里插入图片描述

3,继承Thread类创建线程

先写一个MyThread类,继承Thread类,如下所示

public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("hello thread");
    }
}

操作系统本身就提供了一组操作线程的函数,JVM就把这些操作系统的函数封装了下,封装成Java版本,拿到程序员手里的,就是Thread类,使用整个类,就相当于在使用操作系统的api,Thred类就是标准库中的一个类

run方法里面就是线程要执行的任务,在原Thread类中,这个run方法里面是空的,因为线程执行什么任务,本身就是我们写进去的,这里执行一个打印任务



再写一个Test类,首先创建线程对象
再创建启动线程,使用start方法,调用系统api,创建线程,这个新的线程就会以run作为入口方法
run方法里面表示的线程要执行的东西,创建线程后就会自动执行,无需手动调用run方法

public class Test {
    public static void main(String[] args) {
        //创建线程对象
        Thread thread = new MyThread();
        //启动线程
        thread.start();
    }
}

执行结果如下图
在这里插入图片描述



铁汁们可以试一下,这里直接调用run方法,还是能打印出hello thread,那跟用start调用有什么区别呢?

public class Test {
    public static void main(String[] args) {
        //创建线程对象
        Thread thread = new MyThread();
        thread.run();
    }
}

前面提到,start方法是创建线程,故这样写并没有创建一个新的线程,而是直接在主线程(main方法所在的线程)中执行了run的逻辑,相当于程序执行完还是只有一个主线程
使用start的话,程序执行完,有两个线程,一个主线程,一个新创建的线程

新创建一个线程能提升效率,因此一般不会直接调用run方法,而是用start方法去创建一个新线程

4,start和run方法的解析

为了更好的区分start和run方法,我们可以做一个测试,在main方法下面加上一个打印循环,下面使用Thread类的sleep方法(静态方法,直接通过类名来调用)
sleep方法就是让程序休眠,里面传入的1000单位是ms,这里就是1000ms执行一次这个while循环的打印
注:使用sleep方法需要抛出异常或者catch一下,代码如下所示

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //创建线程对象
        Thread thread = new MyThread();
        thread.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

接着在MyThread类的run方法中也写一个这样的逻辑,每隔1s打印一次

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



铁汁们在处理异常时会发现:在main方法中处理这个异常可以直接抛出,也可以catch一下,但是在MyThread类中,就只能catch异常(如下图所示),这是为什么呢
因为run方法是对Thread类的方法重写,如果抛出异常,那重写的方法的声明跟原方法的声明就不一样了,也就不算方法重写了

注:catch后IDEA自动生成的是throw new RuntimeException(e);
使用IDEA编码的铁汁要把这个替换e.printStackTrace(); 具体原因下篇博客在解析线程终止的时候会提到

在这里插入图片描述
在这里插入图片描述



运行下这段代码,两个线程就会进行打印,这两个线程各自在cpu上是独立执行的

在这里插入图片描述




接着在main方法中把start改为run,再运行下程序,这时候就会一直打印hello thread,而不打印hello main,如下所示

这是因为run方法是在主线程中执行的,这里并没有创建新的线程,在主线程中要等到run方法执行完,才能往下执行,但是这个run方法是个无限循环,因此就走不到下面打印hello main的逻辑

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //创建线程对象
        Thread thread = new MyThread();
        thread.run();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

在这里插入图片描述



当然,也可以采用工具来观察线程
打开jdk目录,打开里面的bin目录,在里面找到一个叫做jconsole的.exe程序,这个程序就是观察线程的

在这里插入图片描述



先在IDEA中把run改为start,运行线程,再双击打开这个程序,就能发现Test线程,点击Test线程,再点击不安全连接,就能观察线程了

在这里插入图片描述

进入线程栏目部分,就会显示一个线程数量图,在左下角会看到许多线程,里面就有主线程跟创建的线程,左下角表示的是这个进程中的所有线程,在这个进程中,除了这两个线程,像其他线程就都是跟JVM相关的

在这里插入图片描述

铁汁们可以把程序中的start方法再改为run方法,运行程序后再使用这个工具查看下,这时候Thread-0线程就没有了🐵

5,实现Runnable接口创建线程

前面创建线程的方法是继承Thread类,重写run方法,调用start来创建线程

也可以通过实现Runnable接口(标准库的接口,接口中不包含具体的方法实现,只提供抽象方法,让子类去重写)来创建线程

创建一个Runnalble包,在里面写下代码,首先创建个MyRunnalble类,实现下接口,在run方法里面安排线程的任务

package Runnable;

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



接着创建MyRunnable类的对象,但是runnable是没有start方法的,还是需要搭配Thread来使用(也就是继承Runnable接口,在run方法中安排下线程任务,再将Runnable任务传入线程中),代码如下所示:

package Runnable;

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new MyRunnable();

        Thread thread = new Thread(runnable);
        thread.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

运行结果如下(线程执行顺序并不固定,因为线程调度由 JVM 决定)

在这里插入图片描述



两种写法分析:

  1. 对于继承Thread类的写法,在描述任务的时候,是吧任务写到Thread子类的run方法中的,意味着任务内容和Thread类的耦合程度更高(写代码时要追求高内聚,低耦合)
  2. 而继承Runnable接口的方法,是将任务写到MyRunnable类中的,任务内容跟Thread类的耦合度是很小的,几乎没有关系,后续就可以把这个任务交给其他主体来处理(例如协程,是比线程更小的单位,一个线程中有多个协程)

6,匿名内部类创建线程

使用匿名内部类相当于做了升级,减少了代码量

这里就是创建了一个没有名字的Thread的子类,子类重写了run方法

public class Demo_03 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            @Override
            public void run(){
                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");
            Thread.sleep(1000);
        }
    }
}



实现Runnable接口,也可以使用匿名内部类,如下所示

public class Demo04 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                       e.printStackTrace();
                    }
                }
            }
        };
        Thread t = new Thread(runnable);
        t.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

7,lambda表达式创建线程(推荐)

前面使用匿名内部类来创建线程,就比最初的方法代码要简便,但是这四种方法我们在工作中一般都不用,还有一种最简便的方法,就是使用lambda表达式

代码如下,lambda表达式就相当于匿名函数,相当于就是用lambda作为入口方法了
后续会在Java学习之旅专栏更新有关lambda的博客,想要复习的铁汁们可以来看🐵

public class Demo05 {
    public static void main(String[] args) throws InterruptedException {
        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");
            Thread.sleep(1000);
        }
}
}

结语💕💕

线程部分的基础知识都不难,铁汁们只需要理解下线程优势这些基础概念,然后大概看下线程的创建规则,在记一下使用lambda表达式来创建线程的方法,下一篇博客会从进一步解析线程
以上就是今天的所有内容啦~完结撒花~🥳🎉🎉

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值