从“单线程社畜“到“多线程时间管理大师“的进化之路

前言

多线程有什么作用?
想象一下,你是一个餐厅里唯一的服务员,既要接单、上菜,又要收拾桌子、结账……忙得脚不沾地,顾客却还在抱怨“太慢了!”(程序卡成PPT)。这时候,多线程就像老板突然给你雇了几个帮手:

线程A专门接单,线程B负责上菜,线程C埋头算账……大家各司其职,效率直接起飞!

顾客满意了(用户体验流畅),老板赚钱了(程序性能提升),你甚至能偷闲刷个手机(CPU不再摸鱼)。

什么是线程?

首先我们要了解一下什么是进程,进程是一个要执行的任务,每一个运行的软件都有一个进程,而线程也是一个要执行的任务,线程拥有以下特点:

  • 线程在创建和销毁时,开销更小,相当于一个轻量级的进程。
  • 每个进程中包含一个或多个线程,进程内部管辖多个线程,共享进程的内存资源和硬盘资源。
  • 只有第一个线程创建的时候才会申请资源,其他线程穿件不涉及申请,只有所有线程都销毁的时候,才这正释放资源。干的事情小所以快。
  • 进程与进程直接资源相互独立,彼此直接互不干扰。而线程与线程直接是容易相互影响的。
  • 多个线程同时去执行一个进程的任务,可以大幅度提高效率,并且相对于开多个进程开销更小。
  • 进程是操作系统资源分配的基本单位
  • 线程是操作系统调度执行的基本单位

线程的出现是为了解决进程太的问题(开销和销毁比较大)。
在这里插入图片描述
这里要注意的是虽然多个线程同时去执行一个进程的任务,可以大幅度提高运行效率,但也并不是越多越好。当线程的数目达到一定程度后,即使线程再多也没法达到效果,而且线程数目如果太多,线程的调度开销会增大,反而会拖慢程序的性能。

同时我们需要注意线程安全问题,当多个线程去抢同一份任务时,可能会出现抢不到的情况,这时候抢不到的线程会抛出异常,导致整个进程出现错误。如果可以及时处理,可以避免这种情况发生。

上述总结是一道经典的面试题目:线程与进程的区别以及基本特点。

线程创建

在Java中线程的创建是通过Thread类实现的,这里我们提供几种Thread类的创建方法,这些方法并没有优劣之分。

1.重写Thread子类的run方法
class MyThread1 extends Thread {
    @Override
    public void run() {
        System.out.println("666");
    }
}
public class demo {
    public static void main(String[] args) {
        Thread t = new MyThread1();
        t.start();
    }
}

2.实现Runnable接口的run方法
class Run implements Runnable {
    @Override
    public void run() {
        System.out.println("666");
    }
}
public class demo6 {
    public static void main(String[] args) {
        Run r = new Run();
        Thread t = new Thread(r);
        t.start();
    }
}
3.lambada实现Runnable的run方法(最简洁)

public class demo6 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            System.out.println("666");
        });
        t.start();
    }
}

这里要注意的是,我们虽然重写的是run方法,但是run方法只是这个线程运行的任务,并没有涉及创建线程,所以我们在运行线程时使用的是start方法,这个才是真正意义上的创建线程。

Thread类只能运行一次start,不可以重复利用,java在设计的时候希望每一个Thread都可以和操作系统的线程一一对应的,如果可以重复利用,那就是一对多的情况了。

当使用两次start时:在这里插入图片描述

当我们使用上述的第三种方法时,如果我们需要使用外部的变量,可能会触发”参数捕获“这一事件。

当使用lambada实现的时候,你如果要使用外部的参数,会触发”参数捕获“,这时你使用的参数只能是final类型,或者实际不会被修改的类型(例如使用一个引用变量),或者使用外部类的成员变量,也是可以正常使用的,不会触发这个事件。

举个例子:

你如果这么写就是可行的在这里插入图片描述

但是你如果这么干的话,就会报错,因为它实际上被修改了
在这里插入图片描述
改变引用变量不会有影响在这里插入图片描述

