打怪升级之小白的大数据之旅(二十六)<Java面向对象进阶之多线程概述与基本使用>

打怪升级之小白的大数据之旅(二十六)

Java面向对象进阶之多线程概述与基本使用

上次回顾

上一章对IO的其他流如缓冲流,打印流等进行了介绍,IO流我们未来大数据可能会用的到,我们现在只需要大概记得它的实现方法即可,本章开始对多线程进行介绍,多线程可以加快我们代码的执行效率,具体怎么做呢?让我们开始进入正题:

多线程

  • 多线程是java中的一个难点,所以介绍多线程前,我举一个现实的例子,然后整个多线程的案例几乎都是围绕这个现实,这样方便大家理解:
  • 夏天要到了,此时又到了使用空调的季节,某个小镇中,来了一个年轻人Main,他发现该小镇从来没有人使用空调,于是他在当地创业,成立了一个M空调公司…打怪升级之小白的大数据之旅(二十六)<Java面向对象进阶之多线程概述与基本使用>_第1张图片

概述

单线程

  • 公司成立之初,空调的生产,销售安装都是Main一个人,当生产出一个空调后,就需要拿到市场上去售卖,卖出后还要亲自上门安装调试
  • 程序按照顺序并且只有自己一个线程在执行,就是单线程

多线程

  • 随着自己公司的空调口碑越来越好,公司有了一定规模,于是,Main老板雇佣了员工并且对整个业务流程进行了分工,雇佣工人专门用于生产空调,雇佣销售专门售卖空调,雇佣安装员专门负责空调的安装维护
  • 将自己的任务拆分开,分配给不同的人(线程)来运行,就是多线程打怪升级之小白的大数据之旅(二十六)<Java面向对象进阶之多线程概述与基本使用>_第2张图片

并发与并行

并行

  • 当Main老板在一个客户家里安装空调时,收到了一个个需要维修空调的订单电话,他突然觉得自己好像忙不过来了。
  • 在同一时刻有多个任务在执行,就是并行
    并发
  • 因为顾客至上,所以Main老板一边安装空调一边接电话,当然了,他打电话的时候,就会分心,安装空调的工作效率就不会那么高了
  • 在同一个时间段内,两个或多个任务切换运行就是并发打怪升级之小白的大数据之旅(二十六)<Java面向对象进阶之多线程概述与基本使用>_第3张图片

串行与并行

  • 在M空调公司初期,一部空调必须生产出来才能进行销售,卖到客户手里才可以进行安装
  • 再举一个栗子加深对并行的理解:M公司扩充人员后,对整个流程进行了分工,因此,当空调在生产时,就有销售人员在推销空调了,同样的,在安装人员安装空调的同时,空调也在生产。
  • 并行和串行指的是任务的执行方式。串行是指多个任务时,各个任务按顺序执行,完成一个之后才能进行下一个。并行指的是多个任务可以同时执行,异步是多个任务并行的前提条件打怪升级之小白的大数据之旅(二十六)<Java面向对象进阶之多线程概述与基本使用>_第4张图片

异步与同步

同步

  • Main在成立的时候,他必须生产出一个空调才可以进行销售,卖出空调后才可以安装空调

异步

  • 后来的任务分工后,不需要等待空调生产,销售人员可以直接售卖,并告诉客户规定时间内直接上门安装
  • 异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情

线程与进程

  • 相信大家对线程很熟悉了,下面我再举一个实际的栗子:我们开启了一个微信(开启了进程),选择了要聊天的好友(开启了一个好友聊天的线程),在聊天中可以边打字边听好友发送的语音(多线程)。当我们退出了微信,下次启动就需要重新扫码登录(进程的结束)
  • 进程就是一个软件的运行过程(微信的开启-使用-关闭)
  • 软件就是一个或多个程序+相关素材和资源文件的组合(微信由聊天程序、朋友圈、小程序等组成)
  • 程序就是单独的功能(微信的聊天功能)
  • 为大家科普一个面试题:
    • 线程是进程中的一个执行单元,负责完成执行当前程序的任务,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这时这个应用程序也可以称之为多线程程序。多线程使得程序可以并发执行,充分利用CPU资源

