多线程进阶上

常见锁策略

锁策略和程序员无关,和"实现锁"的人才有关系

所提及到的锁策略,和Java本身没有关系,适用于所有和"锁"相关的情况.

悲观锁 vs 乐观锁(处理锁冲突的原因)

悲观锁:预期所冲突的概率很高

乐观锁:预期锁冲突的概率很低

悲观锁做的工作更多,付出的成本更多,更低效

乐观锁做的工作更多,付出的成本更低,更高效

读写锁 vs普通的互斥锁

对于普通的互斥锁,只有两个操作 加锁和解锁

只要两个线程针对同一个对象加锁,就会产生互斥

对于读写锁来说,分为了三个操作

  • 加读锁 : 如果代码只是进行了读操作,就加读锁
  • 加写锁 : 如果代码只是进行了写操作,就加写锁
  • 解锁 : 针对上面两个锁都可以解锁

针对读写锁:

  • 针对读锁和读锁之间,是不存在互斥的(多线程同时同一个变量,不会有线程安全问题)
  • 针对读锁和写作之间,存在互斥
  • 针对写锁和写锁之间,存在互斥

重量级锁 vs轻量级锁(处理所冲突的结果)

重量级锁,就是做的事情比较多,开销更大

轻量级锁,就是做的事情比较少,开销更小

在一般情况下,不绝对的情况下

悲观锁可以看做是一个重量级锁

乐观锁可以看做是一个轻量级锁

再使用的锁中,如果锁是基于内核的一些功能来实现的(比如调用了操作系统提供的mutex接口)此时一般认为是重量级锁(操作系统的锁会在内核做很多的事情,比如线程等待…)

如果锁是纯用户态实现的,此时一般认为是轻量级锁(用户态的代码更可控,更高效)

挂起等待锁 vs 自旋锁

挂起等待锁 , 往往就是通过内核的一些机制来实现的,往往较重 (重量级锁的一种典型实现)

自旋锁 , 往往就是通过用户态代码实现的 (轻量级锁的一种典型实现)

公平锁 vs非公平锁

公平锁:多线程在等待同一把锁的时候,谁是先来的,谁可以获得这把锁(先来后到原则)

非公平锁: 多线程在等待同一把锁的时候,不遵循先来后到(每个等待的线程获取到锁的概率是均等的)

对于操作系统来说,本身线程之间的调度是随机的(均等的) 操作系统提供的 mutex 这个锁就是非公平锁

要想实现公平锁,反而需要付出更多的代价

可重入锁 vs 不可重入锁

一个线程针对同一个锁,连续加锁两次,如果会死锁,就是不可重入锁;如果不会发生死锁,就是可重入锁

synchronized的锁策略

  1. 既是一个乐观锁,也是一个悲观锁(根据锁的激励竞争,自适应)
  2. 不是读写锁,只是一个普通互斥锁
  3. 既是一个轻量级锁,也是一个重量级锁(根据锁的激励竞争,自适应)
  4. 轻量级锁的部分基于自旋锁来实现,重量级锁的部分基于挂起等待锁来实现
  5. 非公平锁
  6. 可重入锁

CAS

CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

  1. 比较 A 与 V 是否相等。(比较)

  2. 如果比较相等,将 B 写入 V。(交换)

  3. 返回操作是否成功。

CAS伪代码

下面写的代码不是原子的, 真实的 CAS 是CPU提供的单独的CAS指令通过这一指令,就可以完成下面伪代码的过程,是原子的,所以线程安全

这个伪代码只是辅助理解CAS 的工作流程.

boolean CAS(address, expectValue, swapValue) {
 if (&address == expectedValue) {
   &address = swapValue;
        return true;
   }
    return false; 
}

address: 待比较的内存地址

expectValue: 预期内存的值

swapValue :希望将内存的值改成这个新的值

代码拿出待比较的内存空间地址的值和预期内存的值进行比较,如果相同,就把期望更改的新的 值写入到内存地址中,并且返回true;如果比较结果不相同,返回false.

CAS最大的意义就是为我们写多线程安全的代码,提供了新的思路和方向

CAS的应用

实现原子类

Java标准库中提供了一组原子类,针对所常用的依稀int,long.int array…进行一封装,可以基于CAS的方式进行修改,并且线程安全

多线程进阶上_第1张图片

这个代码里面不存在线程安全问题,他是基于CAS实现的++操作,既可以保证线程安全,可比synchronized高效,synchronized会涉及锁的竞争,两个线程相互等待,CAS不会涉及这些.是工作中高频使用更多东西

伪代码实现

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

value存储的是原始的数据

int oldValue = value; 操作看起来是一个oldValue变量实际可能是一个寄存器(伪代码不好表示寄存器) 这个操作相当于从内存中读到寄存器中(load)

CAS(value, oldValue, oldValue+1) 判定一下当前内存的值是不是和刚才寄存器读到的值一致,如果判定成功,就把value的值改为oldValue+1,返回true,循环结束,如果判定失败,就啥也不做,返回false,继续下次循环(下次循环,先重新读一下value,在cas)

实现简图:
多线程进阶上_第2张图片

基于CAS实现"自旋锁"

伪代码

public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

owner 表示的是自旋锁,记录的是当前的锁被哪个线程持有,如果为null表示该当前未加锁\

while(!CAS(this.owner, null, Thread.currentThread())) 通过一个循环来实现循环里调用CAS,CAS会比较前面的owner值是否为null,如果是null就改成当前线程,意思就是当前线程拿到了锁;如果不是null,就返回false进入下一次循环,下次循环依旧是CAS操作

如果当前这个锁一直被别人持有,当前尝试加锁的线程就会在这个循环里快速反复的进行循环---->自旋

自旋锁是一个轻量级锁,可以看做是一个乐观锁

CAS的ABA问题(重要)

CAS的关键就是 先比较 在交换

比较其实是在比较当前值 和旧值是不是相同,把这两个值相同,就是视为中间没有发生过改变.

但是这个中间没有发生改变也有可能是中间改变过了,然后又变回来了

例如一开始是A,中间变成了B,最后又变回成了A,这是比较的时候,以为没有发生改变,但其实发生过改变.

关于ABA问题的实例

假如我有的账户有100元,这时我去ATM机准备取50元,但当我按下取款按钮的时候,机器卡了,我就多按了一下取款按钮.这相当于一次取钱操作,执行了两次(两个线程,并发的去执行这个操作),期望的是只取成功一次

基于CAS对的方式实现这里的取款操作

ind oldValue = value;//读取旧值
CAS(&value,oldValue,oldValue-50)

多线程进阶上_第3张图片

此时没有发生ABA问题,但是加一个条件,就是朋友这个时候转过来了50元钱,就会发生ABA

多线程进阶上_第4张图片

可以看到,在执行t2的cas操作之前,value的值和oldValue的值相同,所以还要继续扣50,这就产生了安全问题.

解决方案

给要修改的值, 引入版本号. 这个版本号只能变大,不能变小,修改变量的时候,比较的不是变量本身,而是比较版本号.

CAS 操作在读取旧值的同时, 也要读取版本号.

  • 真正修改的时候,
    • 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
    • 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).

上述例子:

ATM的余额引入一个版本号

多线程进阶上_第5张图片

一开始的版本号为一,t1线程cas后的版本号为2,t3转账后的版本号编程了3,t2线程在进行cas时,比较版本号会发现不相同,不做操作

不光是可以插入版本号,只要是一个单调递增或者单调递减的数据就可以,比如时间戳

你可能感兴趣的:(JavaEE,java,mysql,数据库)