大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚!
死鬼~看完记得给我来个三连哦!
本文主要介绍
Java并行的入门
如有需要,可以参考
如有帮助,不忘 点赞 ❥
忘掉那该死的并行
在2014年底的 Avoiding ping pong论坛上,Linus Torvalds 提出了一个截然不同的观点,他说:***“忘掉那该死的并行把!”***
(原文: Give it up . The whole “parallel computing is the future” is a bunch of crock)
看到这个消息,突然心里一紧,还没记住就要我忘记岂不美滋滋
但是想要做到不菜的小蔡,发现事情并不简单~ 开发中我们都想用多线程来处理程序,难道不是为了让程序变快吗,这TM让我为难了呀!
什么是并行呢?
并行程序会比串行程序更容易适应业务需求。
简单来讲就是
:一家三口,你去上学,老妈在家干家务,老爸上班赚钱。在同一个时间段,三个人在做不同的事情,让生活变得更加美满。如果是串行的情况,就是一个人要身兼多职,一个人干三个人的活,你说这可咋整。
专业来讲就是
:Java虚拟机是很忙的,除了要执行 main 函数主线程外,还要做 JIT 编译,垃圾回收等待。那这些事情在虚拟机内部都是单独的一个线程,一起操作,每个任务相互独立,更容易理解和维护。
忘掉是不可能忘掉的,先不说我还有没有记住,那么不忘掉就要更努力的使用好它,来吧,牵着小菜的手,咱们一起征服它!
几个重要概念
同步(Synchronous)和异步(Asynchronous)
同步 和 异步 通常用来形容一次方法的调用。
同步
: 同步方法调用一旦开始,调用者必须等到方法执行结束,才能继续后续的行为。
异步
:异步方法就像是一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。执行方法通常会在另外一个线程中执行,不会阻碍到调用者的工作。
简单来讲就是:同步的话就是你去车站买票,必须排队等待,排到的时候才能进行买票,然后去做其他事情。异步的话就是你可以在网上买票,完成支付后,你的票也到手了,期间你也可以做其他事情。
并发(Concurrency)和并行(Parallelism)
并发和并行 是两个特别容易混淆的概念。
并行
:是真正意义上的多个任务**“同时执行”**。
并发
:多个任务交替执行,多个任务之间可能还是串行的。
实际开发中:如果系统内只有一个 CPU,这个时候使用多进程或者多线程执行任务,那么这些任务不可能是真实并行的,而是并发,采用时间片轮转的方式。
临界区
临界区 是用来表示一种公共资源或者是一种共享数据,可以被多个线程共同使用。但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程想要使用这个线程就必须要等待。
简单来讲就是
:有一台打印机,打印机一次只能执行一个任务,如果两个人同时要使用打印机,那么 A 同学只能等 B 同学使用完打印机,才能打印自己的材料。
在并行程序中,临界区资源就是要保护的对象。
阻塞(Blocking)和非阻塞(Non-Blocking)
阻塞和非阻塞 用来形容多线程间的相互影响。
阻塞
:A 同学占用了打印机,B 同学想要使用打印机就必须要等待 A 同学使用完成后才能使用打印机。如果 A 同学一直占用着打印机不肯让别人用,那么就会导致其他同学无法正常工作。
非阻塞
:A 同学占用了打印机,但是妨碍不到 B 同学的正常工作,B 同学可以去做其他事情。
死锁(DeadLock)、饥饿(Starvation)和活锁(LiveLock)
死锁
:如图上四个线程相互等待,构成环形。他们彼此之间都不愿意释放自己拥有的资源,那么这个状态将永远持续下去,谁都不可能出圈。
饥饿
:A 同学在食堂窗口打饭,B 同学在后面排队,这个时候来了 C、D…好几个同学直接插队在了 B 同学的后面,后续如果有同学来继续在 B 同学前面插队,这样导致的结果就是 B 同学永远打不到饭,那么就会出现饥饿的现象。此外,如果某一个线程一直占着关键资源不放,导致其他需要这个资源的线程无法正常执行,这种情况也是饥饿的一种。
活锁
:一条走廊上,A 同学想要通过,迎面走来了 B 同学,但是很不巧的是两个同学相互挡住,这时候 A 同学往右边让路,B 同学也往右边让路,A 同学又往左边让路,B 同学也往左边让路,反复后,最终还是会让出一条路。但是两个线程遇见这种情况,就没有人类那么智能,它们会相互堵上,资源在两个线程间不停的跳动,导致没有一个线程可以拿到资源,这就是活锁的情况。
并发级别
并发级别 可以分为:
当一个线程是阻塞的时候,在其他线程释放资源之前,当前线程无法继续执行。例如使用synchronized或者重入锁之前,我们得到的就是阻塞的线程。
如果线程之间存在优先级,那么线程调度的时候总会倾向于高优先级的线程,也就是不公平的。
非公平锁
:系统允许高优先级的线程插队,这样有可能会导致低优先级线程产生饥饿。
公平锁
:按照先来后到的顺序,不管新来的优先级多高,就必须排队,那么饥饿就不会产生。
- 无障碍
无障碍是一种最弱的非阻塞调度。两个线程如果无障碍地执行,不会因为临界区的问题导致一方挂起。
如果说阻塞是*悲观策略***,那么非阻塞***就是***乐观策略***。无障碍的多线程程序并非能够顺利执行,如果临界区资源严重冲突的时候,那么所有线程都会回滚自己的操作,导致没有一个线程能够走出临界区。
可以使用 CAS(Compare And Set)策略来实现无障碍的可行性。设置一个***一致性标志***,线程在操作之前,先读取并保存这个标志,操作完成后,再次读取这个标志,判断是否被修改,如果是一致则说明资源访问没有冲突。如果不一致,则说明资源可能在操作过程中与其他线程发生冲突,需要重试操作。
因此,任何线程对资源有操作的过程中,都应该更新这个一致性标志,表示数据不再安全。
- 无锁
无锁的并行都是无障碍的。在无锁的情况下,任何线程都能对临界区进行访问,不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区
- 无等待
无锁只要求一个线程能够在有限步内完成操作离开临界区,而无等待则在无锁的基础上更进一步扩展,它要求所有的线程都必须在有限步内完成。
一种典型的无等待结构就是RCU(Read Copy Update),它的基本思想是,在读取的时候可以不加控制,在写数据的时候,先取得原始数据的副本,修改完成后,再写回数据
JMM(Java Memory Model)
JMM 关键技术点都是围绕着多线程的原子性,可见性和有序性来建立的。
原子性(Atomicity)
原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
简单来讲就是
:有一个静态全局变量 i ,两个线程同时对它赋值,A 线程给它赋值为 1,B 线程给它赋值为 2,那么不管以任何方式操作,i 的值不是 1 就是 2,两个线程之间是没有任何干扰的。
注意:如果使用的是 long 类型而不是 int 类型,可能就会出现问题。因为long类型的读写不是原子性的(long类型有64位)
可见性(Visibility)
可见性是指一个线程修改了某一个共享变量的值时,其他线程能够立即知道这个值发生修改。可见性问题对于串行的系统是不存在的,因为你在任何一个操作步骤中修改了某个变量,后续的步骤中读到的一定是修改后的变量。
两个线程共享变量,由于编译器优化或硬件优化的缘故,B 线程将变量做了优化,将其设置在了缓存cache中或寄存器中,这个时候如果 A 线程对变量进行了修改,那么 B 线程将无法意识到这个改动,依然会读取存储在缓存中的旧值
有序性(Ordering)
对于一个线程的执行代码而言,我们总是习惯性地任务代码是从前往后依次执行的。当然,这是针对于整个程序只有一个线程的情况下。在多线程的情况下,程序在执行的时候可能会出现乱序,也就是说写在前面的代码,会在后面执行。这是因为程序执行时会进行指令重排,重排后的指令与原指令的顺序未必一致。
如果 A 线程首先执行了 writer()
方法,紧接着 B 线程执行了 reader()
方法,这个时候发生指令重排,那么 B 线程在执行i = a + 1
的时候就不能看到a = 1
了。
这里需要注意的是: 对于一个线程来说,它看到的指令执行顺序一定是一致的(否则应用根本无法正常工作)。指令重排的前提就是:保证串行语义的一致性。
哪些指令不能重排(Happen-Before规则)
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile变量的写先于读发生,这保证了 volatile 变量的可见性
- 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
- 传递性:A 先 于 B,B 先于 C,那么 A 必然先于 C
- 线程的
start()
方法先于它的每一个动作 - 线程的中断
interput()
先于被中断线程的代码 - 对象的构造函数的执行,结束先于
finalize()
方法
今天的你多努力一点,明天的你就能少说一句求人的话!
我是小菜,一个和你一起学习的男人。
💋