[短文速读 -5] 多线程编程引子:进程、线程、线程安全

601 阅读10分钟

前言

最近在总结《Java多线程编程核心技术》这本书。实话实说:多线程编程核心技术,这些字眼属实有些夸大。

但是也不能因为此,就直接否认了书籍本身的价值。这是一篇比较适合入门阅读的书籍。和我最近在尝试写的文章有相似之处,也就是尽力让知识点,少思考性而多阅读性。

因此接下来会将这本书的内容,揉到我的接下来的文章中,旨在:真正能在碎片化时代下,进行有效学习。

爱因斯坦:“如果你不能简单地解释一样东西,说明你没真正理解它。”

[短文速读-1] a=a+b和a+=b的区别

[短文速读-2] 重载/重写,动/静态分派?(重新修订)

[短文速读-3] 内部匿名类使用外部变量为什么要加final

[短文速度-4] new子类是否会实例化父类

[短文速读 -5] 多线程编程引子:进程、线程、线程安全

正文

毫无疑问,一切的开始。肯定是先介绍介绍概念性的东西。主题是线程,但是谈到线程,势必不能忘了进程。因此让我们先来聊一聊线程和进程的概念。

接下来让我们有请文章一贯的主角:小AMDove出场~

进程和线程

小A:MDove,我最近在思考一个问题,多线程多线程。那到底什么是多线程呢?

MDove:想要理解多线程,其实就要不得不提一提:进程,以及进程和线程之间的关系。

一个比较通俗且常见的解释:进程:操作系统分配资源的基本单位;线程:操作系统调度最小单位。

稍稍学院派的解释:

来自《现在操作系统(第4版)》 + 维基百科 + 百度百科

进程:

  • 进程的本质是正在执行的一个程序,进程基本上上容纳运行一个程序所需要的所有信息的容器。
  • 在当代多数操作系统中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序的真正运行实例。
  • 早期面向进程设计的计算机结构中,进程是系统进行资源分配和调度的基本单位。

小A:哦?既然进程是程序的实体,那么只要进程不就行了?

MDove:这当然是没有问题啦。但是,一个致命的问题,大家都追求快,更快,非常快!

小A:不不不不不,我就追求慢,坚挺~

MDove:坚挺是吧,你这么秀,你怎么不去Tokyo Hot?搁这给我扯犊子。学不学了?不学你去找加藤鹰去。

小A:学学学,我的理解:是不是线程比进程占用资源更少?

MDove:其实关于消耗资源这个问题很难回答。比如Linux、Windows对进程和线程的设计就是不同的。如果从Linux的角度出发,进程和线程无论是创建还是上下文切换其实不在极端情况下,所谓的性能还是资源,差距并不是很大。差距比较大的一点是:进程是内存独立,而线程内存共享。

MDove:当然,二者都可以悄摸得去做一些事情,但不同点在于:进程间通讯,远比线程间通讯复杂的。因此在上层高级语言的设计上,线程便成了不错的用于后台完成耗时操作的一个工具。但是不可否认的一点,多进程同样扮演者举足轻重的角色!打个比方,单进程的程序,如果杀死这个进程那么这个进程所依附的所有线程全部死亡。而多进程则不怕,毕竟彼此是独立运行的进程,因此多进程在拉活方面有着不俗的战斗力。

小A:哦~原来如此,那可以聊一聊线程么?

MDove:好的,接下来让我们看一看线程。


线程:

  • 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
  • 线程可以为操作系统内核调度的内核线程;由用户进程自行调度的用户线程

MDove:举个小例子:打开我们计算机上的任务管理器时,进程Tab页上,我们看到的就是进程;而独立进程程序的子任务就是线程(不绝对,也可以存在多进程的程序)。比如:QQ运行时(进程),就有很多子任务(线程)在同时运行:你即能一遍和基友视频,一遍还能和其他基友文字聊天这就体现了多线程,其中每一项子任务都可以理解为线程

MDove:对于我们开发者来说线程是一个很常见的概念。对于Java后台来说,能够熟练的掌握对多线程的使用。可以说是掌握核心科技一样。

小A:???听你这么说,多线程没什么难的呀?

多线程的弊端

MDove:初生牛犊不怕虎,但你要明白,虎终究是虎。多线程的一大难点在于线程安全问题。因为内存共享的原因,导致了线程刷新内存的滞后性。

小A:???什么意思???

MDove:打个比方,在Java线程模型中:每个线程运行时,都会把主内存中的变量值复制到自己的工作内存当中,当自己执行完毕后,在把计算完毕的工作内存中的变量,反过来赋值给主内存。

