前言
最近在总结《Java多线程编程核心技术》这本书。实话实说:多线程编程核心技术,这些字眼属实有些夸大。
但是也不能因为此,就直接否认了书籍本身的价值。这是一篇比较适合入门阅读的书籍。和我最近在尝试写的文章有相似之处,也就是尽力让知识点,少思考性而多阅读性。
因此接下来会将这本书的内容,揉到我的接下来的文章中,旨在:真正能在碎片化时代下,进行有效学习。
正文
毫无疑问,一切的开始。肯定是先介绍介绍概念性的东西。主题是线程,但是谈到线程,势必不能忘了进程。因此让我们先来聊一聊线程和进程的概念。
接下来让我们有请文章一贯的主角:小A和MDove出场~
进程和线程
小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:别急,聊了这么久厕所的话题,容我上个厕所,咱们下期再来聊一聊线程的使用。