并发编程,似乎是每个大厂面试必问的技术。
原因之一:大厂的项目有很大的用户量,在生产环境的确会遇到各种并发问题。 原因之二:并发相关知识会涉及到很基础、很底层的概念和原理。所以对面试者而言,如果并发技术知识很扎实,那么可以肯定他的编程基础很扎实,懂原理,是个可塑之才,自然会被大厂青睐。
所以要想成为优秀的Java工程师,学好并发编程是我们必备的能力。
一、JMM模型
Java内存模型(Java Memory Model简称JMM)是一种抽象的概念! JMM描述的是一组规则,通 过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式。
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,线程与主内存中的变量操作必须通过工作内存间接完成,主要过程是将变量 从主内存拷贝的每个线程各自的工作内存空间,然后对变量进行操作,操作完成后再将变量 写回主内存,如果存在两个线程同时对一个主内存中的实例对象的变量进行操作就有可能诱 发线程安全问题。
一个变量如何从主内存拷贝到工作 内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来 完成。 (1)lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态 (2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后 的变量才可以被其他线程锁定 (3)read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存 中,以便随后的load动作使用 (4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工 作内存的变量副本中 (5)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎 (6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内 存的变量 (7)store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存 中,以便随后的write的操作 (8)write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值 传送到主内存的变量中
二、volatile关键字
1、被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改 了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。 缓存一致性协议MESI: MESI 是指4中状态的首字母:M 修改 (Modified)、E 独享、互斥 (Exclusive)、S 共享 (Shared)、I 无效 (Invalid)。
2、禁止指令重排序优化。 了解内存屏障(Memory Barrier)概念: 内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执 行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于 编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉 编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插 入内存屏障禁止在内存屏障前后的指令执行重排序优化
硬件层的内存屏障:Intel硬件提供了一系列的内存屏障,主要有:
- lfence,是一种Load Barrier 读屏障
- .sfence, 是一种Store Barrier 写屏障
- mfence, 是一种全能型的屏障,具备ifence和sfence的能力
- Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对 CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。
三、synchronized原理详解
synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。
synchronized底层原理 synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码 块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。 当然,JVM内置锁在1.5 之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、 偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与 Lock持平。
synchronized关键字被编译成字节码后会被翻译成monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置 与结束位置。
锁的膨胀升级过程
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。从JDK 1.6 中默认是开启偏向锁和轻量级锁的。
偏向锁: 如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时, 无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争 的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。 轻量级锁: 倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时 Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”。轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场 合,就会导致轻量级锁膨胀为重量级锁。 自旋锁: 轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情 况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要 从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。 消除锁: 消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时 进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以 节省毫无意义的请求锁时间
四、AQS
Java并发编程核心在于java.concurrent.util包而juc当中的大多数同步器实现都是围绕 着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象 就是基于AbstractQueuedSynchronizer简称AQS,AQS定义了一套多线程访问共享资源 的同步器框架,是一个依赖状态(state)的同步器。 ReentrantLock ReentrantLock是一种基于AQS框架的应用实现,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。而且它具有比 synchronized更多的特性,比如它支持手动加锁与解锁,支持加锁的公平性。 在ReentrantLock内部定义了一个Sync的内部类,该类继承AbstractQueuedSynchronized,对 该抽象类的部分方法做了实现;并且还定义了两个子类: 1、FairSync 公平锁的实现 2、NonfairSync 非公平锁的实现
AQS具备特性
- 阻塞等待队列
- 共享/独占
- 公平/非公平
- 可重入
- 允许中断
除了Lock外,Java.concurrent.util当中同步器的实现如Latch,Barrier,BlockingQueue等, 都是基于AQS框架实现。 AQS内部维护属性volatile int state (32位)。
state表示资源的可用状态,State三种访问方式 getState()、setState()、compareAndSetState()。
AQS定义两种资源共享方式: Exclusive-独占,只有一个线程能执行,如ReentrantLock。 Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch。
AQS定义两种队列: 同步等待队列
条件等待队列
五、BlockingQueue
BlockingQueue,是java.util.concurrent 包提供的用于解决并发生产者 - 消费者问题 的最有用的类,它的特性是在任意时刻只有一个线程可以进行take或者put操作,并且 BlockingQueue提供了超时return null的机制,在许多生产场景里都可以看到这个工具的 身影。
常见的4种阻塞队列:
-
ArrayBlockingQueue 由数组支持的有界队列 队列基于数组实现,容量大小在创建ArrayBlockingQueue对象时已定义好 数据结构如下图: 应用场景:在线程池中有比较多的应用,生产者消费者场景 工作原理:基于ReentrantLock保证线程安全,根据Condition实现队列满时的阻塞 -
LinkedBlockingQueue 由链接节点支持的可选有界队列 是一个基于链表的无界队列(理论上有界)。 向无限队列添加元素的所有操作都将永远不会阻塞,[注意这里不是说不会加锁保证线程安 全],因此它可以增长到非常大的容量。 -
PriorityBlockingQueue 由优先级堆支持的无界优先级队列 -
DelayQueue 由优先级堆支持的、基于时间的调度队列 由优先级堆支持的、基于时间的调度队列,内部基于无界队列PriorityQueue实现,而无界 队列基于数组的扩容实现。 要求:入队的对象必须要实现Delayed接口,而Delayed集成自Comparable接口 应用场景:电影票 工作原理: 队列内部会根据时间优先级进行排序。延迟类线程池周期执行。
BlockingQueue API:
六、 Semaphore
Semaphore 字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目,底层依赖AQS的状态State,是在生产当中比较常用的一个工具类。
重要方法:acquire() 表示阻塞并获取许可 。release() 表示释放许可。 需求场景:资源访问,服务限流(Hystrix里限流就有基于信号量方式)。
七、CountDownLatch
CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。例 如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。 使用场景:Zookeeper分布式锁,Jmeter模拟高并发等。 CountDownLatch如何工作? CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当 一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的 线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。 API: CountDownLatch.countDown() CountDownLatch.await();
八、CyclicBarrier
栅栏屏障,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程 到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。 CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线 程数量,每个线程调用await方法告CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。 应用场景:可以用于多线程计算数据,最后合并计算结果的场景。
九、Atomic
在Atomic包里一共有12个类,四种原子更新方式,分别是 原子更新基本类型、原子更新数组、原子更新引用和原子更新字段。 Atomic包里的类基本都是使用Unsafe实现的包装类。
基本类:AtomicInteger、AtomicLong、AtomicBoolean; 引用类型:AtomicReference、AtomicReference的ABA实例、AtomicStampedRerence、AtomicMarkableReference; 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray 属性原子修改器(Updater):AtomicIntegerFieldUpdater、 AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
十、Unsafe
Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法。
但由于Unsafe类使Java语言拥有了 类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。 在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语 言变得不再“安全”,因此对Unsafe的使用一定要慎重。
功能介绍:
十一、HashMap
数据结构:数组+链表+(红黑树jdk>=8)
HashMap是线程不安全的,不安全的具体原因就是在高并发场景下,扩容可能产生死锁 (Jdk1.7存在)以及get操作可能带来的数据丢失。(多线程扩容导致形成的链表环)
Jdk8-扩容 对源码进行了优化,采用高低位拆分转移方 式,避免了链表环的产生。 扩容前: 扩容后:
十二、ConcurrentHashMap
ConcurrentHashMap的数据结构与HashMap基本类似,区别在于: 1、内部在数据 写入时加了同步机制(分段锁)保证线程安全,读操作是无锁操作; 2、扩容时老数据的转移 是并发执行的,这样扩容的效率更高。
并发安全控制 Java7 ConcurrentHashMap基于ReentrantLock实现分段锁: Java8中 ConcurrentHashMap基于分段锁+CAS保证线程安全,分段锁基于synchronized 关键字实现;
十三、线程池概念
作用:重用存在的线程,减少线程创建,消亡的开销,提高性能。提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资 源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程的实现方式 Runnable,Thread,Callable
Executor Executor接口是线程池框架中最基础的部分,定义了一个用于执行Runnable的execute方法。 Executor下有一个重要子接口ExecutorService,其中定义了线程池的具体行为:
- execute(Runnable command):履行Ruannable类型的任务。
- submit(task):可用来提交Callable或Runnable任务,并返回代表此任务的Future 对象。
- shutdown():在完成已提交的任务后封闭办事,不再接管新任务。
- shutdownNow():停止所有正在履行的任务并封闭办事。
- isTerminated():测试是否所有任务都履行完毕了。
- isShutdown():测试是否该ExecutorService已被关闭。
十四、线程池—ThreadPoolExecutor
线程池的创建:
线程池监控: 线程池原理:
十五、定时线程池—ScheduledThreadPoolExecutor
它用来处理延时任务或定时任务:
十六、什么是 Fork/Join 框架?
Fork/Join 框架是 Java7 提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后 得到大任务结果的框架。 Fork 就是把一个大任务切分为若干子任务并行的执行,Join 就是合并这些子任务的执行结果,最后得到这个大任务的结果。
Fork/Jion特性:
- ForkJoinPool 不是为了替代 ExecutorService,而是它的补充,在某些应用场景下性能比 ExecutorService 更好。(见 Java Tip: When to use ForkJoinPool vs ExecutorService )
- ForkJoinPool 主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数,例如 quick sort 等。
- ForkJoinPool 最适合的是计算密集型的任务,如果存在 I/O,线程间同步,sleep() 等会造成线程长时间阻塞的情况时,最好配合使用 ManagedBlocker。
执行流程:
十七、Future
Future是java 1.5引入的一个interface,可以方便的用于异步结果的获取。
Future代表的是异步执行的结果,意思是当异步执行结束之后,返回的结果将会保存在Future中。
那么我们什么时候会用到Future呢? 一般来说,当我们执行一个长时间运行的任务时,使用Future就可以让我们暂时去处理其他的任务,等长任务执行完毕再返回其结果。
经常会使用到Future的场景有:
- 计算密集场景。
- 处理大数据量。
- 远程方法调用等。
十八、Disruptor
Disruptor是一个开源框架(仅做了解即可),研发的初衷是为了解决高并发下列队锁的问题,最早由 LMAX(一种新型零售金融交易平台)提出并使用。 框架最经典也是最多的应用场景:生产消费。
核心设计原理: Disruptor通过以下设计来解决队列速度慢的问题:
- 环形数组结构: 为了避免垃圾回收,采用数组而非链表。同时,数组对处理器的缓存机制更加友好。
- 元素位置定位: 数组长度2^n,通过位运算,加快定位的速度。下标采取递增的形式。不用担心index溢出的问 题。index是long类型,即使100万QPS的处理速度,也需要30万年才能用完。
- 无锁设计: 每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位 置写入或者读取数据。
|