多线程总结
多线程面试题总结
Java并发
volatile 关键字
保证可见性
volatile关键字可以保证变量的可见性,如果我们将变量声明为volatile,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。禁指令重排序
如果我们将变量声明为
volatile,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。此外,如下方法也可以实现内存屏障,但会麻烦一些。1
2
3public native void loadFence();
public native void storeFence();
public native void fullFence();不保证原子性
可以通过锁或者Atomic类保证原子性
synchronized
1、修饰实例方法 (锁当前对象实例)
2、修饰静态方法 (锁当前类)
3、修饰代码块 (锁指定对象/类)
对括号里指定的对象/类加锁:
synchronized(object)表示进入同步代码库前要获得 给定对象的锁。synchronized(类.class)表示进入同步代码前要获得 给定 Class 的锁
synchronized 底层原理
synchronized 关键字底层原理属于 JVM 层面。
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。


多线程下 synchronized 的加锁就是对同一个对象的对象头中的 MarkWord 中的变量进行CAS操作。
锁可以升级,但不能降级。即:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的。
==//TODO 锁升级过程待完善==
ThreadLocal
ThreadLocal作用:对数据进行线程之间的隔离,每个线程都会有这个变量的本地副本。
内存泄漏问题
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法
ThreadPoolExecutor
ThreadPoolExecutor构造函数重要参数分析
ThreadPoolExecutor 3 个最重要的参数:
corePoolSize: 核心线程数定义了最小可以同时运行的线程数量。maximumPoolSize: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor其他常见参数:
keepAliveTime:当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被回收销毁;unit:keepAliveTime参数的时间单位。threadFactory:executor 创建新线程的时候会用到。handler:饱和策略。关于饱和策略下面单独介绍一下。
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:
ThreadPoolExecutor.AbortPolicy: 抛出RejectedExecutionException来拒绝新任务的处理。ThreadPoolExecutor.CallerRunsPolicy: 调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。建议使用这个策略ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

Atomic 原子类
AtomicInteger常用方法,其他类型类似
1 | |
原子类的实现原理
原子性操作的实现是基于CAS,CAS底层依赖于Unsafe和volatile的能力
CAS存在的问题
CAS存在ABA问题,Java 提供了 AtomicStampedReference 工具类,通过为引用建立类似版本号(stamp)的方式,来保证 CAS 的正确性。
除了ABA问题CAS还要考虑限制自旋的次数,以免过度消耗 CPU
AQS
AQS原理
AQS 的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。

AQS 定义两种资源共享方式
Exclusive
(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享):多个线程可同时执行,如
CountDownLatch、Semaphore、CyclicBarrier、ReadWriteLock我们都会在后面讲到。
自定义同步器
- 使用者继承
AbstractQueuedSynchronizer并重写指定的方法。 - 将 AQS 组合在自定义同步组件的实现中,并重写如下模板方法。
1 | |
ReentrantLock流程
以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock() 时,会调用 tryAcquire() 独占该锁并将 state+1 。此后,其他线程再 tryAcquire() 时就会失败,直到 A 线程 unlock() 到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。
CountDownLatch流程
再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown() 一次,state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 state=0 ),会 unpark() 主调用线程,然后主调用线程就会从 await() 函数返回,继续后余动作。不能循环使用
CyclicBarrier
CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。可以通过reset方法循环使用
JMM(Java 内存模型)详解
CPU缓存模型

JMM内存模型
JMM 说白了就是定义了一些规范来解决这些问题,开发者可以利用这些规范更方便地开发多线程程序。主要目的是为了简化多线程编程,增强程序可移植性的。
