「JAVA」通过抢气球案例,来梳理线程基础知识
程序在没有流程控制的前提下,代码都是从上而下逐行依次执行的。基于这样的机制,如果我们使用程序来实现边打游戏,边听音乐的需求时,就会很困难;因为按照执行顺序,只能从上往下依次执行;同一时刻,只能执行听音乐和打游戏的其中之一。
为了解决这样的问题,在程序设计中引入了多线程并发。本文中的知识对windows
、mac
、linux
系统都适用,但展示界面和功能名称上不太一样;相关的截图这里以windows
为例。
并行和并发
并行和并发是两个很容易混淆的概念,他们在字面上理解起来可能没有很大的差异,但要放在计算机运行环境中来解释,两者是有很大区别的:
- 并行:多个事件在同一个时间点同时发生,是真正的同时发生;
- 并发:多个事件在同一时间段内在宏观上同时发生,而在微观上是CPU在多个事件上来回切换,但切换的时间很快,并不能被人眼捕获,因此在一段人类可以观察到的时间内,多个事件是同时发生的;
操作系统的运行环境中,并发指的就是一段时间内宏观上多个程序在同时运行;在单CPU
的环境中,微观上每一个时刻仅有一个程序被CPU
执行(也就是仅有一个程序获得了CPU时间片
),CPU
是在多个程序之间来回交替执行,也就是给每个程序的运行时间进行调度,从而实现多个程序的并发运行。
随着计算机硬件的不断发展,现如今的计算机一般都是有多个CPU
的,在这样的多个CPU
的环境中,原本由单个处理器运行的这些程序就可以被分配给多个CPU
来运行,从而实现真程序的并行运行,无论从宏观上,还是微观上,程序都是同时运行的。这样,程序的运行效率就会大大提高。
PS:CPU时间片就是CPU分配给每个程序的运行时间。
在买电脑的时候,电脑厂商宣传的“几核处理器”,其中“核”表示的是CPU
有几个物理核心,能够并行处理几个程序的调用。想要知道自己电脑是几核的,可以打开“任务管理器”来查看。
也可以通过计算机属性、设备管理器来查看。 所以,单核处理器是不能并行运行多个任务的,只能是多个任务在单核处理器中并发运行,我们把每个任务用一个线程来表示,多个线程在单个处理器中的并发运行我们称之为线程调度。
从宏观上讲,多个线程是并行运行的;从微观上讲,多个线程是串行运行的,也就是一个线程一个线程的运行;如果对这里的宏观和微观不太好理解的话,可以把宏观看作是站在人的角度看待程序运行,把微观看作是站在CPU
的角度看待程序运行,这样就好理解多了。
线程和进程
进程:进程是指一个在内存中运行的应用程序,每个进程在内存中都有一块独立的内存空间。每个软件都可以启动多个进程。
线程:线程指的是进程中的一个控制单元,也就是进程中的每个单元任务,一个进程中可以有多个线程同时并发运行。
多进程指的是操作系统中同时运行的多个程序,多线程指的是同一个进程中同时运行的多个任务。操作系统中运行的每个任务就是一个进程,进程中的每个任务就是一个线程;操作系统就是一个多任务系统,它可以有多个进程,每个进程又可以有多个线程。
线程和进程的区别:
每个进程都有独立的内存空间,也就是进程中的数据存储空间(堆、栈空间)是独立的,且每个进程都至少有一个线程;
对于线程来说:堆内存空间是共享的,栈内存空间是独立的;线程消耗的资源比进程要小得多,且线程之间是可以相互通信的;
线程是进程的基本组成单元,故也把线程称作进程元,或者轻型进程;
线程的执行是通过
CPU
调度器来决定的,程序员无法控制;
线程调度: 计算机单个CPU
在任意时刻只能执行一条计算机指令,每个进程只有获得CPU
使用权才能执行相关指令;
多线程并发,其实就是运行中各个进程轮流获取CPU
的使用权来分别执行各自的任务;在多进程的环境中,会有多个线程处于等待获取CPU
使用权的状态中,为这些等待中的线程分配CPU
使用权的操作就成为线程调度。
线程调度分为抢占式调度和分时调度。
抢占式调度:多个线程在瞬间抢占
CPU
资源,谁抢到谁就运行,有更多的随机性;分时调度:为等待中的多个线程平均的分配
CPU
时间片;
Java
的多线程中线程调度就是使用抢占式调度的。
多线程
多线程和单线程,就好比多行道和单行道,多行道可以有多辆车同时行驶通过,而单行道只能是多辆车按顺序依次行驶通过;多线程同时有多个线程并发运行,单线程只有单个线程对多个任务按顺序依次执行。
如果以下载文件为例:单线程就是只有一个文件下载的通道,多线程则是同时有多个下载通道在下载文件。当服务器提供下载服务时,下载程序是共享服务器带宽的,在优先级相同的情况下,服务器会对下载中的所有线程平均分配带宽:
宽带带宽是以位(bit
)来计算的,而下载速度是以字节(byte
)计算的,1 byte = 8 bit
,故1024KB/s
代表的是上网宽带为1M
(1024
千位),而下载速度需要用1024KB/s
除去8
,得出128KB/s
。
多线程是为了同步完成多项任务,是为了提高系统整体的效率,而不能提高程序代码自身的运行效率。
多线程的优势: 多线程作为一种多任务、高并发的运行机制,有其独到的优势所在:
进程之间不能共享内存空间,但是线程之间是可以的(通过堆内存);
创建进程时,操作系统需要为其重新分配系统资源;而创建线程耗费的资源会小很多,在实现多任务并发时,相比较于多进程,多线程的效率会高很多;
Java
语言内置了对多线程的支持,而不仅仅是简单的调用底层操作系统的调度;Java
对多线程的支持也很友好,能大大简化开发成本;
创建进程
在Java
中创建进程可通过两种方式来实现:
1. 通过java.lang.Runtime
来实现,示例代码如下:
public static void main(String []args) throws IOException { // 方式一:通过通过java.lang.Runtime来实现打开 cmd Runtime runtime = Runtime.getRuntime(); runtime.exec("cmd");
}
2. 通过java.lang.ProcessBuilder
来实现,示例代码如下:
public static void main(String []args) throws IOException { // 方式二:通过通过java.lang.ProcessBuilder来实现打开 cmdProcessBuilder pb = new ProcessBuilder("cmd"); pb.start();
}
创建线程
一、通过继承Thread
类创建线程; 需要注意的是:只有Thread
的子类才是线程类;
新创建一个类继承于
java.lang.Thread
;在新建的
Thread
子类中重写Thread
类中的run
方法,在run
方法中编写线程逻辑;创建线程对象,执行线程逻辑;
public class ExtendsThreadDemo { public static void main(String []args) { for (int i = 0; i < 50; i++) { System.out.println("主线程" + i); if (i == 13) { NewThread newThread = new NewThread(); newThread.start(); } } }
} // 新线程类
class NewThread extends Thread { @Override public void run() { for (int i = 0; i < 50; i++) { System.out.println("新线程" + i); } }
}
二、通过实现Runnable
接口创建线程; 需要注意,这里的Runnable
实现类并不是线程类,所以启动方式和Thread
子类会有所不同;
- 新创建一个类实现
java.lang.Runnable
; - 在新建的实现类中重写
Runnable
类中的run
方法,在run
方法中编写线程逻辑; - 创建
Thread
对象,传入Runnable
实现类对象,执行线程逻辑;
示例代码如下:
public class ImplementsRunnableDemo {public static void main(String []args) {for (int i = 0; i < 50; i++) {System.out.println("主线程" + i);if (i == 13) {Runnable runnable = new NewRunnableImpl();Thread thread = new Thread(runnable);thread.start();}}}
}// 新线程类
class NewRunnableImpl implements Runnable {@Overridepublic void run() {for (int i = 0; i < 50; i++) {System.out.println("新线程" + i);}}
}
三、使用匿名内部类创建线程,使用接口的匿名内部类来创建线程,示例代码如下:
// 使用接口的匿名内部类
public class AnonymousInnerClassDemo {public static void main(String []args) {for (int i = 0; i < 50; i++) {System.out.println("主线程" + i);if (i == 13) {new Thread(new Runnable() {@Overridepublic void run() {for (int j = 0; j < 50; j++) {System.out.println("新线程" + j);}}}).start();}}}
}
当然了,也可以使用Thread
类的匿名内部类创建线程,不过这样的方式很少使用;示例代码如下:
// 使用Thread类的匿名内部类
public class AnonymousInnerClassDemo {public static void main(String []args) {for (int i = 0; i < 50; i++) {System.out.println("主线程" + i);if (i == 13) {new Thread() {@Overridepublic void run() {for (int j = 0; j < 50; j++) {System.out.println("新线程" + j);}}}.start();}}}
}
多线程案例
案例需求:六一儿童节,设置了抢气球比赛节目,共有50
个气球,三个小朋友小红、小强、小明来抢;请使用多线程技术来实现上述比赛过程。
一、使用继承Thread
类的方式来实现上述案例; 示例代码如下:
public class ExtendsDemo {public static void main(String []args) {new Student("小红").start();new Student("小强").start();new Student("小明").start();}
}class Student extends Thread {private int num = 50;private String name;public Student(String name) {super(name);this.name = name;}@Overridepublic void run() {for (int i = 0; i < 50; i++) {if (num > 0) {System.out.println(this.name + "抢到了" + num + "号气球");num--;}}}
}
通过查看输出结果,发现一个问题:每个小朋友都抢到了50
个气球,这和原本只有50
个气球相矛盾了;不过别急,我们可以使用第二种方式:使用实现接口的方式来实现上述案例 来解决。
二、使用实现接口的方式来实现上述案例; 示例代码如下:
public class ImplementsDemo {public static void main(String []args) {Balloon balloon = new Balloon();new Thread(balloon, "小红").start();new Thread(balloon, "小强").start();new Thread(balloon, "小明").start();}
}// 气球
class Balloon implements Runnable {private int num = 50;@Overridepublic void run() {for (int i = 0; i < 50; i++) {if (num > 0) {System.out.println(Thread.currentThread().getName() + "抢到了" + num + "号气球");num--;}}}}
在该案例中我们是用了Thread.currentThread()
方法,该方法的作用是返回当前正在执行的线程对象的引用,所以当前正在执行的线程对象的名称就可以这样来获取:String name = Thread.currentThread().getName();
。
通过查看该案例的打印结果,不难发现:三个小朋友一共抢到了50
个气球,符合了需求中规气球总共有50
个的要求。我们再来分析主函数中的代码,发现是因为3个线程共享了一个Balloon
对象,该对象中的气球数量就在50
个。
按照这样的思路,上述使用继承Thread
类的方式中出现的问题就可以解决了。接下来就对上述两种实现多线程的方式进行分析和总结:
使用继承Thread
类的方式:
- 使用继承方式来实现多线程在操作上会更加简便;比如:可以通过
super.getName()
来直接获取当前线程对象的名称; - 由于
Java
是单继承的,所以如果继承了Thread
,该类就不能再有其他的父类了; - 对于抢气球案例需求来说,并不能很好的解决问题;
使用实现接口的方式:
- 相较于继承方式,实现方式和线程操作会稍加复杂;比如:获取当前线程名称需要通过
Thread.currentThread().getName();
来获取; - 由于是使用实现的方式,
Java
是支持多实现的,所以除了Runnable
接口之外,还可以实现其他的接口,继承另外的类; - 能够很好的实现案例需求:多个线程共享一个资源;
完结,老夫虽不正经,但老夫一身的才华!关注我,获取更多编程基础知识。
「JAVA」通过抢气球案例,来梳理线程基础知识相关推荐
- java 同步解决不安全类_「JAVA」Java 线程不安全分析,同步锁和Lock机制,哪个解决方案更好...
线程不安全 线程不安全的问题分析:在小朋友抢气球的案例中模拟网络延迟来将问题暴露出来:示例代码如下: public class ImplementsDemo { public static void ...
- android string拼接字符串_「JAVA」细述合理创建字符串,分析字符串的底层存储,你不该错过...
Java基础之字符串操作--String 字符串 什么是字符串?如果直接按照字面意思来理解就是多个字符连接起来组合成的字符序列.为了更好的理解以上的理论,我们先来解释下字符序列,字符序列:把多个字符按 ...
- 「Java」基于Mirai的qq机器人开发踩坑笔记(其一)
目录 0. 前置操作 I. 安装MCL II. MCL自动登录配置 III. 安装IDEA插件 1. 新建Mirai项目 2. 编写主类 3. 添加外部依赖 4. IDEA运行 5. 插件打包 6. ...
- 「Java」基于Mirai的qq机器人开发踩坑笔记(其二)
目录 0. 配置机器人 1. onLoad方法 2. onEnable方法 3. 消息属性 4. 消息监听 I. 好友消息 II. 群聊消息 III. 无差别消息 5. 发送消息 I. 文本消息 II ...
- 超详细的Java面试题总结(四 )之JavaWeb基础知识总结
系列文章请查看: 超详细的Java面试题总结(一)之Java基础知识篇 超详细的Java面试题总结(二)之Java基础知识篇 超详细的Java面试题总结(三)之Java集合篇常见问题 超详细的Java ...
- 《Java并发编程实践》学习笔记之一:基础知识
<Java并发编程实践>学习笔记之一:基础知识 1.程序与进程 1.1 程序与进程的概念 (1)程序:一组有序的静态指令,是一种静态概念: (2)进程:是一种活动,它是由一个动作序列组成 ...
- (Java高级教程)第四章必备前端基础知识-第二节1:CSS概述和选择器
文章目录 一:CSS概述 (1)概述 (2)语法规范 (3)CSS引入方式 二:选择器 (1)基础选择器 ①:标签选择器 ②:类选择器 ③:id选择器 ④:通配符选择器 总结 (2)复合选择器 ①:后 ...
- Java中的线程基础知识
Java中的线程基础知识 1.线程概念 线程是程序运行的基本执行单元.当操作系统(不包括单线程的操作系统,如微软早期的DOS)在执行一个程序时,会在系统中建立一个进程,而在这个进程中,必须至少建立一个 ...
- 「Java」- 八大排序
目录 前言 1.冒泡排序 2.选择排序 3.插入排序 4.希尔排序 5.堆排序 6.快速排序 7.归并排序 8.计数排序 前言 由于本章介绍的大多数排序都需要用到数组两个元素之间进行交换操作 , 所以 ...
最新文章
- 技术图文:如何利用 Turtle 绘制一棵漂亮的樱花树
- python_Day5_web开发(下)
- Hystrix中的批量(折叠)请求
- python数据处理和数据分析的区别_python数据处理(七)之数据探索和分析
- liunxu mysql_Liunx下安装MySql
- Ubuntu18.04开机后图像界面消失解决(千万不要使用autoremove卸载软件!!!)
- 滚动长截图,截取第三方应用,如微博、知乎、头条长图文...
- OpenCV算法精解3--OpenCV中C++矩阵基本运算
- Win7专业版密码忘了使用U深度启动U盘清除登录密码
- DrawIO 基于MinIO以及OSS私有云方案
- Java设计模式 - 依赖倒转原则
- 全国企业信用信息 网站
- 真正的好老板,都是高层次的“给予者”
- 小程序审核失败:你的小程序涉及提供播放、观看等服务,请补充选择:文娱-其他视频类目。怎么解决呢
- 图灵 | 一站式图应用平台
- Tracert命令 路由跟踪数据包解析
- c# Dispose实现 MSDN官方教程
- js校验图片是否加载成功或失败
- 倍增算法入门 超详细解答+LCA+RMQ(ST表)+例题剖析
- SQL Server、Oracle 如何清除指定SQL的执行计划