1. 多线程
线程和进程的生命周期都是五个阶段①创建②运行③就绪④阻塞⑤终止
1.1. 进程
每个进程都有独立的运行空间, 进程间的切换有较大开销, 进程中有多个线程, 进程是资源分配的最小操作单元
一个进程对应一个jvm,一个CPU执行权,一个进程执行可同时执行多个线程, 多线程执行时,本质是每个多线程抢占CPU执行权,
1.2. 线程
同一线程共享代码和线程以内的数据空间,每个线程独享一个运行栈和程序计数器, 线程是CPU调用的最小单位.
2. 锁
锁是多线程的情况下, 未防止出现不改出现的线程并发而存在的
锁即是正在执行的一个线程占据一个内存执行被锁的代码块, 防止其他线程并行执行和这个代码块不被允许同时执行的代码块
2.1. 可重入锁
可重入锁实例: Synchronize, ReentrantLock
可重入锁的代码块中, 调用持有同一个锁对象的方法时, 可以继续执行该方法
public class Test01 {
public static void main(String[] args) {
Test01 test01 = new Test01();
synchronized (test01){test01.lock01();}
}
public synchronized void lock01(){
System.out.println("lock01()");
this.lock02();
}
public synchronized void lock02(){
System.out.println("lock02()");
}
}
// main(),lock01(),lock02()的锁对象都是test01这个实例,因为Synchronize是可重入锁,所以,lock01(),lock02()两个方法都能执行
2.2. 独占锁
当一个线程占有锁对象时, 其他线程不能同时占有该锁对象
一个锁对象本质占用一个指定内存, 一个锁的执行代码块占用该内存, 其他同一个锁对象的代码块需要等待该内存被释放掉才能占用该内存
2.3. 共享锁
多个线程可以共享这把锁
共享锁是和独占锁一起以读写锁的形式出现的, 当一个线程对对一个锁对象使用读锁时, 其他线程可以对这个锁对象再次使用读锁, 但是不能对这个锁对象使用写锁
2.4. 读写锁
读写锁分为读锁和写锁
读锁是共享锁, 写锁是独有锁
一个代码块持有读锁的时候, 另一个代码块允许继续持有读锁, 即允许读读并发
2.5. 悲观锁和乐观锁
悲观锁: 在访问之前会获取锁, 并确保其他线程无法访问资源, 独占锁, 读写锁都是悲观锁
乐观锁: 在更新(修改, 添加)资源之前, 会先获取一个版本号或者时间戳, 在修改的时候会判断这个时间戳是否被修改, 如果没有则说明没有线程同步修改这个资源
2.6. 自旋锁: 多个线程访问锁资源, 一个线程获取到了锁, 可以让另一个线程不放弃cpu, 不间断尝试获取锁, 知道另一个线程放开锁, 这个线程就可以获取锁, 自旋锁适用于锁保持时间较短的情况
3. Synchronize(可重入锁, 独占锁)
可重入锁, 锁代码块中持有同一个锁对象的方法或者代码块能正常执行
独占锁, 独占一个锁对象, 释放前其他的同一个锁对象的代码块无法执行
不可响应中断, 一个线程获取不到锁就会一直等待, 直到获取到锁
自动加锁和释放锁, 锁的区域在锁修饰的代码块
4. lock
lock锁需要手动加入锁和释放锁, 是父类锁, 提供lock()和unlock()方法, 加锁和释放锁
lock锁的特性:
可重入;
支持公平和非公平策略: 每个线程按照申请锁的顺序队列获取锁叫公平锁
5. ReentrantLocal(可重入锁,独占锁 )
可重入锁, 锁代码块中持有同一个锁对象的方法或者代码块能正常执行
独占锁, 独占一个锁对象, 释放前其他的同一个锁对象的代码块无法执行
可响应中断, 一个线程获取不到锁, 不会一直等待
6. ReentrantReadWriteLock(可重入锁, 读写锁)
- 可重入
- 支持公平和非公平策略
- 支持锁降级不支持锁升级, 加入写锁 -> 加入读锁 -> 释放写锁 -> 释放读锁 叫做锁降级,(一定要先释放写锁)
读写锁: 持有同一个锁对象的代码块, 允许同时进行读操作
分为读锁和写锁, 读锁和写锁都需要手动上锁
class MyCache{
private volatile Map<String, String> cache= new HashMap<>();
// 加入读写锁
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public void put(String key, String value){
// 加写锁
rwl.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 开始写入!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放写锁
rwl.writeLock().unlock();
}
}
public void get(String key){
// 加入读锁
rwl.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 开始读出!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放读锁
rwl.readLock().unlock();
}
}
}
7. 线程通信
线程通信条件: ①多线程; ②共用了一把锁; ③通信(wait/notify);
lock.lock();
//线程等待(会释放掉锁, 待到线程被唤醒会重新获取到锁)
lock.wait();
//唤醒该锁对象的其他等待线程
lock.notifyAll();
lock.
7.1. 虚假唤醒
if (i >= 0) {
lock.wait();
}
//虚假唤醒:由于多线程执行, i>=0时会等待, 但是当线程再次抢到锁,并不能保证抢到锁之后仍旧符合i>=0
//解决方案:
while (i >= 0) {
lock.wait();
}
7.2. Condition(精确制导)
精确通信, 可以分类等待, 然后根据等待分的类来选择唤醒
ReentrantLock lock = new ReentrantLock();
Condition condition01 = lock.newCondition();
Condition condition02 = lock.newCondition();
//上锁
lock.lock();
//指定现在等待所属类型为condition01
condition01.await();
//被指定等待类型为condition02的被唤醒
condition02.signal();
lock.unlock();
8. 多线程容器安全
8.1. List
ArrayList的写操作没有上锁, 所以是线程不安全的
可以使用Vector和SynchronizeList, 这两个类的add()方法都上了synchronize锁
Vector和Synchronized的缺点:
vector:内存消耗比较大,适合一次增量比较大的情况
SynchronizedList:迭代器涉及的代码没有加上线程同步代码
8.2. CopyOnWrite容器
写时复制: 写时复制是向容器中添加时, 不直接添加, 而是将当前容器先进行copy, 然后再之前的容器做为读容器, 复制后的容器作为写容器, 写完成后将容器的地址引用更改为后面的容器, 删掉之前的读容器,
CopyOnWriteArrayList和CopyOnWriteSet为写时复制
-
写时复制能解决读写并发问题
-
适用于一次添加海量数据的并发容器
-
占用内存较大
-
存在数据一致性问题, 只能保证数据的最终一致性, 不能保证实时一致性
8.3. Collections.synchronizedList()
Collections工具类提供synchronizedList(), synchronizedSet(),synchronizedMap()方法, 为集合的增删改查操作提供了锁
9. 多线程
多线程实现方式:
①继承Thread的实现类
②实现Runnable接口
③实现Callable接口
线程启动底层都是使用Thread.start()
Callable和Runnable
使用Callable创建线程能够获取线程的异常和返回值,
Runnable接口的run()没有抛出异常, 所以重写的run()方法只能内部处理异常, Runnable没有返回值
使用Callable创建线程实际上是通过Runnable的子类FutureTask来创建线程
10. 线程池
作用:
①复用线程
②管理线程, 可以通过任务队列来管理线程执行任务
③控制线程数量来控制并发数量, 线程不是越多越好, 最好和cpu数量适配
线程池核心七个参数:
-
corePoolSize: 核心线程数;
-
maximumPoolSize: 最大线程数;
-
keepAliveTime: 多余线程(除核心线程的线程)存活时间
-
unit:keepAliveTime的单位
-
workQueue: 任务队列;
-
threadFactory: 线程工厂;
-
handler: 拒绝策略, 当队列满了, 而且所有线程都在执行任务, 还有任务访问线程池, 会对该任务执行拒绝策略
ThreadPoolExecutor自带的拒绝策略如下:
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中 尝试再次提交当前任务。
- DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。 如果允许任务丢失,这是最好的一种策略。
以上内置的策略均实现了RejectedExecutionHandler接口,也可以自己扩展RejectedExecutionHandler接口,定义自己的拒绝策略
线程池执行流程:
11. 多线程高并发底层原理
由于cpu的运行程序速度远大于主存储的速度,所以会在主存RAM和CPU之间加多级高速缓存,缓存的速度接近cpu的运行速度,这样会大大提高计算机的运行速度。
11.1. java内存模型(JMM)
java为了屏蔽系统硬件的内存差异, jdk1.5发布了JMM
jvm视角解析: 主内存存储共享变量(作为对象, 存放在堆中), 变量的副本计较在堆或者栈没有意义, 因为是存放在高速缓存中(应该是在堆中的线程独有区域)
硬件视角解析: 主内存对应的是硬件的物理内存, 本地内存对应的是寄存器和高速缓存
内存模型的三大特性:
原子性: 同时成功或者同时失败, 代码之间不可分割
可见性: 两个线程之间不可见;
- 每个线程都有自己的工作内存, 当一个线程修改了共享变量的数据时, 其他线程读取该共享数据还是会从本地内存中读取副本的信息, 感知不到主内存中的对象被修改
- 在 Java 中 volatile、synchronized 和 final 实现可见性。
有序性: 单线程是有序的(方法按顺序执行), 多线程不是有序的,
- 使用volatile和synchronized可以保证多线程的有序性
volatile能禁用指令重排, 能有效的消除一个线程指令的排序变化对另一个线程的影响
volatile主要原理是确保变量的变化通知到其他线程
volatile原理:
如何把修改后的值刷新到主内存中的?
现代的处理器使用写缓冲区临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,较少对内存总线的占用。但是什么时候写入到内存是不知道的。
所以就引入了volatile,volatile是如何保证可见性的呢?
在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,会多出lock addl。Lock前缀的指令在多核处理器下会引发两件事情:
将当前处理器缓存行的数据写回到系统内存。
-
这个写回内存的操作会使其他cpu里缓存了该内存地址的数据无效。
-
如果声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的还是旧的,在执行操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
原文链接:https://blog.csdn.net/xinghui_liu/article/details/124379221
总结,
写操作会存放在写缓存区, 写缓存区会在线程执行期间将同一地址的数据一次性合并写入
volatile实现原理即是通过嗅探自己的缓存是否过期, 过期了则在修改的时候会将缓存进行更新到最新版本然后更新
11.2. CAS
CAS是解决多线程并发安全问题的一种乐观锁算法。
CAS:原子,操作物理内存。
Unsafe类是CAS的核心类,提供硬件级别的原子操作
开销大:在并发量比较高的情况下,如果反复尝试更新某个变量,却又一直更新不成功,会给CPU带来较大的压力
ABA问题:当变量从A修改为B再修改回A时,变量值等于期望值A,但是无法判断是否修改,CAS操作在ABA修改后依然成功。
不能保证代码块的原子性:CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。
CAS算法涉及到三个操作数:
需要读写的内存值 V。
进行比较的值 A。
要写入的新值 B。
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作(自旋)。CAS 的意思是:“我认为V的值应该是A,如果是,那么将其赋值为B,若不是,则不修改,并告诉我应该为多少”。CAS是项乐观技术—它抱着成功的希望进行更新,并且如果另一个线程在上次检查后更新了该变量,它能够发现错误。
原文链接:https://blog.csdn.net/weixin_61543601/article/details/125122691
CAS可将V, A, B优化为时间戳, 可解决ABA问题, (ABA: 数值从A改成了B,B是任意值, 然后又从B改回了A, CAS仍然会将新值替换掉之前的老值)
volatile不具备原子性要做到绝对安全要使用原子类
CAS提供了原子类, Collections工具类提供了单列集合加锁的方法
11.3. AQS: 抽象队列同步器
- 加锁是同步器在加锁,
- 控制公平锁还是非公平锁也是同步器控制
自定义悲观锁就是在设置一个固定内存(同一个变量, 对象均可)作为锁对象, 加锁就是原子占用或者volatile占用这个锁对象, 然后不允许其他对象占用, 解锁就是释放掉这个变量, 即将这个变量的状态置于未被修改的状态
AQS: 抽象队列同步器: 即是AbstractQueuedSynchronizer抽象类及其子类
- 同步: 加锁, 让异步操作变成同步(多个读锁异步除外)
- 队列: 可以设置获取锁的队列和抢锁的策略(公平/非公平)
Sync是AQS的子类,也是ReentrantLock的内部类, FairSync和NonfairSync是Sync的子类
12. ReentrantLock锁的原理:
14、Lock源码; 基本概念
锁: 共同抢一个位置
CAS:原子,操作物理内存。
抢不到锁处理:
1)、while: 自旋重试; 快,但是浪费CPU
2)、wait: 你可以等待,等别人释放锁了notify下你。慢,但是不浪费CPU。
好的设计:少量线程抢锁可以快速自旋重试,大量线程抢锁应该使用线程通信机制
源码解析:
1、AbstractQueuedSynchronizer:抽象队列同步器 AQS;
1)、sync = new NonfairSync();
2)、sync = new FairSync()
sync.lock(); //加锁实际上是 AQS调用的加锁办法
3)、加锁流程
第一次加锁:
1)、先CAS尝试把 AbstractQueuedSynchronizer 里面的 int state的0改为1
2)、再把当前线程标记保存一下,直到释放锁(目的是为了重入锁)
已经被别人加上锁,我再来加锁;
1)、先CAS尝试把 AbstractQueuedSynchronizer 里面的 int state的0改为1,改不成功
2)、acquire(1);
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
selfInterrupt();
}
1、!tryAcquire(arg):尝试要1次锁;
1、getState如果是0.没人抢锁,继续CAS
2、getState如果是1,说明锁被人占
1)、如果当前线程就是占锁的人。
2)、state变为 1+重复加锁次数
2、上面加锁不成功,就执行这个 acquireQueued; acquireQueued执行成功就执行 selfInterrupt();
1、等锁的线程里 利用 addWaiter 添到等待队列[CLH队列]
(The wait queue is a variant of a "CLH" (Craig, Landin, and Hagersten) lock queue. )。
2、并且把这个线程park掉;
Unsafe.park(线程);
4)、解锁流程
1)、sync.release(1); 解锁成功以后,总是唤醒队头的人
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
看似非公平,实际在唤醒的时候是公平的。随机,伪随机。
抢不到锁的人,存到队列里面了,总是从队头召唤。
公平与非公平的差别?
公平:sync.lock(){
acquire(1); //走公平特性
}
非公平:sync.lock(){
if (compareAndSetState(0, 1)) //上来就抢
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
return true;
}
看似非公平,实际在唤醒的时候是公平的。随机,伪随机。
抢不到锁的人,存到队列里面了,总是从队头召唤。
公平与非公平的差别?
公平:sync.lock(){
acquire(1); //走公平特性
}
非公平:sync.lock(){
if (compareAndSetState(0, 1)) //上来就抢
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}