打怪升级之小白的大数据之旅(二十六)<Java面向对象进阶之多线程概述与基本使用>
打怪升级之小白的大数据之旅(二十六)
Java面向对象进阶之多线程概述与基本使用
上次回顾
上一章对IO的其他流如缓冲流,打印流等进行了介绍,IO流我们未来大数据可能会用的到,我们现在只需要大概记得它的实现方法即可,本章开始对多线程进行介绍,多线程可以加快我们代码的执行效率,具体怎么做呢?让我们开始进入正题:
多线程
- 多线程是java中的一个难点,所以介绍多线程前,我举一个现实的例子,然后整个多线程的案例几乎都是围绕这个现实,这样方便大家理解:
- 夏天要到了,此时又到了使用空调的季节,某个小镇中,来了一个年轻人Main,他发现该小镇从来没有人使用空调,于是他在当地创业,成立了一个M空调公司…
![在这里插入图片描述]()
概述
单线程
- 公司成立之初,空调的生产,销售安装都是Main一个人,当生产出一个空调后,就需要拿到市场上去售卖,卖出后还要亲自上门安装调试
- 程序按照顺序并且只有自己一个线程在执行,就是单线程
多线程
- 随着自己公司的空调口碑越来越好,公司有了一定规模,于是,Main老板雇佣了员工并且对整个业务流程进行了分工,雇佣工人专门用于生产空调,雇佣销售专门售卖空调,雇佣安装员专门负责空调的安装维护
- 将自己的任务拆分开,分配给不同的人(线程)来运行,就是多线程
![在这里插入图片描述]()
并发与并行
并行
- 当Main老板在一个客户家里安装空调时,收到了一个个需要维修空调的订单电话,他突然觉得自己好像忙不过来了。
- 在同一时刻有多个任务在执行,就是并行
并发 - 因为顾客至上,所以Main老板一边安装空调一边接电话,当然了,他打电话的时候,就会分心,安装空调的工作效率就不会那么高了
- 在同一个时间段内,两个或多个任务切换运行就是并发
![在这里插入图片描述]()
串行与并行
- 在M空调公司初期,一部空调必须生产出来才能进行销售,卖到客户手里才可以进行安装
- 再举一个栗子加深对并行的理解:M公司扩充人员后,对整个流程进行了分工,因此,当空调在生产时,就有销售人员在推销空调了,同样的,在安装人员安装空调的同时,空调也在生产。
- 并行和串行指的是任务的执行方式。串行是指多个任务时,各个任务按顺序执行,完成一个之后才能进行下一个。并行指的是多个任务可以同时执行,异步是多个任务并行的前提条件
![在这里插入图片描述]()
异步与同步
同步
- Main在成立的时候,他必须生产出一个空调才可以进行销售,卖出空调后才可以安装空调
异步
- 后来的任务分工后,不需要等待空调生产,销售人员可以直接售卖,并告诉客户规定时间内直接上门安装
- 异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情
线程与进程
- 相信大家对线程很熟悉了,下面我再举一个实际的栗子:我们开启了一个微信(开启了进程),选择了要聊天的好友(开启了一个好友聊天的线程),在聊天中可以边打字边听好友发送的语音(多线程)。当我们退出了微信,下次启动就需要重新扫码登录(进程的结束)
- 进程就是一个软件的运行过程(微信的开启-使用-关闭)
- 软件就是一个或多个程序+相关素材和资源文件的组合(微信由聊天程序、朋友圈、小程序等组成)
- 程序就是单独的功能(微信的聊天功能)
- 为大家科普一个面试题:
- 线程是进程中的一个执行单元,负责完成执行当前程序的任务,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这时这个应用程序也可以称之为多线程程序。多线程使得程序可以并发执行,充分利用CPU资源
每个应用程序的运行都是一个进程
一个应用程序的多次运行,就是多个进程
一个进程中包含多个线程

多线程的优点与应用场景
主要优点:
- 充分利用CUP空闲时间片,用尽可能短的时间完成用户的请求。也就是使程序的响应速度更快
应用场景:
- 多任务处理。多个用户请求服务器,服务端程序可以开启多个线程分别处理每个用户的请求,互不影响
- 单个大任务处理。下载一个大文件,可以开启多个线程一起下载,减少整体下载时间
线程调度
线程调度指CPU资源如何分配给不同的线程。常见的两种线程调度方式:
- 分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。 - 抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性) - 在java中,采用的是抢占式调度
![在这里插入图片描述]()
- 大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我一边撸着博客,一边听着歌,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,感觉这些软件好像在同一时刻运行着
- 实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高
线程的创建与启动
- java虚拟机是支持多线程的,当运行Java程序时,至少已经有一个线程了,那就是main线程
- Main老板的空调公司刚开始就是一个单线程,同时也是一个单进程
- 创建和启动线程有两种方式,分别是继承Thread类与实现Runnable接口
- 以M空调公司的销售部门举例,M空调公司有两个销售团队T团队和R团队,当Main老板成立销售部门后,首先需要对其进行培训,将自己的销售方法和经验传授给手下的团队:
继承Thread类
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的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接口
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正
的线程对象。 - 调用线程对象的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需要在多条线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换
![在这里插入图片描述]()
新建
- 当一个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中是一个难点,网上对于多线程,多任务的讲解有很多,对于并行,并发,异步,同步的介绍总是比较官方,我希望可以通过实际的例子让大家清楚这些概念都是什么,可以更好的帮助大家理解多线程的原理。
- 今天对线程的知识点就介绍到这里,下一章,我会对线程的剩下知识点,线程安全、等待唤醒机制以及线程的死锁与释放锁进行讲解






浙公网安备 33010602011771号