程序在没有流程控制的前提下,代码都是 从上而下逐行依次执行 的。基于这样的机制,如果我们使用程序来实现边打游戏,边听音乐的需求时,就会很困难;因为按照执行顺序,只能从上往下依次执行;同一时刻,只能执行听音乐和打游戏的其中之一。为了解决这样的问题,在程序设计中引入了多线程并发。本文中的知识对 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"); }
public static void main(String []args) throws IOException { // 方式二:通过通过java.lang.ProcessBuilder来实现打开 cmd ProcessBuilder pb = new ProcessBuilder("cmd"); pb.start(); }
一、通过 继承Thread类 创建线程;需要注意的是:只有 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
子类会有所不同;
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 { @Override public 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() { @Override public 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() { @Override public 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; } @Override public 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; @Override public 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
类的方式中出现的问题就可以解决了。接下来就对上述两种实现多线程的方式进行分析和总结:
super.getName()
来直接获取当前线程对象的名称; Java
是单继承的,所以如果继承了 Thread
,该类就不能再有其他的父类了; Thread.currentThread().getName();
来获取; Java
是支持多实现的,所以除了 Runnable
接口之外,还可以实现其他的接口,继承另外的类; 在下一篇文章中,我会继续使用上述的案例来分析线程不安全以及相关的解决方法,敬请期待。
完结。老夫虽不正经,但老夫一身的才华