每个应用程序的运行都是一个进程打怪升级之小白的大数据之旅(二十六)<Java面向对象进阶之多线程概述与基本使用>_第5张图片
一个应用程序的多次运行,就是多个进程打怪升级之小白的大数据之旅(二十六)<Java面向对象进阶之多线程概述与基本使用>_第6张图片

一个进程中包含多个线程
打怪升级之小白的大数据之旅(二十六)<Java面向对象进阶之多线程概述与基本使用>_第7张图片

多线程的优点与应用场景

主要优点:

  • 充分利用CUP空闲时间片,用尽可能短的时间完成用户的请求。也就是使程序的响应速度更快

应用场景:

  • 多任务处理。多个用户请求服务器,服务端程序可以开启多个线程分别处理每个用户的请求,互不影响
  • 单个大任务处理。下载一个大文件,可以开启多个线程一起下载,减少整体下载时间

线程调度

线程调度指CPU资源如何分配给不同的线程。常见的两种线程调度方式:

  • 分时调度
    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
  • 抢占式调度
    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)
  • 在java中,采用的是抢占式调度打怪升级之小白的大数据之旅(二十六)<Java面向对象进阶之多线程概述与基本使用>_第8张图片
  • 大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我一边撸着博客,一边听着歌,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,感觉这些软件好像在同一时刻运行着
  • 实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
    其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高

线程的创建与启动

  • java虚拟机是支持多线程的,当运行Java程序时,至少已经有一个线程了,那就是main线程
  • Main老板的空调公司刚开始就是一个单线程,同时也是一个单进程
  • 创建和启动线程有两种方式,分别是继承Thread类与实现Runnable接口
  • 以M空调公司的销售部门举例,M空调公司有两个销售团队T团队和R团队,当Main老板成立销售部门后,首先需要对其进行培训,将自己的销售方法和经验传授给手下的团队:

继承Thread类

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程
package com.test01socket;
// 多线程的创建与启动实例
public class Demo {
    public static void main(String[] args) {
        // 创建线程(创建T销售团队)
        Thread GroupT = new MainCompanyGroupT();
        // 启动线程,T销售团队开始售卖空调
        GroupT.start();

    }
}

// M公司的T销售团队
class MainCompanyGroupT extends Thread{
    @Override
    public void run() {
        System.out.println("T团队售卖空调");
    }
}

实现Runnable接口

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正
    的线程对象。
  3. 调用线程对象的start()方法来启动线程
package com.test01socket;
// 多线程的创建与启动实例
public class Demo {
    public static void main(String[] args) {
        // 实现Runnable类,成立R团队
        Runnable R = new MainCompanyGroupR();
        // 创建线程,创建R销售团队
        Thread GroupR = new Thread(R);
        // R销售团队开始售卖空调
        GroupR.start();

    }
}

// M公司的R销售团队
class  MainCompanyGroupR implements Runnable{
    @Override
    public void run() {
        System.out.println("R团队售卖空调");

    }
}

两种创建线程方式比较

  • 根据上面两种创建方式,我们发现Runnable方法好像更麻烦一点,Thread类更简单,其实Thread类本身也是实现了Runnable接口的,run方法都来自Runnable接口,run方法也是真正要执行的线程任务,Thread源码如下:
    public class Thread implements Runnable {}
    
  • 那么为什么要有实现类的方式创建线程呢?答案就是java的继承关系,因为java只有单继承,我们学习接口的时候说过,接口的作用之一就是实现多继承
  • 实现Runnable接口的方式,避免了单继承的局限性,并且可以使多个线程对象共享一个Runnable实现类(线程任务类)对象,从而方便在多线程任务执行时共享数据