这样做是为了避免当主进程结束时,变量自动销毁,所以他在引进去变量时,实际上是增加了一个参数,这个参数就是这个变量的副本,为了避免这个副本和原先的不一样,Java不允许他可以被改变。而final是不会被改变的,引用和外部类的成员变量都是由java的gc(垃圾回收)机制管理的,不用担心生命周期失效的问题。

Thread类的常见属性

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

这里我们着重讲解后台线程这一属性。

前台进程:可以影响到进程是否继续存在,都结束时,连带着后台进程也一起结束。
后台进程:伴随着前台进程存在的进程,比如说jvm的垃圾回收机制。

正常来说,我们创建的进程都是前台线程,我们可以通过setDaemon方法将其设置为后台线程。

我们来看这样一段代码:


public class demo6 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            int cnt = 1;
            while (true) {
                System.out.println(cnt);
                cnt++;
            }
        });

        t.setDaemon(true);
        t.start();
        System.out.println("main线程终止");
    }
}

如果是正常来说的话,这个线程时会一直运行的,但是我们通过setDaemon方法将其设置为了后台线程,此时前台线程只剩下了main,而随着main的终止,所有的后台线程包括这个t线程也会随之终止。

线程休眠

在Java中我们可以通过sleep()方法让线程主动进入休眠状态。
例如:

    public static void main(String[] args) {
        Thread t = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("666");
        });
        t.start();
    }

这个代码就是让此进程在运行1s后再输出666,sleep内部的参数单位是毫秒。

我们在实际中停止的时间是比设置的要多一点点的,因为sleep方法是让cpu停止对他的调度,达到时间后才会继续参与,但是cpu的调度是不可控的,虽然重新参与了,但是也得等cpu什么时候再给调度。

换句话说,时间到不是立即执行,而是允许被调度了。

由此我们可以诞生一个特殊写法:sleep(0),让当前的线程立即放弃cpu资源,等待操作系统重新调度。

获取当前线程

Thread.currentThread()方法可以调用当前的类的线程。

注:创建线程类时,第二个参数是线程名字在这里插入图片描述
运行结果
在这里插入图片描述

该方法可以帮你自动找到当前所处的进程,你在创建Thread类时,你在其内部是无法使用它还没创建的引用的,此方法可以解决这个问题。

线程等待

先看一段代码:

public class demo6 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            System.out.println("t线程结束");
        });
        t.start();
        System.out.println("main线程结束");
    }
}

在这个程序中,因为线程调度的不确定性,他可能会产生两种结果,一个是t先结束,另一个是main先结束。

而在程序中,不确定性往往意味着危险,容易出现Bug。

为此Java引入了join方法,join能够要求线程之间的结束的先后顺序,可以减少随机调度带来的不确定性。


public class demo6 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            System.out.println("t线程结束");
        });
        t.start();
        try {
            t.join();   //等待t线程结束后再运行
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("main线程结束");
    }
}

在main运行到join的时候main线程会阻塞在这里,等待t线程结束后才继续运行。

这里不要理解错误,是main进程在等待t进程结束。

但是这种方法不是特别好,如果t进程迟迟没有结束,那么main进程就得一直等下去吗?这显然不合理,所以我们得设置join的参数,也就是它等待的最大时间。如果超过这个时间,就不会继续等它,这种方法才更科学。


public class demo6 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while (true) {
                System.out.println("666");
            }
        });

        t.start();
        try {
            t.join(1000);//最多等待1秒后继续运行
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        
        System.out.println("main线程结束");
    }
}

线程终止

在Java多线程编程中,interrupt()方法提供了一种协作式的中断机制,就像同事轻拍你的肩膀说"该下班了",而不是直接拔掉你的电源线。这样可以保证执行出一些阶段性的结果,才真正退出,而不是得到一个不上不下的结果。

public class demo6 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("这里进行终止后的处理");
                    break;
                }
            }
        });

        t.start();
        t.interrupt();
        
        System.out.println("main线程结束");
    }
}

像sleep(), wait(), join()会立即抛出InterruptedException,我们只需要对异常进行处理,来保障后续的运行。


感谢各位的观看Thanks♪(・ω・)ノ,如果觉得满意的话留个关注再走吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值