🍑一. JUC包(java.util.concurrent)下的常见类
juc包下的所有类都是提供多线程并发编程用的,不仅满足线程安全而且效率也很高
ReentranLock是可重入锁,具体用法:
Lock lock = new ReentrantLock();
try{
lock.lock(); //锁对象加锁,只有一个线程获取到锁
//需要保证线程安全的代码
}finally{
lock.unlock(); //不管是否出现异常都需要释放锁
}
lock提供了更多获取锁的方式:
🍬synchronized与lock的区别:
🍂synchronized是一个关键字在JVM内部实现的,lock是标准库的一个类在JVM外部实现
🍂语法上看,sychronized是自动加锁与释放锁,lock是手动的加锁与释放锁,相对lock比较灵活,但需要保证不管是否发生异常都需要释放锁
🍂lock获取锁的方式相对更加丰富,提供了多种api
🍂从效率上看,当线程冲突严重时,lock性能要高很多
🍖线程冲突严重时lock性能高的原因:
synchronized在释放锁以后,之前因为申请锁失败而阻塞的线程,都会再次竞争
lock是基于aqs来实现,aqs是一个双端队列专门用来管理线程的状态,竞争失败的线程就入队列并设置状态,释放锁后把队列中的线程引用拿出来并设置状态(获取到锁)
🍬如何选择使用哪个锁呢?
🍃当锁竞争不激烈的时候,使用synchronized效率高,自动释放更方便
🍃当锁竞争激烈的时候,使用lock,搭配tryLock更灵活控制加锁的行为,而不是死等
🍃需要使用公平锁的时候使用lock,lock默认是非公平的锁,但可以通过构造方法传入true开启公平模式(后面在锁策略中介绍)
🍉二. 常见的锁策略
🌴1. 乐观锁与悲观锁
悲观锁: 大部分时间来看,都是存在线程冲突的(悲观的看待问题),每次拿数据的时候都认为会修改,所以每次拿数据的时候都先加锁,完成执行操作后再释放锁
乐观锁: 大部分时间来看,是不存在线程冲突的(乐观的看待问题),每次都直接执行修改数据的操作,在数据提交更新的结果时,才会检测是否产生并发冲突,返回是否修改成功的结果,让程序自行处理逻辑
🍖乐观锁最重要的就是检测出是否发生线程冲突,这里引入一个版本号(version)解决:
主存中有变量和一个版本号,两个线程同时将主存中的信息读到各自的寄存器中,然后进行修改,线程1先修改然后往主存中写,然后对比版本号,相同的话version++,当线程2执行相同操作时发现版本号不相同,就写入失败
具体过程如下图:
乐观锁从Java层面来看是无锁操作,直接修改共享变量
返回true,修改成功
返回false,修改失败
🌾2. 自旋锁(Spin Lock)
线程在竞争锁失败的时候会进入阻塞状态,放弃CPU,需要过很久才能被再次调度,但是实际上,虽然当前竞争锁失败但是没有过多久就竞争到锁,没必要放弃CPU,这个时候就可以使用自旋锁来解决这种情况
自旋锁实际也是Java层面的无锁操作
while(true){
boolean result = 乐观锁方式修改数据;
if(result) break;
}
说明: 如果获取锁失败,就立即在尝试获取锁,无限循环直到获取到锁,一旦其他线程释放锁,就能第一时间获取到锁
如果满足乐观锁的使用条件:冲突概率比较小,如果发生冲突也能够很快的得到执行
一般来说乐观锁都考虑结合自旋锁来操作
🌵3. 读写锁
多个线程在数据读取时不会存在线程安全问题,但多个线程的写与读和写需要进行加锁,如果这两种场景下都用同一个锁就会产生很大的性能损耗,所以就要使用读写锁
读写锁就是对读和写操作区分对待,Java标准库中提供了ReentrantReadWriteLock类,实现了读写锁
ReentrantReadWriteLock.ReadLock表示一个读锁,提供了lock/unlock方法进行加锁释放锁
ReentrantReadWriteLock.WriteLock表示一个写锁,提供了lock/unlock方法进行加锁释放锁
关于读写锁:
🍁读与读之间不互斥
🍁写与写之间互斥
🍁读与写之间互斥
🍬读写锁适用频繁读,不频繁写的场景中
🌳4. 重量级锁与轻量级锁
重量级锁: 加锁机制重度依赖OS提供的mutex
大量的内核态与用户态切换
容易引发线程的调度
操作成本高
轻量级锁: 加锁机制尽可能不用mutex,而是尽量在用户态代码完成
少量的内核态与用户态切换
不太容易引发线程调度
操作成本相对低
🌴5. 公平锁与非公平锁
非公平锁:不以申请锁的时间先后顺序来获取到锁
缺陷:可能出现线程饥饿现象
优点:效率更高
公平锁:以申请锁的时间先后顺序来获取到锁
🍂synchronized是非公平锁,因为多个线程之间申请锁是竞争关系,不存在时间先后顺序
🍂lock默认下是非公平锁,但可以修改构造方法的参数来启用公平锁
🌾6. 可重入锁与不可重入锁
可重入锁: 同一个线程可以重复申请到同一个对象的锁
不可重入锁: 同一个线程不可以重复申请到同一个对象的锁
Java中,只要以Reentrant开头命名的锁都是可重入锁,lock实现类,synchronized关键字锁都是可重入锁
🍋三. CAS
🌴1. 认识CAS
CAS全称Compared And Swap(比较并交换),是jdk提供的一种乐观锁的实现,能够满足线程安全,以乐观锁的方式修改变量
比较并交换(比较,+1,交换)为一条CAS指令,是线程安全的
此处所谓的CAS指的是CPU提供了一个单独的CAS指令,通过这个指令可以完成比较并交换的过程
🍬CAS操作步骤:
🍀从主存拿值读取到自己的工作内存中,记为值A,执行修改操作后,值为B,主存的最新值记为C
🍀将B写入主存时比较A与主存的最新值C是否相等
🍀相等就写入成功,不相等就写入失败
🍀返回修改操作的结果
画图说明如下:
🌾2. jdk如何实现CAS
jdk提供了一个Unsafe的类,来执行cas的操作
jdk中unsafe提供的cas方法,是基于操作系统提供的cas方法,又是基于CPU提供的cas硬件支持,且CPU使用了lock加锁
简单说,Unsafe是基于操作系统和CPU提供的cas来实现的
🌵3. CAS的常见应用
Java的java.util.concurrent.atomic包下都是使用了cas来保证线程安全的,例如:++,--,!flag
原因: ++,--,!flag这种操作执行速度非常快,很快能得到执行,使用cas+自旋锁效率更高
既能够保证线程安全又能够比synchronized高效,因为不涉及竞争锁的竞争,线程要等待
典型的就是AtomicInteger类:
🍂addAndGet(int delta) i += delta
🍂decrementAndGet() --i
🍂getAndDecrement() i--
🍂incrementAndGet() ++i
🍂getAndIncrement() i++
👁🗨️例如:执行1000次n++和n--操作看结果
import java.util.concurrent.atomic.AtomicInteger;
public class AutoInt {
//共享变量n初始值为0
private static AtomicInteger n = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i < 1000;i++){
n.getAndIncrement(); //1000次n++;
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i < 1000;i++) {
n.getAndDecrement(); //1000次n--
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(n);
}
}
结果:打印结果为0
🌳4. CAS中的ABA问题
在没有引入版本号的情况下,CAS是基于变量的值,在读和写的时候比较的,但这个时候会存在下面的一个问题:
从中发现,如果在当前线程写入值进行比较的时候,如果有其他线程对主存中的的值进行修改,修改为当前线程从主存读取的值的时候,当前线程仍然能写入成功,但是存在线程安全问题
🍖ABA问题的解决?
引入版本号,线程每次修改共享变量的时候版本号+1,比较的时候比较读和写时候版本号是否相同,相同的话写入成功,否则写入失败
jdk中,提供了一个AtomicStampedReferenceE>的类,这个类可以对某个类进行包装,在其内部就提供了版本号的管理
🍏四. Synchronized的原理
🌴1.synchronized的特性
synchronized是基于对象头加锁,实现线程间的同步互斥
同步互斥:对同一个对象进行加锁的多个线程,同一个时间只有一个线程获取到锁,相当于多个线程获取锁执行同步代码是互斥的
🍬synchronized加锁操作,,只涉及对象头状态升级能升级不能降级,升级的过程:
无锁
偏向锁
轻量级锁
重量级锁
🌾2. 加锁的工作过程
偏向锁:第一次进入的线程,或是这个线程再次申请同一个对象锁
偏向锁不是真的 "加锁", 只是给对象头中做一个 "偏向锁的标记", 记录这个锁属于哪个线程.如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)如果后续其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态
轻量级锁:随着其他线程的竞争进入轻量级锁状态,CAS+(自适应的)自旋锁
自适应:自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源,因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了,也就是所谓的 "自适应"
重量级锁:竞争锁进一步激烈,就会升级为重量级锁(使用操作系统mutex加锁)
🌵3. JVM对synchronized的优化方案
锁消除,前提:变量只有一个线程可以操作
比如这段代码:
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
此时每个 append 的调用都会涉及加锁和解锁,但如果只是在单线程中执行这个代码, 那么这些加
锁解锁操作是没有必要的, 白白浪费了一些资源开销
锁粗化,前提:变量是多个线程可以使用,,但是一个线程出现多次连续sychronized加锁