匿名内部类对象创建线程

  • R团队和T团队在销售高峰期,需要一批兼职来完成销售工作,因此,他们招一些兼职人员,此时就可以使用匿名内部类来完成线程的创建:
    package com.test01Thread;
    // 匿名内部类创建线程
    public class Demo2 {
        public static void main(String[] args) {
            // T团队招收兼职人员
            new Thread("T团队兼职人员"){
                @Override
                public void run() {
                    System.out.println("T团队的销售人员售卖空调");
                }
            }.start();
            // R团队招收兼职人员
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("R团队的销售人员售卖空调");
                }
            },"R团队兼职人员").start();
        }
    
    }
    

Thread类

下面,我为大家详细介绍一下Thread类

构造方法

  • public Thread() :分配一个新的线程对象。
  • public Thread(String name) :分配一个指定名字的新的线程对象。
  • public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字

线程使用基础方法

  • public void run() :此线程要执行的任务在此处定义代码。
  • public String getName() :获取当前线程名称。
  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
  • public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。
  • public final int getPriority() :返回线程优先级
  • public final void setPriority(int newPriority) :改变线程的优先级
    • 每个线程都有一定的优先级,优先级高的线程将获得较多的执行机会
    • 每个线程默认的优先级都与创建它的父线程具有相同的优先级
    • Thread类提供了setPriority(int newPriority)getPriority()方法类设置和获取线程的优先级,其中setPriority方法需要一个整数,并且范围在[1,10]之间,通常推荐设置Thread类的三个优先级常量:
优先级常量 优先级说明
MAX_PRIORITY(10) 最高优先级
MIN _PRIORITY (1) 最低优先级
NORM_PRIORITY (5) 普通优先级,默认情况下main线程具有普通优先级

示例代码:

public static void main(String[] args) {
    Thread t = new Thread(){
        public void run(){
            System.out.println(getName() + "的优先级:" + getPriority());
        }
    };
    t.setPriority(Thread.MAX_PRIORITY);
    t.start();

    System.out.println(Thread.currentThread().getName() +"的优先级:" + Thread.currentThread().getPriority());
}

