锁策略, cas 和 synchronized 优化过程

     

常见的锁策略

1)乐观锁与悲观锁:这里的锁并不是指某个具体的锁,而是概念,描述锁的特性,描述的是一类锁。       

 乐观锁:预测该场景中,不太容易出现锁冲突的情况。后续做的工作较少。

悲观锁:预测该场景中,非常容易出现锁冲突的情况。后续做的工作较多。

2)重量级锁和轻量级锁:

重量级锁:加锁的开销比较大(花的时间多,占用系统资源多),一个悲观锁(后续做的工作较多)很可能就是一个重量级锁。

轻量级锁:加锁的开销比较小(花的时间少,占用系统资源少),一个乐观锁(后续做的工作较少)很可能就是一个轻量级锁。

悲观乐观,是在加锁之前,对锁冲突概率的预测。

轻量重量,是在加锁之后,考量实际的锁的开销。

3)自旋锁和挂起等待锁:

自旋锁是轻量级锁的一种典型实现,在用户态下,通过自旋的方式(如while()),实现类似与锁的机制。如果发生阻塞时,线程会一直循环尝试访问,使之能最快速度能获取到这把锁。因为是第一时间发现,因此花费的时间更少,但是因为一直尝试获取,所占用的资源更多。

挂起等待锁是重量级锁的一种典型实现,通过内核态,借助系统提供的锁机制,当出现锁冲突时,会牵扯到内核对与线程的调度,使冲突的线程出现阻塞等待。线程会不再访问,直到发现该锁被释放后,才去尝试获取这把锁,因为不是第一时间发现,可能途中这把锁已经被多次释放,因此花费的时间更多,但是因为不需要一直尝试获取,所占用的资源更少。

4)读写锁与互斥锁:

读写锁:把读操作加锁和写操作加锁给分开了,多个线程同时取读同一个变量,不涉及到线程安全问题。如果两个线程都是读加锁的话,不产生锁竞争,一个线程读加锁,一个线程写加锁,会产生锁竞争,两个线程都是写加锁的话,会产生锁竞争。与数据库事务的锁类似,但事务的锁更为细致,情况更多。

互斥锁:就是普通的锁,一个线程获取了这把锁后,其他的线程就不能获取,直到它被释放。

5)公平锁和非公平锁:

公平锁:遵守先来后到的规则,多个线程等待一把锁的释放,其中一个线程最先来的,那么它就能比其他的线程更快的获取这把锁。

非公平锁:不遵守先来后到的规则,多个线程等待一把锁的释放,它们获取这把锁的概率是相同的。操作系统自带的锁(pthread_mutex)就是非公平锁。

6)可重入锁与不可重入锁:如果一个线程针对一把锁,连续加锁两次,如果出现死锁的话,就是不可重入锁,不出现死锁的话,则是可重入锁。

synchronized 具体采用了哪些锁策略呢?

1.synchronized既是悲观锁,又是乐观锁。(在某些情况下是悲观锁,在某些情况下又是乐观锁)

2.synchronized即使重量级锁,又是轻量级锁。synchronized重量级锁部分是基于系统的互斥锁实现的,轻量级锁部分是基于自旋锁实现的

3.synchronized是非公平锁

4.synchronized是可重入锁。

5.synchronized不是读写锁。

synchronized内部实现策略(内部原理):代码写了一个synchronized之后,这里可能会产生一系列的”自适应的过程“,这个过程也称为锁升级或锁膨胀,synchronized就会从无锁状态到偏向锁(偏向锁,不是真的加锁,而是只做了标记,如果有别的线程来竞争锁,才会真的加锁,否则就自始至终都不加锁),当有其他线程来竞争锁后就升级为轻量级锁,当竞争变得更激烈后就升级为重量级锁。

锁消除:编译器会智能的判定,该代码是否需要加锁,如果加了锁,但其实实际上没必要加锁,那么就会把加锁操作自动消除。

锁粗化:与锁的粒度有关,如果加锁操作里面的包含的实际要执行的操作越多,就认为锁的粒度越大,有些时候希望锁的粒度大,因为加锁也需要开销,有时希望锁的粒度小,并发程度高。当一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.

