目录
概述
并发在技术层面上的问题
volatile&synchronized&cas
概述
一晃的时间,做Java开发已经有七个年头了,过去的几年多数时候其实是混混度日,没有对自己的工作和生活有太多的深入思考,当我意识到的时候,犹如晴天霹雳,这段时间思考了很多但依然没有什么头绪,我想或许应该换一种策略,找出几个有代表性的问题去深入思考一下,而技术的意义就是其中一个。
而之所以再次来写Java并发,原因在于对这块知识的理解其实一直的片面的,稍微回想一些相关的知识点,好像都有所了解,但对于并发的意义以及Java并发模块的整体设计思路和相关的思想并没有一个比较深入的理解,所以借此机会,再来探索一下Java并发。这次和以前相比,会更侧重于实现原理和设计思想,而不是范范的罗列一些人尽皆知的知识点。好了,那就开始吧。
并发这个词,从广义上理解应该是多件事情同时发生,比如经常听到的并发症指的是患一种病的同时又患了由它引起的另一种疾病,那延伸到计算机的世界也是一样,但由于在现代计算机中的最小执行单元是线程,所以一说到并发大多数人就会想到多线程,但这样理解还是有点狭隘,比如对于操作系统用户来说,多个应用程序同时执行这本身也是一种并发,所以,我对于并发的广义理解是,多件同时发生的事或多件同时正在做的事,这里的”事“可以是生病,可以是旅行,可以是线程,可以是进程,可以是任何事。
那么,在Java中的并发指的是多线程,也就是多个线程同时执行。这里需要明确两个概念,并发和并行,并发指的是做多件事情,但不是在同一时间做,这就好比一个人他同一时刻只能做一件事情,但在一天内可以做多件事情,而并行指的是在同一时刻同时做多件事情,这种事情人是做不到的,计算机之所以能做到并行,是因为多cpu/cpu多核的出现,如果是单cpu,计算机也只能通过cpu时间片轮转的方式实现并发,但没办法实现并行。
凡事都有利弊两面,作为一个面向对象设计语言,Java在引入多线程时,解决了什么问题?有带来的什么问题?对于这些多线程的”并发症“又是如何解决的?这三个基本问题必须贯穿整个学习Java并发的过程。
多线程主要解决了三个方面的问题:
- 提高程序的响应速度
- 简化编程模型
- 最大化的使用计算机资源
在计算机中,一个线程就相当于一个干活的人,当一个工作不能在给定的时间内完成时,增加人手是一个有效的选择,春节期间武汉修建火神山雷神山就是一个很好的利器,另外,引入多线程可以优化原有的顺序编程模型,对于一些常场景来说极大的简化了实现,最有代表性的是生产者-消费者模式和java图形化界面,如果用顺序编程模式就比较难实现,第三,尽最大努力压榨计算机资源,别让它占着茅坑不拉屎,比较计算资源还是比较贵的,尤其是一些大型的服务器。
多线程再带来好处的同时,也带来了一些并发症:
- 线程切换带来的开销
- 死锁问题
- 计算机资源本身带来的限制
当线程的数量超过cpu的数量时,并行就转换成了并行+并发,而并发本身其实是通过cpu的时间片轮转实现的,如果线程的数量特别多,会造成线程上下文切换的开销大于它带来的性能提升,所以当线程数量达到一个临界值时,程序的整体性能就开始下降了,第二个问题是死锁,死锁问题源于多线程操作共享资源时的锁的开启和释放,第三是计算机资源本身的限制,造成了不能一直通过增加线程的方式来提高程序性能。
并发在技术层面上的问题
从技术层面讲,学习Java并发其实就是学习Java是如何解决并发编程中的问题的的,那么并发问题的根源都可以归结为以下几个方面:
- 可见性
- 原子性
- 有序性
可见性问题是由现代cpu硬件架构带来的,现在cpu架构多采用多cpu/cpu多核 + 多级缓存的架构,cpu在运行时会首先从主内存中将数据加载到cpu缓存中进行计算,计算完成后再写入主内存,另外,java线程模型在cpu多级缓存架构的基础上,也具备这样的特性,每个线程都有自己的工作内存,线程在计算数据时,会先把数据从主内存加载到自己的工作内存,再写回到主内存,这种架构模式在多线程环境下,如何保证每个线程工作内存中的主数据的副本的一致性就成了一个巨大的挑战,这就是内存可见性问题。
原子性问题本质上其实是由cpu时间片轮转引起的,如果一个操作在cpu级别不是一个原子指令,那么它在执行的时候很可能会被打断,这样多个线程交叉在一起就可能造成计算结果错误。
有序性问题其实是由两个因素引起的,一是cpu为了提高计算速度,会对待执行的指令集进行乱序优化,二是java编译器在编译代码的时候,为了最大化性能,也会对代码指令进行重新编排以达到性能的最大化(java编译器在重排序指令时只能保证单线程环境下结果的正确性)。
Java作为一个支持并发编程的面向对象语言,必须解决这三个问题,才能为程序员提供稳定、好用的并发编程环境。从总体上看,Java通过一系列的关键字、并发组件等来解决上述问题,比如volatile、synchronized、Atomic、AQS、BlockingQueue等等,而学习解决并发编程中出现的问题的关键字和组件其实就是我们学习java并发编程的重点。
从问题入手总比一上来就学有效的多。
volatile&synchronized&cas
volatile在一些并发框架和本地缓存框架的源码中经常能看到,它的主要作用有两个
- 保证内存可见性
- 禁用指令重排序
volatile是在编译时插入一个#lock信号来保证内存可见性的,这个#lock信号的作用就是,当前线程对volatile变量进行修改时,会强迫其他线程其他线程工作内存中该变量的副本失效,从而强迫其他线程从主内存中重新拉取最新的数据。而对于禁用指令重排序这一点,其实并不是完全禁用,知识在对被volatile修饰的变量进行指令重排序时,需要遵循一个happens-before规则:
- 对一个volatile变量的写,happens-before于任意后续对这个volatile变量的读
java是通过插入内存屏障的方式实现这个happens-before规则的,正因为volatile有这个特性,有时也把它称为轻量级的锁,通过使用volatile就可以避免通过同步锁的方式实现对单个变量操作的线程安全。一个比较经典的场景是:实现一个双检锁模式的单例
public class Singleton {private static volatile Object instance;public Object getInstance() {if (Objects.isNull(instance)) {synchronized (Singleton.class) {if (Objects.isNull(instance)) {instance = new Object();}}}return instance;}}
另外,volatile一般用来修饰状态变量,比如AQS中的同步状态
/*** Returns the current value of synchronization state.* This operation has memory semantics of a {@code volatile} read.* @return current state value*/protected final int getState() {return state;}