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

简介: 本文主要介绍 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() 方法


目录
相关文章
|
6月前
|
安全 Java 编译器
揭秘JAVA深渊:那些让你头大的最晦涩知识点,从泛型迷思到并发陷阱,你敢挑战吗?
【8月更文挑战第22天】Java中的难点常隐藏在其高级特性中,如泛型与类型擦除、并发编程中的内存可见性及指令重排,以及反射与动态代理等。这些特性虽强大却也晦涩,要求开发者深入理解JVM运作机制及计算机底层细节。例如,泛型在编译时检查类型以增强安全性,但在运行时因类型擦除而丢失类型信息,可能导致类型安全问题。并发编程中,内存可见性和指令重排对同步机制提出更高要求,不当处理会导致数据不一致。反射与动态代理虽提供运行时行为定制能力,但也增加了复杂度和性能开销。掌握这些知识需深厚的技术底蕴和实践经验。
132 2
|
6月前
|
安全 Java 调度
解锁Java并发编程高阶技能:深入剖析无锁CAS机制、揭秘魔法类Unsafe、精通原子包Atomic,打造高效并发应用
【8月更文挑战第4天】在Java并发编程中,无锁编程以高性能和低延迟应对高并发挑战。核心在于无锁CAS(Compare-And-Swap)机制,它基于硬件支持,确保原子性更新;Unsafe类提供底层内存操作,实现CAS;原子包java.util.concurrent.atomic封装了CAS操作,简化并发编程。通过`AtomicInteger`示例,展现了线程安全的自增操作,突显了这些技术在构建高效并发程序中的关键作用。
95 1
|
3月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
3月前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
3月前
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
94 2
|
3月前
|
缓存 监控 Java
Java 线程池在高并发场景下有哪些优势和潜在问题?
Java 线程池在高并发场景下有哪些优势和潜在问题?
|
4月前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
57 1
|
4月前
|
设计模式 缓存 Java
Java高并发处理机制
Java高并发处理机制
44 1
|
5月前
|
Java API 容器
JAVA并发编程系列(10)Condition条件队列-并发协作者
本文通过一线大厂面试真题,模拟消费者-生产者的场景,通过简洁的代码演示,帮助读者快速理解并复用。文章还详细解释了Condition与Object.wait()、notify()的区别,并探讨了Condition的核心原理及其实现机制。
|
4月前
|
消息中间件 前端开发 Java
java高并发场景RabbitMQ的使用
java高并发场景RabbitMQ的使用
158 0

热门文章

最新文章