|
|
|
# 相关概念
|
|
|
|
|
|
|
|
## Java内存模型(JMM)
|
|
|
|
|
|
|
|
Java 内存模型定义了程序中各种变量的访问规则。其规定所有变量都存储在主内存,线程均有自己的工作内存。工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。操作完成后,线程的工作内存通过缓存一致性协议将操作完的数据刷回主存。
|
|
|
|
|
|
|
|
## as-if-serial
|
|
|
|
|
|
|
|
编译器等会对原始的程序进行指令重排序和优化。但不管怎么重排序,其结果和用户原始程序输出预定结果一致。
|
|
|
|
|
|
|
|
## happens-before八大原则
|
|
|
|
|
|
|
|
- 程序次序规则:一个线程内写在前面的操作先行发生于后面的。
|
|
|
|
- 锁定规则:unlock 操作先行发生于后面对同一个锁的 lock 操作。
|
|
|
|
- volatile 规则:对 volatile 变量的写操作先行发生于后面的读操作。
|
|
|
|
- 线程启动规则:线程的 start 方法先行发生于线程的每个动作。
|
|
|
|
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
|
|
|
|
- 线程终止规则:线程中所有操作先行发生于对线程的终止检测。
|
|
|
|
- 对象终结规则:对象的初始化先行发生于 finalize 方法。
|
|
|
|
- 传递性规则:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C
|
|
|
|
|
|
|
|
## as-if-serial 和 happens-before 的区别
|
|
|
|
|
|
|
|
as-if-serial 保证单线程程序的执行结果不变,happens-before 保证正确同步的多线程程序的执行结果不变。
|
|
|
|
|
|
|
|
## 原子性操作
|
|
|
|
|
|
|
|
一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,这就是原子性操作。
|
|
|
|
|
|
|
|
## 线程的可见性
|
|
|
|
|
|
|
|
可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。volatile, synchronized, final关键字都能保证可见性。
|
|
|
|
|
|
|
|
## 有序性
|
|
|
|
|
|
|
|
即虽然多线程存在并发和指令优化等操作,在本线程内观察该线程的所有执行操作是有序的。
|
|
|
|
|
|
|
|
## Java中volatile关键字作用
|
|
|
|
|
|
|
|
- 保证变量对所有线程的可见性。当一条线程修改了变量值,新值对于其他线程来说是立即可以得知的
|
|
|
|
- 禁止指令重排序优化。使用 volatile 变量进行写操作,汇编指令带有 lock 前缀,相当于一个内存屏障,编译器不会将后面的指令重排到内存屏障之前
|
|
|
|
|
|
|
|
## Java线程的实现方式
|
|
|
|
|
|
|
|
- 实现Runnable接口
|
|
|
|
- 继承Thread类、
|
|
|
|
- 实现Callable接口
|
|
|
|
|
|
|
|
## Java线程的状态
|
|
|
|
|
|
|
|
- NEW:新建状态,线程被创建且未启动,此时还未调用 start 方法。
|
|
|
|
- RUNNABLE: 运行状态。其表示线程正在JVM中执行,但是这个执行,不一定真的在跑,也可能在排队等CPU。
|
|
|
|
- BLOCKED:阻塞状态。线程等待获取锁,锁还没获得。
|
|
|
|
- WAITING: 等待状态。线程内 run 方法运行完语句 Object.wait()或Thread.join() 后进入该状态。
|
|
|
|
- TIMED_WAITING:限期等待。在一定时间之后跳出状态。调用 Thread.sleep(long) Object.wait(long) Thread.join(long) 进入状态。其中这些参数代表等待的时间。
|
|
|
|
- TERMINATED:结束状态。线程调用完run方法进入该状态。
|
|
|
|
|
|
|
|
## 线程通信的方式
|
|
|
|
|
|
|
|
- volatile 关键字修饰变量,保证所有线程对变量访问的可见性。
|
|
|
|
- synchronized关键字。确保多个线程在同一时刻只能有一个处于方法或同步块中。
|
|
|
|
- wait/notify方法。
|
|
|
|
- IO 通信。java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信。
|
|
|
|
|
|
|
|
## 线程池
|
|
|
|
|
|
|
|
没有线程池的情况下,多次创建、销毁线程开销会比较大。如果在开辟线程池的情况下,线程执行完当前任务后执行接下来任务,复用已创建的线程,降低开销、控制最大并发数。
|
|
|
|
|
|
|
|
线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后还会循环获取工作队列中的任务来执行。
|
|
|
|
|
|
|
|
将任务派发给线程池时,会出现以下几种情况
|
|
|
|
|
|
|
|
- 核心线程池未满,创建一个新的线程执行任务。
|
|
|
|
- 如果核心线程池已满,工作队列未满,将线程存储在工作队列。
|
|
|
|
- 如果工作队列已满,线程数小于最大线程数就创建一个新线程处理任务。
|
|
|
|
- 如果超过大小线程数,按照拒绝策略来处理任务。
|
|
|
|
|
|
|
|
## 线程池参数
|
|
|
|
|
|
|
|
- corePoolSize:常驻核心线程数。超过该值后如果线程空闲会被销毁。
|
|
|
|
- maximumPoolSize:线程池能够容纳同时执行的线程最大数。
|
|
|
|
- keepAliveTime:线程空闲时间,线程空闲时间达到该值后会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存资源。
|
|
|
|
- workQueue:工作队列。
|
|
|
|
- threadFactory:线程工厂,用来生产一组相同任务的线程。
|
|
|
|
- handler:拒绝策略。有以下几种拒绝策略:
|
|
|
|
- AbortPolicy:丢弃任务并抛出异常
|
|
|
|
- CallerRunsPolicy:重新尝试提交该任务
|
|
|
|
- DiscardOldestPolicy 抛弃队列里等待最久的任务并把当前任务加入队列
|
|
|
|
- DiscardPolicy 表示直接抛弃当前任务但不抛出异常
|
|
|
|
|
|
|
|
## 线程池创建方法
|
|
|
|
|
|
|
|
- newFixedThreadPool,创建固定大小的线程池。
|
|
|
|
- newSingleThreadExecutor,使用单线程线程池。
|
|
|
|
- newCachedThreadPool,maximumPoolSize 设置为 Integer 最大值,工作完成后会回收工作线程
|
|
|
|
- newScheduledThreadPool:支持定期及周期性任务执行,不回收工作线程。
|
|
|
|
- newWorkStealingPool:一个拥有多个任务队列的线程池。
|
|
|
|
|
|
|
|
## 阻塞队列
|
|
|
|
|
|
|
|
阻塞队列是生产者消费者模式的实现具体组件之一。当阻塞队列为空时,从队列中获取元素的操作将会被阻塞,当阻塞队列满了,往队列添加元素的操作将会被阻塞。具体实现有:
|
|
|
|
|
|
|
|
- ArrayBlockingQueue:底层是由数组组成的有界阻塞队列。
|
|
|
|
- LinkedBlockingQueue:底层是由链表组成的有界阻塞队列。
|
|
|
|
- PriorityBlockingQueue:阻塞优先队列。
|
|
|
|
- DelayQueue:创建元素时可以指定多久才能从队列中获取当前元素
|
|
|
|
- SynchronousQueue:不存储元素的阻塞队列,每一个存储必须等待一个取出操作
|
|
|
|
- LinkedTransferQueue:与LinkedBlockingQueue相比多一个transfer方法,即如果当前有消费者正等待接收元素,可以把生产者传入的元素立刻传输给消费者。
|
|
|
|
- LinkedBlockingDeque:双向阻塞队列。
|
|
|
|
|
|
|
|
## ThreadLocal
|
|
|
|
|
|
|
|
ThreadLocal 是线程共享变量。ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是 ThreadLocal 对象,值是 Entry 对象,ThreadLocalMap是每个线程私有的。
|
|
|
|
|
|
|
|
- set 给ThreadLocalMap设置值。
|
|
|
|
- get 获取ThreadLocalMap。
|
|
|
|
- remove 删除ThreadLocalMap类型的对象。
|
|
|
|
|
|
|
|
存在的问题
|
|
|
|
|
|
|
|
1. 对于线程池,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用,造成一系列问题。
|
|
|
|
2. 内存泄漏。由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放,产生内存泄漏。
|
|
|
|
|
|
|
|
## 聊聊你对Java并发包下unsafe类的理解
|
|
|
|
|
|
|
|
对于 Java 语言,没有直接的指针组件,一般也不能使用偏移量对某块内存进行操作。这些操作相对来讲是安全(safe)的。
|
|
|
|
|
|
|
|
Java 有个类叫 `Unsafe` 类,这个类使 Java 拥有了像 C 语言的指针一样操作内存空间的能力,同时也带来了指针的问题。这个类可以说是 Java 并发开发的基础。
|
|
|
|
|
|
|
|
## Java中的乐观锁与CAS算法
|
|
|
|
|
|
|
|
对于乐观锁,开发者认为数据发送时发生并发冲突的概率不大,所以读操作前不上锁,到了写操作时才会进行判断。数据在此期间如果被其他线程修改,那就返回写入失败;如果没有被修改,那就执行修改操作,返回修改成功。
|
|
|
|
|
|
|
|
乐观锁一般都采用 Compare And Swap(CAS)算法进行实现。顾名思义,该算法涉及到了两个操作,比较(Compare)和交换(Swap)。
|
|
|
|
|
|
|
|
CAS 算法的思路如下:
|
|
|
|
|
|
|
|
1. 该算法认为不同线程对变量的操作时产生竞争的情况比较少。
|
|
|
|
2. 该算法的核心是对当前读取变量值 E 和内存中的变量旧值 V 进行比较。
|
|
|
|
3. 如果相等,就代表其他线程没有对该变量进行修改,就将变量值更新为新值 N。
|
|
|
|
4. 如果不等,就认为在读取值 E 到比较阶段,有其他线程对变量进行过修改,不进行任何操作。
|
|
|
|
|
|
|
|
## ABA问题及解决方法简述
|
|
|
|
|
|
|
|
CAS 算法是基于值来做比较的,如果当前有两个线程,一个线程将变量值从 A 改为 B ,再由 B 改回为 A ,当前线程开始执行 CAS 算法时,就很容易认为值没有变化,误认为读取数据到执行 CAS 算法的期间,没有线程修改过数据。
|
|
|
|
|
|
|
|
juc 包提供了一个 AtomicStampedReference,即在原始的版本下加入版本号戳,解决 ABA 问题。
|
|
|
|
|
|
|
|
## 常见的Atomic类
|
|
|
|
|
|
|
|
在很多时候,我们需要的仅仅是一个简单的、高效的、线程安全的++或者--方案,使用synchronized关键字和lock固然可以实现,但代价比较大,此时用原子类更加方便。基本数据类型的原子类有:
|
|
|
|
|
|
|
|
- AtomicInteger 原子更新整形
|
|
|
|
- AtomicLong 原子更新长整型
|
|
|
|
- AtomicBoolean 原子更新布尔类型
|
|
|
|
|
|
|
|
Atomic数组类型有:
|
|
|
|
|
|
|
|
- AtomicIntegerArray 原子更新整形数组里的元素
|
|
|
|
- AtomicLongArray 原子更新长整型数组里的元素
|
|
|
|
- AtomicReferenceArray 原子更新引用类型数组里的元素。
|
|
|
|
|
|
|
|
Atomic引用类型有
|
|
|
|
|
|
|
|
- AtomicReference 原子更新引用类型
|
|
|
|
- AtomicMarkableReference 原子更新带有标记位的引用类型,可以绑定一个 boolean 标记
|
|
|
|
- AtomicStampedReference 原子更新带有版本号的引用类型
|
|
|
|
|
|
|
|
FieldUpdater类型:
|
|
|
|
|
|
|
|
- AtomicIntegerFieldUpdater 原子更新整形字段的更新器
|
|
|
|
- AtomicLongFieldUpdater 原子更新长整形字段的更新器
|
|
|
|
- AtomicReferenceFieldUpdater 原子更新引用类型字段的更新器
|
|
|
|
|
|
|
|
## Atomic类基本实现原理
|
|
|
|
|
|
|
|
以AtomicIntger 为例:方法getAndIncrement:以原子方式将当前的值加1,具体实现为:
|
|
|
|
|
|
|
|
- 在 for 死循环中取得 AtomicInteger 里存储的数值
|
|
|
|
- 对 AtomicInteger 当前的值加 1
|
|
|
|
- 调用 compareAndSet 方法进行原子更新
|
|
|
|
- 先检查当前数值是否等于 expect
|
|
|
|
- 如果等于则说明当前值没有被其他线程修改,则将值更新为 next,
|
|
|
|
- 如果不是会更新失败返回 false,程序会进入 for 循环重新进行 compareAndSet 操作。
|
|
|
|
|
|
|
|
## CountDownLatch
|
|
|
|
|
|
|
|
countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,调用countDown方法,计数器的值就减1,当计数器的值为0时,表示所有线程都执行完毕,然后在等待的线程就可以恢复工作了。只能一次性使用,不能 reset。
|
|
|
|
|
|
|
|
## CyclicBarrier
|
|
|
|
|
|
|
|
从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障。
|
|
|
|
|
|
|
|
作用就是会让所有线程都等待完成后才会继续下一步行动
|
|
|
|
|
|
|
|
## Semaphore
|
|
|
|
|
|
|
|
Semaphore 即信号量。Semaphore 的构造方法参数接收一个 int 值,设置一个计数器,表示可用的许可数量即最大并发数。使用 acquire 方法获得一个许可证,计数器减一,使用 release 方法归还许可,计数器加一。如果此时计数器值为0,线程进入休眠。
|
|
|
|
|
|
|
|
## Exchanger
|
|
|
|
|
|
|
|
Exchanger 类可用于两个线程之间交换信息。可简单地将 Exchanger 对象理解为一个包含两个格子的容器,通过 exchanger 方法可以向两个格子中填充信息。线程通过 exchange 方法交换数据,第一个线程执行 exchange 方法后会阻塞等待第二个线程执行该方法。当两个线程都到达同步点时这两个线程就可以交换数据当两个格子中的均被填充时,该对象会自动将两个格子的信息交换,然后返回给线程,从而实现两个线程的信息交换。
|
|
|
|
|
|
|
|
## ConcurrentHashMap
|
|
|
|
|
|
|
|
和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。
|
|
|
|
|
|
|
|
ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment
|
|
|
|
|
|
|
|
JDK8 的改进
|
|
|
|
|
|
|
|
- 取消分段锁机制,采用 CAS 算法进行值的设置,如果 CAS 失败再使用 synchronized 加锁添加元素
|
|
|
|
- 引入红黑树结构,当某个槽内的元素个数超过8且 Node 数组 容量大于 64 时,链表转为红黑树。
|
|
|
|
- 使用了更加优化的方式统计集合内的元素数量。
|