多线程与高并发
一、Volatile
1.1 可见性
- read(读取):从主内存读取数据
- load(载入):将主内存读取到的数据写入工作内存
- use(使用) :从工作内存读取数据来计算
- assign(赋值):将计算好的值重新赋值到工作内存中
- store(存储):将工作内存数据写入主内存
- write(写入):将store过去的变量值赋值给主内存中的变量
- lock(锁定) :将主内存变量加锁,标识为线程独占状态
- unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
1.2 禁止指令重排
屏障类型
|
指令示例
|
说明
|
LoadLoad
|
Load1;LoadLoad;Load2
|
保证Load1的读取操作在Load2及后续读取操作之前执行
|
StoreStore
|
Store1;StoreStore;Store2
|
在Store2及其后的写操作执行前,保证Store1的写操作已刷新到主内存
|
LoadStore
|
Load1;LoadStore;Store2
|
在Store2及其后的写操作执行前,保证Load1的读操作已读取结束
|
StoreLoad
|
Store1;StoreLoad;Load2
|
保证load1的写操作已刷新到主内存之后,load2及其后的读操作才能执行
|
二、锁
2.1 公平锁与非公平锁
2.2 Synchronized锁升级 无锁、偏向锁、轻量级锁、重量级锁
a>为什么要进行锁升级优化
b>Java对象的内存布局
c>锁升级过程
- 对于某个锁对象,如果自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而允许自旋等待持续相对更长时间
- 对于某个锁对象,如果自旋很少成功获得过锁,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
2.3 读写锁的锁降级和邮戳锁
a>ReentrantReadWriteLock
b>StampedLock
StampedLock有三种访问模式
- Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
- Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
- Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式
2.4 Synchronized与Lock的区别
- 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题
2.5 Synchronized的重入的实现机理
2.6 死锁
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
2.7 CAS
LongAdder为什么这么快?
LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
- 内部有一个base变量,一个Cell[]数组。
- base变量:非竞态条件下,直接累加到该变量上
- Cell[]数组:竞态条件下,累加个各个线程自己的槽Cell[i]中
三、AQS
3.1 CountDownLatch、CyclicBarrier、Semaphore使用场景与区别
3.1.1 CountDownLatch
CountDownLatch门闩基于AQS实现,volatile变量state维持倒数状态,多线程共享变量可见。计数器值递减到0的时候,不能再复原的。
- CountDownLatch通过构造函数初始化传入参数实际为AQS的state变量赋值,维持计数器倒数状态
- 当主线程调用await()方法时,当前线程会被阻塞,当state不为0时进入AQS阻塞队列等待。
- 其他线程调用countDown()时,state值原子性递减,当state值为0的时候,唤醒所有调用await()方法阻塞的线程
3.1.2 CyclicBarrier
CyclicBarrier叫做回环屏障,它的作用是让一组线程全部达到一个状态之后再全部同时执行,而且他有一个特点就是所有线程执行完毕之后是可以重用的。
- 当子线程调用await()方法时,获取独占锁,同时对count递减,进入阻塞队列,然后释放锁
- 当第一个线程被阻塞同时释放锁之后,其他子线程竞争获取锁,操作同1
- 直到最后count为0,执行CyclicBarrier构造函数中的任务,执行完毕之后子线程继续向下执行
3.1.3 Semaphore
Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
3.2 AQS原理
四、线程
4.1 线程与进程
- 定义:进程是系统进行资源分配和调度的独立单位,实现了操作系统的并发;线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发
- 开销方面:进程有自己的独立数据空间,程序之间的切换开销大;线程也有自己的运行栈和程序计数器,线程之间的切换开销较小
- 共享空间:进程拥有各自独立的地址空间、资源,所以共享复杂;线程共享所属进程的资源,所以共享简单
- 进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。
4.2 线程的状态
线程的六种状态:
-
初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
-
运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
-
阻塞(BLOCKED):表示线程阻塞于锁。
-
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
-
超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
-
终止(TERMINATED):表示该线程已经执行完毕。
4.3 线程中断
方法
|
说明
|
public void interrupt()
|
实例方法interrupt()仅仅是设置线程的中断状态为true,不会停止线程
|
public static boolean interrupted()
|
静态方法,Thread.interrupted();
判断线程是否被中断,并清除当前中断状态
这个方法做了两件事:
1 返回当前线程的中断状态
2 将当前线程的中断状态设为false
|
public boolean isInterrupted()
|
实例方法,判断当前线程是否被中断(通过检查中断标志位)
|
4.4 线程池
4.4.1 线程池定义
4.4.2 线程池的优势
- 重用存在的线程,减少创建、消亡的开销,提高性能
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池也可以进行统一的分配、调优与监控
4.4.3 线程池的状态
- Running:能接收新任务以及处理已经添加的任务
- Shutdown:不接受新任务,可以处理已经添加的任务
- Stop:不接受新任务,不处理已经添加的任务,并且中断正在处理的任务
- Tidying:所有的任务已经终止,ctl记录的任务数量为“0”(ctl负责记录线程池的运行状态与活动线程数)
- Terminated:线程池彻底终止,则线程池转化为terminated状态
源码解读详情参考
public class ThreadPoolExecutor extends AbstractExecutorService { // ctl初始化了线程的状态和线程数量,初始状态为RUNNING并且线程数量为0 // 这里一个Integer既包含了状态也包含了数量,其中int类型一共32位,高3位标识状态,低29位标识数量 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // 这里指定了Integer.SIZE - 3,也就是32 - 3 = 29,表示线程数量最大取值长度 private static final int COUNT_BITS = Integer.SIZE - 3; // 这里标识线程池容量,也就是将1向左位移上面的29长度,并且-1代表最大取值,二进制就是 000111..111 private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 这里是高三位的状态表示 private static final int RUNNING = -1 << COUNT_BITS; // 111 private static final int SHUTDOWN = 0 << COUNT_BITS; // 000 private static final int STOP = 1 << COUNT_BITS; // 001 private static final int TIDYING = 2 << COUNT_BITS; // 010 private static final int TERMINATED = 3 << COUNT_BITS; // 011 // 获取当前线程池状态:通过传入的c,获取最高三位的值,拿到线程状态吗,最终就是拿 1110 000......和c做&运算得到高3位结果 private static int runStateOf(int c) { return c & ~CAPACITY; } // 获取当前线程数量,最终得到现在线程数量,就是拿c 和 0001 111......做&运算,得到低29位结果 private static int workerCountOf(int c) { return c & CAPACITY; } private static int ctlOf(int rs, int wc) { return rs | wc; } }