🤡🤡🤡个人主页🤡🤡🤡
🤡🤡🤡JavaEE专栏🤡🤡🤡
文章目录
- 1.锁策略
- 1.1悲观锁和乐观锁
- 1.2重量级锁和轻量级锁
- 1.3自旋锁和挂起等待锁
- 1.4可重入锁和不可重入锁
- 1.5公平锁和非公平锁
- 1.6互斥锁和读写锁
- 2.synchronized的实现原理
- 2.1实现过程
- 2.2偏向锁
- 2.3优化策略
- 2.3.1锁升级
- 2.3.2锁消除
- 2.3.3锁粗化
- 3.CAS
- 3.1什么是CAS
- 3.2CAS的应用
- 3.2.1原子类
- 3.2.2实现自旋锁
- 3.3CAS的ABA问题
- 3.3.1ABA问题
- 3.3.2ABA引起的BUG
- 3.3.3如何避免由ABA问题引起的BUG
- 4.java.util.concurrent常见类
- 4.1Callable接口
- 4.2ReentrantLock类
- 4.2.1发音
- 4.2.2ReentrantLock的关键特性与功能
- 4.3Semaphore类——信号量
1.锁策略
1.1悲观锁和乐观锁
区别:加锁的时候,预测当前锁冲突的概率是大还是小
悲观锁:预测当前锁冲突概率大,后续要做的工作往往就会更多,加锁的开销(时间,系统资源)就更大。
乐观锁:预测当前锁冲突概率小,后续要做的工作往往就会更少,加锁的开销(时间,系统资源)就更小。
注意:悲观锁往往通过内核来完成操作的,所以做的工作多,乐观锁往往通过用户完成操作的,所以做的工作少。
1.2重量级锁和轻量级锁
这两个锁和上述的悲观锁和乐观锁有着很大的关系,一般悲观锁就是重量级锁,因为悲观锁做的任务多,那么就需要很大的开销所以就是重量级锁,反之乐观锁做的任务少,那么开销就少那么就是轻量级锁。
注意:一般这两个锁和上述两个锁都混着用,之所以有区别是因为出发点不一样。
1.3自旋锁和挂起等待锁
自旋锁:锁未被释放之前,cpu会一直空转和忙等,但等锁被释放之后就会立马获取到锁,自旋锁是实现轻量级锁典型的案例。
挂起等待锁:当出现锁冲突,那么要加锁的这个线程就会被挂起等待,此时的线程就不会参与调度,直到这个锁被释放,然后系统内核才唤醒这个线程,去尝试获取锁,拿锁的速度很慢,挂起等待锁是实现重量级锁的典型案例。
1.4可重入锁和不可重入锁
可重入锁:针对一个线程,可以连续加锁两次不会出现死锁,synchronized就是可重入锁。
不可重入锁:针对一个线程,连续加锁两次会出现死锁。像c++中的std::mutex就是不可重入锁
1.5公平锁和非公平锁
公平锁:严格按照先来后到的顺序来获取锁,哪个线程等待的时间长,哪个线程就拿到锁
非公平锁:若干个线程,各凭本事,随机获取锁和线程等待时间无关,synchronized就是一个非公平锁。
系统调度本来就是随机的,如果想实现一个公平锁,那么就需要引入一个特殊的队列来根据线程等待的时间来出队列。
1.6互斥锁和读写锁
互斥锁:加锁和解锁,synchronized就是一个互斥锁
读写锁:加读锁和加写锁,读与读之间不会产生互斥,写与写之间会产生互斥,读与写之间也会产生互斥,
2.synchronized的实现原理
2.1实现过程
未加锁(无锁状态)——>偏向锁——>轻量级锁——>重量级锁
2.2偏向锁
首次使用sychronized对对象加锁,此时并不是真正的加锁而是做了一个标记(非常轻量非常快,几乎没有开销)
如果在这其中没有其他线程对这个对象加锁,那么就一直保持这样的一个状态直到解锁的时候(解锁也只是修改标记,几乎没有开销)
但是如果在这期间,有其他对象对进行加锁,那么就会立马升级成轻量级锁。
注意:在这个升级过程中,是不可逆的,一旦升级了就不能降级(在目前版本的JVM中)
2.3优化策略
2.3.1锁升级
2.3.2锁消除
当你在这个代码中加了锁,编译器和JVM会检查当前代码需不需要加锁,不需要就会将这个锁帮你消除,这是在内部操作的,程序猿是感知不到的,例如我在单线程中加了锁,那么编译器和JVM就会把这个锁给消除。
2.3.3锁粗化
在有些逻辑中,需要频繁加锁和解锁,编译器就会自动把这些多次细粒度的锁合成一次粗粒度的锁,"粒度"是指加锁范围中代码的多少,代码越多粒度就越粗,反之越细。
3.CAS
3.1什么是CAS
CAS这是一个比较交换指令,而且这一指令详细的就是读取内存,比较是否相等,修改内存三个步骤,而与之前线程安全问题中多个线程对同一个变量进行修改操作是一样的,也是上述这三个步骤,但CAS这三个步骤是打包一起的是原子的,而对变量修改不是原子的,所以需要加锁,才能保证线程安全,所以CAS在某种程度也可以实现锁的功能。
3.2CAS的应用
3.2.1原子类
标准库中提供了 java.util.concurrent.atomic 包, ⾥⾯的类都是基于这种⽅式来实现的. 典型的就是 AtomicInteger 类.
通过方法来实现变量的算术运算
//count++
count.getAndIncrement();
//++count
count.incrementAndGet();
//count--
count.getAndDecrement();
//--count
count.decrementAndGet();
//count += 10
count.getAndAdd(10);
通过CAS实现两个线程对count变量相加
AtomicInteger count = new AtomicInteger(0);
Thread t1 = new Thread(()-> {
for (int i = 0; i < 50000; i++) {
count.getAndIncrement();
}
});
Thread t2 = new Thread(()-> {
for (int i = 0; i < 50000; i++) {
count.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count);
这种编程方式也称为无锁编程,这种方式可以提高效率,但适用范围不大。
3.2.2实现自旋锁
由于在读取内存,比较相等,修改内存这三个步骤是原子,所以在某种程度上可以实现锁的功能
以下就是一段伪代码简单实现一下自旋锁
class SpinLock{
public Thread owner = null;
public void lock() {
while(!CAS(this.owner,null,Thread.currentThread())) {
}
}
public void unlock() {
this.owner = null;
}
}
在这个方法中,owner变量如果是null,那么此时就是未加锁状态,那么CAS方法就返回true,取反则为false,退出循环,此时就是未加锁,但当owner这个变量不为null的时候调用CAS方法的线程就会将该对象的引用赋值给this.owenr,这个操作是原子的,所以不会有线程安全问题,从侧面就可以体现相当于对这个线程加锁,从此实现了锁的功能。
3.3CAS的ABA问题
3.3.1ABA问题
假设此时我有两个线程分别为t1和t2线程,还有一个共享变量num,此时t1线程想要通过CAS编码的形式把num原来的值A改为C,但是在这个操作之中,t2线程将num中的值从A改为B,又从B改为A,但是在这期间,t1线程不知道。
3.3.2ABA引起的BUG
比如,有一个cs中的悍匪玩家刚好现在余额还有17元买一个钥匙开个箱子,在购买的过程中,因为网络的波动,他按了两次购买键,导致启动了两个线程去完成这个任务,那么t1线程在购买的时候,t2线程在等待阻塞,t1线程完成了购买获得了一把钥匙,但在t2线程执行前刚好另一个ct好友给这位悍匪玩家充值了17元,导致t2线程在执行的时候发现余额中还有17元,则又帮悍匪玩家买了一把,此时就出现了bug,这个时候买了两把钥匙就是ABA问题引起的。
3.3.3如何避免由ABA问题引起的BUG
在要修改的值加入一个版本号,在CAS比较数据的时候比较当前值和之前要修改的值,也要比较版本号是否相同
CAS操作在读取之前要修改的值的同时,也要读取版本号
真正到修改的时候:
如果当前读的版本号与之前要修改的版本号相同,则修改数据并且修改版本号
如果当前读的版本号与之前要修改的版本号不相同,则视为修改失败(可以认为该数据被修改过)
将这个思路带到悍匪玩家买钥匙的案例中:
- 购买钥匙需要17余额,t1和t2线程获取的余额是17,版本号都是1
- t1线程购买成功之后,余额变为0,版本号变为2,此时t2线程再阻塞等待
- 在t2线程执行前,悍匪玩家的好友ct兄弟为其充值了17元余额,此时账户余额又变为17,但此时版本号为3
- 到t2线程执行的时候,发现余额是17,和之前读到的余额是一样,但是版本号不一样,第一次读的是1,但现在读的是3,版本号不同,则可以视为操作失败
4.java.util.concurrent常见类
4.1Callable接口
这个接口里有个叫call()方法,这个方法与Runnable接口中的run()方法其实大同小异,只是前者有个返回值
在Java中,Thread类的构造方法不直接接受一个Callable对象作为参数。,则需要另一个类来作为一个媒介,FutureTask作为一个媒介。
public static void main(String[] args) throws InterruptedException, ExecutionException {
Callable<Integer> callable = new Callable<Integer>() {
int sum = 0;
@Override
public Integer call() throws Exception {
for (int i = 1; i < 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
thread.join();
System.out.println("sum = " + futureTask.get());
}
4.2ReentrantLock类
4.2.1发音
ReentrantLock
4.2.2ReentrantLock的关键特性与功能
- ReentrantLock提供了公平锁的实现
- ReentrantLock提供了tryLock操作,该操作会立即返回,是否获取到锁,这对于避免死锁和实现超时机制非常有用。
- ReentrantLock搭配了Condition类完成等待通知,Condition比wait和notify更强点,Condition可以指定阻塞线程唤醒
4.3Semaphore类——信号量
信号量就是一个计数器,计系统资源的个数。
在底层中对信号量的两个基本操作分别为P操作和V操作
P操作——申请资源
V操作——释放资源
在java中JVM将这两个操作分别封装成acquire(申请资源),release(释放资源)