Java并发基础,不怕你看不懂

一、前言

当我们使用计算机时,可以同时做许多事情,例如一边打游戏一边听音乐。这是因为操作系统支持并发任务,从而使得这些工作得以同时进行。

  • 那么提出一个问题:如果我们要实现一个程序能一边听音乐一边玩游戏怎么实现呢?
public class Tester {public static void main(String[] args) {System.out.println("开始....");playGame();playMusic();System.out.println("结束....");}private static void playGame() {for (int i = 0; i < 50; i++) {System.out.println("玩游戏" + i);}}private static void playMusic() {for (int i = 0; i < 50; i++) {System.out.println("播放音乐" + i);}}
}

我们使用了循环来模拟过程,因为播放音乐和打游戏都是连续的,但是结果却不尽人意,因为函数体总是要执行完之后才能返回。那么到底怎么解决这个问题?

并行与并发

Java并发基础,不怕你看不懂

 

并行性和并发性是既相似又有区别的两个概念。

并行性是指两个或多个事件在同一时刻发生。而并发性是指两个或多个事件在同一时间间隔内发生。

在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机环境下(一个处理器),每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。例如,在 1 秒钟时间内,0 - 15 ms 程序 A 运行;15 - 30 ms 程序 B 运行;30 - 45 ms 程序 C 运行;45 - 60 ms 程序 D 运行,因此可以说,在 1 秒钟时间间隔内,宏观上有四道程序在同时运行,但微观上,程序 A、B、C、D 是分时地交替执行的。

如果在计算机系统中有多个处理机,这些可以并发执行的程序就可以被分配到多个处理机上,实现并发执行,即利用每个处理机爱处理一个可并发执行的程序。这样,多个程序便可以同时执行。以此就能提高系统中的资源利用率,增加系统的吞吐量。

进程和线程

进程是指一个内存中运行的应用程序。一个应用程序可以同时启动多个线程,那么上面的问题就有了解决的思路:我们启动两个进程,一个用来打游戏,一个用来播放音乐。这当然是一种解决方案,但是想象一下,如果一个应用程序需要执行的任务非常多,例如 LOL 游戏吧,光是需要播放的音乐就有非常多,人物本身的语音,技能的音效,游戏的背景音乐,塔攻击的声音等等等,还不用说游戏本身,就光播放音乐就需要创建许多许多的进程,而进程本身是一种非常消耗资源的东西,这样的设计显然是不合理的。更何况大多数的操作系统都不需要一个进程访问其他进程的内存空间,也就是说,进程之间的通信很不方便,此时我们就得引入“线程”这门技术,来解决这个问题。

线程是指进程中的一个执行任务(控制单元),一个进程可以同时并发运行多个线程。我们可以打开任务管理器,观察到几乎所有的进程都拥有着许多的「线程」(在 WINDOWS 中线程是默认隐藏的,需要在「查看」里面点击「选择列」,有一个线程数的勾选项,找到并勾选就可以了)。

Java并发基础,不怕你看不懂

 

进程和线程的区别

进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。

线程:堆空间是共享的,栈空间是独立的,线程消耗的资源也比进程小,相互之间可以影响的,又称为轻型进程或进程元。

因为一个进程中的多个线程是并发运行的,那么从微观角度上考虑也是有先后顺序的,那么哪个线程执行完全取决于 CPU 调度器(JVM 来调度),程序员是控制不了的。我们可以把多线程并发性看作是多个线程在瞬间抢 CPU 资源,谁抢到资源谁就运行,这也造就了多线程的随机性。下面我们将看到更生动的例子。

Java 程序的进程(Java 的一个程序运行在系统中)里至少包含主线程和垃圾回收线程(后台线程),你可以简单的这样认为,但实际上有四个线程(了解就好):

  • [1] main——main 线程,用户程序入口
  • [2] Reference Handler——清除 Reference 的线程
  • [3] Finalizer——调用对象 finalize 方法的线程
  • [4] Signal Dispatcher——分发处理发送给 JVM 信号的线程