关于cas

  cas,全称为compare and swap,就是字面意义的比较和交换。能够比较和交换某个寄存器中的值和内存中的值,看看是否相等,如果相等,则把另外一个寄存器中的值进行交换。

cas伪代码:

boolean CAS ( address , expectValue , swapValue ) {
if ( & address == expectedValue ) {
  & address = swapValue ;
        return true ;
  }
    return false ;
}
其中address是内存中的地址, expectValue , swapValue是寄存器中的地址, 真实的 CAS 是一个原子的硬件指令完成的. 这个伪代码只是辅助理解。 当多个线程同时对某个资源进行 CAS 操作,只能有一个线程操作成功,但是并不会阻塞其他线程 , 其他线程只会收到操作失败的信号。因此,基于cas,又能衍生出一套无锁编程。
        那么cas有哪些应用场景呢?
1)实现原子类:比如增对一个count进行自增。 标准库中提供了 java.util.concurrent.atomic , 里面的类都是基于这种方式来实现的 . 典型的就是 AtomicInteger . 其中的 getAndIncrement 相当于 i++ 操作 .
public static AtomicInteger atomicInteger = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                atomicInteger.getAndIncrement();//相当于count++
//                atomicInteger.incrementAndGet();//相当于++count
//                atomicInteger.decrementAndGet();//相当于--count
//                atomicInteger.getAndDecrement();//相当于count--;
            }
        });

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                atomicInteger.getAndIncrement();
            }
        });

        thread.start();
        thread1.start();
        thread.join();
        thread1.join();
        System.out.println(atomicInteger.get());
    }

并没有加锁,就完成了代码的正确运行。那么它是如何做到的呢?以下是伪代码实现:

class AtomicInteger {
    private int value ;
    public int getAndIncrement () {
        int oldValue = value ;
        while ( CAS ( value , oldValue , oldValue + 1 ) != true ) {
            oldValue = value ;
      }
        return oldValue ;
  }
}
倘若有两个线程同时执行这个代码,那么可能就会出现这种情况,线程1(以下简称t1)先执行
int oldValue = value ;然后线程2(以下简称t2)将整个方法都执行完,最后t1执行完整个方法,
因为t2执行完后,会使得value++,因此此时t1线程里的oldvalue与当前的value会不相等,因此当执行cas时,因为value != oldervalue,因此返回false,因为满足循环条件,因此会进入循环,去更新oldvalue的值,然后再去判断,倘如此时 oldValue = = value,那么就执行value++。如果有更多线程进行操作的话,就可能会使得oldValue更新多次,然后进行自增操作。因此这里的cas其实是判断该过程中是否有其他线程的插入,如果有,就返回false,然后更新到最新结果,如果没,就直接进行自增操作。因此原子类/CAS保证线程安全,是通过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 ;
  }
}

CAS的ABA问题:

CAS的关键要点是比较寄存器1和内存中的值,通过这里的值是否相等,来判断内存的值是否发生改变,也就是是否有其他的线程修改过。如果值不相等,存在线程进行修改,如果值相等,不存在线程进行修改,那么倘若有一个线程对它进行修改过后又改回来了,那么它就无法知道是否有线程插入,这就出现问题了。例如有人在银行取钱,因为取款机卡了,点了两下取款,取款机创建了两个线程,并发执行-100的操作,那么就我们就期望一个线程-100后,第二个线程就不能执行,但是如果使用CAS来操作,就会出现问题。
  存款 200. 线程1 获取到当前存款值为 200, 期望更新为 100; 线程2 获取到当前存款值为 200,
望更新为 100. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中. 在线程2 执行之前, 这个人的朋友正好给他转账 100, 账户余额变成 200 !! 轮到线程2 执行了, 发现当前存款为 200, 和之前读到的 200 相同, 再次执行扣款操作,这就执行了两次扣款操作,这就是ABA问题

解决CAS问题:

那么如何解决这个问题呢?我们可以给要修改的数据引入版本号. CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当前版本号比之前读到的版本号大, 就认为操作失败。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值