【Java高并发系列】之走进并发世界

你们好,我是小菜,一个渴望在互联网行业作到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚! 死鬼~看完记得给我来个三连哦!缓存

本文主要介绍 Java并行的入门安全

若有须要,能够参考markdown

若有帮助,不忘 点赞多线程

忘掉那该死的并行

在2014年末的 Avoiding ping pong论坛上,Linus Torvalds 提出了一个大相径庭的观点,他说:“忘掉那该死的并行吧!”并发

(原文: Give it up . The whole "parallel computing is the future" is a bunch of crock)app

看到这个消息,忽然内心一紧,还没记住就要我忘记岂不美滋滋异步

可是想要作到不菜的小蔡,发现事情并不简单~ 开发中咱们都想用多线程来处理程序,难道不是为了让程序变快吗,这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() 方法
看完不赞,都是坏蛋
看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一块儿学习的男人。 💋

相关文章
相关标签/搜索