小A:嗯?这好像没什么问题啊?

MDove:没问题?问题大了去了!咱们举一个花钱的例子:现在咱们账本上有十个亿。你是一个线程,我是一个线程。

  • 我先执行了,我拿了一个亿,去建设了一下社会主义核心价值观,然后我在我的账本里记了一下账:我拿了1个亿,手头还有9个亿

此时咱们的账本还剩9个亿

  • 接下来,你也出动了,你拿了9个亿中的一个亿,你也在你的账本里记了一下账:我拿了1个亿,还有8个亿。然后你去吸烟喝酒烫头了。

此时咱们的账本还剩8个亿

  • 这个时候我干完了我的事,我花了5千万,还剩5千万。然后对于我来说我的账本变成了,还有9亿5千万。此时我把我的账本写到了咱们们公共账本里...

小A:等会等会?你想啥呢?钱越花越多了!我都拿走1个亿了,你那咋还9个亿呢?你就不会同步一个我的账本么?

MDove:没错呀,我就不会同步一个你的账本呀!这就是多线程的问题所以。因为每个线程是独立运行的,谁都不管谁,因此,如何同步线程之间的数据便成了至关重要的一点!

小A:这么一说我就明白了,那怎么同步呢?

保证线程安全

可见性,原子性

MDove:其实刚才的问题就出现在账本的不同步上,因此如果我们能够解决账本的同步问题,理论上就可以解决咱们的线程安全问题。当然你可以用一些手段通知我,让我更新我的账本(可见性)。但是仍存在问题,如果我们写账本的操作是个多步骤的复杂操作,可能就会存在问题了,因为这个里每一步通存在同步问题,只有当我们的写账本操作是一个单一操作(原子性)那么这种做法就是没有问题的。

小A:局限还挺多,那有没有其他方式呢?

加锁

MDove:接下来说一个加锁的方式。举个咱们上厕所的例子。我们很多人都要上厕所,如果我们的厕所没有任何措施,那么画面简直无法想象。因此,咱们...那啥...大号...的时候,都会把门锁上。其他人一看门锁上了,也就只好默默的憋一会。

MDove:当我们解决后,打开门,其他人就可以尽情释放了。

那么这个过程就相当于:一个线程在操作一个变量时,直接把这个变量锁起来。其他线程只能等着,因此肯定就不会存在多个线程同时操作变量的问题了。

线程优先级

小A:那么接下来就是队伍中第一个人去...那啥么?

MDove:当然不是,线程的世界可没有什么先来后到。而是按线程的优先级去排列。那么如果没有优先级,那就看谁的拳头更硬谁的运气更好了。

小A:好残暴,那有没有顺利排列的可能性呢?那我们可不可以自己固定一个顺序去开启线程的执行呢?

线程执行顺序

MDove:我们当然可以指定一种策略去顺序的start我们的线程,但是我们只能保证线程start的顺序,没办法保证线程调度的顺序。因为对于线程来说,什么时候能够获得操作系统的宠幸那是不确定的。因此线程会有多种状态:

  • 新建(NEW),表示线程被创建出来还没真正启动的状态。

  • 就绪(RUNNABLE),表示该线程已经在 JVM 中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它 CPU 片段,在就绪队列里面排队。

  • 阻塞(BLOCKED),阻塞表示线程在等待锁释放。比如,线程试图去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态。

  • 等待(WAITING),表示正在等待其他线程采取某些操作。。

  • 终止(TERMINATED),不管是意外退出还是正常执行结束,线程已经完成使命,终止运行。

当然也可以加上一个:运行(RUNNING):可运行状态(RUNNABLE)的线程获得了CPU时间片,开始执行代码。

分段锁

MDove:咱们再回到那个厕所的例子。我们日常中...厕所肯定不止一个坑位,一般会有好几个。毕竟都是解决同样的问题,没有只设置一个坑位的道理。

MDove:所以对于我们程序来说也是如此,在面对类型的场景也会有类似的实现,这个方式就叫做分段锁。比如:ConcurrentHashMap,JDK1.8版本之前的设计。

MDove:当然关于锁这个话题,其实水是很深的。我们嘚吧嘚说了这么多,其实就是在解决多线程安全问题。因此明白了吧,多线程是一个值得深入学习的内容。

小A:学的我热血沸腾的,接下来教教我,Java中对线程的使用吧!

MDove:别急,聊了这么久厕所的话题,容我上个厕所,咱们下期再来聊一聊线程的使用。

剧终

我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,以及我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

个人公众号:IT面试填坑小分队