多线程和单线程的区别和联系?

  1. 单核 CPU 中,将 CPU 分为很小的时间片,在每一时刻只能有一个线程在执行,是一种微观上轮流占用 CPU 的机制。
  2. 多线程会存在线程上下文切换,会导致程序执行速度变慢,即采用一个拥有两个线程的进程执行所需要的时间比一个线程的进程执行两次所需要的时间要多一些。

结论:即采用多线程不会提高程序的执行速度,反而会降低速度,但是对于用户来说,可以减少用户的响应时间。

多线程的优势

尽管面临很多挑战,多线程有一些优点仍然使得它一直被使用,而这些优点我们应该了解。

优势一:资源利用率更好

想象一下,一个应用程序需要从本地文件系统中读取和处理文件的情景。比方说,从磁盘读取一个文件需要 5 秒,处理一个文件需要 2 秒。处理两个文件则需要:

1| 5秒读取文件A
2| 2秒处理文件A
3| 5秒读取文件B
4| 2秒处理文件B
5| ---------------------
6| 总共需要14秒

从磁盘中读取文件的时候,大部分的 CPU 时间用于等待磁盘去读取数据。在这段时间里,CPU 非常的空闲。它可以做一些别的事情。通过改变操作的顺序,就能够更好的使用 CPU 资源。看下面的顺序:

1| 5秒读取文件A
2| 5秒读取文件B + 2秒处理文件A
3| 2秒处理文件B
4| ---------------------
5| 总共需要12秒

CPU 等待第一个文件被读取完。然后开始读取第二个文件。当第二文件在被读取的时候,CPU 会去处理第一个文件。记住,在等待磁盘读取文件的时候,CPU 大部分时间是空闲的。

总的说来,CPU 能够在等待 IO 的时候做一些其他的事情。这个不一定就是磁盘 IO。它也可以是网络的 IO,或者用户输入。通常情况下,网络和磁盘的 IO 比 CPU 和内存的 IO 慢的多。

优势二:程序设计在某些情况下更简单

在单线程应用程序中,如果你想编写程序手动处理上面所提到的读取和处理的顺序,你必须记录每个文件读取和处理的状态。相反,你可以启动两个线程,每个线程处理一个文件的读取和操作。线程会在等待磁盘读取文件的过程中被阻塞。在等待的时候,其他的线程能够使用 CPU 去处理已经读取完的文件。其结果就是,磁盘总是在繁忙地读取不同的文件到内存中。这会带来磁盘和 CPU 利用率的提升。而且每个线程只需要记录一个文件,因此这种方式也很容易编程实现。

优势三:程序响应更快

有时我们会编写一些较为复杂的代码(这里的复杂不是说复杂的算法,而是复杂的业务逻辑),例如,一笔订单的创建,它包括插入订单数据、生成订单赶快找、发送邮件通知卖家和记录货品销售数量等。用户从单击“订购”按钮开始,就要等待这些操作全部完成才能看到订购成功的结果。但是这么多业务操作,如何能够让其更快地完成呢?

在上面的场景中,可以使用多线程技术,即将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列),如生成订单快照、发送邮件等。这样做的好处是响应用户请求的线程能够尽可能快地处理完成,缩短了响应时间,提升了用户体验。

其他优势

多线程还有一些优势也显而易见:

  • 进程之前不能共享内存,而线程之间共享内存(堆内存)则很简单。
  • 系统创建进程时需要为该进程重新分配系统资源,创建线程则代价小很多,因此实现多任务并发时,多线程效率更高.
  • Java 语言本身内置多线程功能的支持,而不是单纯地作为底层系统的调度方式,从而简化了多线程编程.

上下文切换

即使是单核处理器也支持多线程执行代码,CPU 通过给每个线程分配 CPU 时间片来实现这个机制。时间片是 CPU 分配给各个线程的时间,因为时间片非常短,所以 CPU 通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。

CPU 通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务的时候,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

这就像我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书独到了多少页的多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。

二、创建线程的两种方式


继承 Thread 类

public class Tester {// 播放音乐的线程类static class P


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部