线程控制常见方法

  • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
  • public static void sleep(long millis) :线程睡眠,使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  • public static void yield():线程礼让,yield只是让当前线程暂时失去执行权,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。
  • void join() :加入线程,当前线程中加入一个新线程,等待加入的线程终止后再继续执行当前线程。
    void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。
    void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
  • public final void stop():强迫线程停止执行。 该方法具有不安全性,已被弃用,最好不要使用。
    • 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
    • 调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
  • public void interrupt():中断线程,实际上是给线程打上一个中断的标记,并不会真正使线程停止执行。
  • public static boolean interrupted():检查线程的中断状态,调用此方法会清除中断状态(标记)。
  • public boolean isInterrupted():检查线程中断状态,不会清除中断状态(标记)
  • public void setDaemon(boolean on):将线程设置为守护线程。必须在线程启动之前设置,否则会报IllegalThreadStateException异常。
    • 守护线程,主要为其他线程服务,当程序中没有非守护线程执行时,守护线程也将终止执行。JVM垃圾回收器也是守护线程。
  • public boolean isDaemon():检查当前线程是否为守护线程
  • 老样子,写一个综合的示例代码(使用空调举例的话,效果不够明显):龟兔赛跑
    /*
    * 案例:编写龟兔赛跑多线程程序,设赛跑长度为30米
    * 兔子的速度是10米每秒,兔子每跑完10米休眠的时间10秒
    * 乌龟的速度是1米每秒,乌龟每跑完10米的休眠时间是1秒
    * 要求:要等兔子和乌龟的线程结束,主线程(裁判)才能公布最后的结果
    * */
    public class Demo2 {
        public static void main(String[] args) {
            Racer rabbit = new Racer("兔子", 30, 100, 10000);
            Racer turtoise = new Racer("乌龟", 30, 1000, 1000);
    
            rabbit.start();
            turtoise.start();
    
            //因为要兔子和乌龟都跑完,才能公布结果
            try {
                rabbit.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                turtoise.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("比赛结束");
            if(rabbit.getTotalTime()==turtoise.getTotalTime()){
                System.out.println("平局");
            }else if(rabbit.getTotalTime()<turtoise.getTotalTime()){
                System.out.println("兔子赢");
            }else{
                System.out.println("乌龟赢");
            }
        }
    }
    
    class Racer extends Thread {
        private String name;//运动员名字
        private long runTime;//每米需要时间,单位毫秒
        private long restTime;//每10米的休息时间,单位毫秒
        private long distance;//全程距离,单位米
        private long totalTime;//跑完全程的总时间
    
        public Racer(String name, long distance, long runTime, long restTime) {
            super();
            this.name = name;
            this.distance = distance;
            this.runTime = runTime;
            this.restTime = restTime;
        }
    
        @Override
        public void run() {
            long sum = 0;
            long start = System.currentTimeMillis();
            while (sum < distance) {
                System.out.println(name + "正在跑...");
                try {
                    Thread.sleep(runTime);// 每米距离,该运动员需要的时间
                } catch (InterruptedException e) {
                    return ;
                }
                sum++;
                try {
                    if (sum % 10 == 0 && sum < distance) {
                        // 每10米休息一下
                        System.out.println(name+"已经跑了"+sum+"米正在休息....");
                        Thread.sleep(restTime);
                    }
                } catch (InterruptedException e) {
                    return ;
                }
            }
            long end = System.currentTimeMillis();
            totalTime = end - start;
            System.out.println(name+"跑了"+sum+"米,已到达终点,共用时"+totalTime/1000.0+"秒");
        }
    
        public long getTotalTime() {
            return totalTime;
        }
    }
    

线程生命周期

传统线程模型的五种线程状态

  • 了解传统的线程模型是为了方便我们更好的理解线程
  • 传统线程模型中把线程的生命周期描述为五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。CPU需要在多条线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换打怪升级之小白的大数据之旅(二十六)<Java面向对象进阶之多线程概述与基本使用>_第9张图片

新建

  • 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状。此时它和其他Java对象一样,仅仅由JVM为其分配了内存,并初始化了实例变量的值。此时的线程对象并没有任何线程的动态特征,程序也不会执行它的线程体run()。

就绪

  • 但是当线程对象调用了start()方法之后,线程就从新建状态转为就绪状态。这时线程并未执行,只是具备了运行的条件,还需要获取CPU资源后才能执行。

运行

  • 如果处于就绪状态的线程获得了CPU资源,开始执行run()方法的线程体代码,则该线程处于运行状态。如果计算机只有一个CPU,在任何时刻只有一个线程处于运行状态,如果计算机有多个处理器,将会有多个线程并行(Parallel)执行。
  • 当然,美好的时光总是短暂的,而且CPU讲究雨露均沾。对于抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务,当该时间用完,系统会剥夺该线程所占用的资源,让其回到就绪状态等待下一次被调度。此时其他线程将获得执行机会,而在选择下一个线程时,系统会适当考虑线程的优先级。

阻塞

  • 当在运行过程中的线程遇到某些特殊情况时,线程会临时放弃CPU资源,不再执行,即进入阻塞状态。比如:线程调用了sleep()方法,会主动放弃所占用的CPU资源。

死亡

  • 线程完成任务结束或意外终止后,线程就处于死亡状态

JDK定义的六种线程状态

在java.lang.Thread类内部定义了一个枚举类用来描述线程的六种状态

lic enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

打怪升级之小白的大数据之旅(二十六)<Java面向对象进阶之多线程概述与基本使用>_第10张图片

总结

  • 多线程在java中是一个难点,网上对于多线程,多任务的讲解有很多,对于并行,并发,异步,同步的介绍总是比较官方,我希望可以通过实际的例子让大家清楚这些概念都是什么,可以更好的帮助大家理解多线程的原理。
  • 今天对线程的知识点就介绍到这里,下一章,我会对线程的剩下知识点,线程安全、等待唤醒机制以及线程的死锁与释放锁进行讲解

你可能感兴趣的:(多线程,java,编程语言,大数据)