【JAVAEE】常见的锁策略

目录

1.常见的锁

1.乐观锁&悲观锁

2.轻量级锁&重量级锁

3.读写锁&普通互斥锁

4.自旋锁&挂起等待锁

5.可重入锁&不可重入锁

6.公平锁&非公平锁

2.CAS

1.什么是CAS

2.CAS的应用

1.实现原子类

2.实现自旋锁

3.synchronized用到的锁策略

1.synchronized实现的锁策略

2.加锁的工作过程

3.一些优化操作

1.锁消除

2.锁粗化


1.常见的锁

1.乐观锁&悲观锁

乐观锁:对运行环境持乐观态度,刚开始不加锁,当有竞争的时候再去加锁

悲观锁:对运行环境持悲观态,刚开始就直接加锁

举个例子:

同学 A 认为 " 老师是比较忙的 , 我来问问题 , 老师不一定有空解答 ". 因此同学 A 会先给老师发消息 : " 老师你忙嘛 ? 我下午两点能来找你问个问题嘛 ?" ( 相当于加锁操作 ) 得到肯定的答复之后 , 才会真的来问问题 . 如果得到了否定的答复 , 那就等一段时间 , 下次再来和老师确定时间 . 这个是悲观锁 .
同学 B 认为 " 老师是比较闲的 , 我来问问题 , 老师大概率是有空解答的 ". 因此同学 B 直接就来找老师 .( 没加锁 , 直接访问资源 ) 如果老师确实比较闲 , 那么直接问题就解决了 . 如果老师这会确实很忙 , 那么同学 B 也不会打扰老师 , 就下次再来 ( 虽然没加锁 , 但是能识别出数据访问冲突 ). 这个是乐观锁 .

2.轻量级锁&重量级锁

轻量级锁:可以是纯用户态的锁,消耗的资源比较小

重量级锁:可能会调用到系统的内核态,消耗的资源比较多。

锁的核心特性 " 原子性 ", 这样的机制追根溯源是 CPU 这样的硬件设备提供的 .
  • CPU 提供了 "原子操作指令".
  • 操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.
  • JVM 基于操作系统提供的互斥锁, 实现了 synchronized ReentrantLock 等关键字和类.
重量级锁 : 加锁机制重度依赖了 OS 提供了 mutex
  • 大量的内核态用户态切换
  • 很容易引发线程的调度
这两个操作 , 成本比较高 . 一旦涉及到用户态和内核态的切换 , 就意味着 " 沧海桑田 ".
轻量级锁 : 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成 . 实在搞不定了 , 再使用 mutex.
  • 少量的内核态用户态切换.
  • 不太容易引发线程调度

3.读写锁&普通互斥锁

读锁:共享锁,读与读可以同时拿到锁资源

写锁:排他锁,不能同时写写,写读或者读写

普通互斥锁:synchronized,只能有一个线程拿到锁资源,其它的要参与锁竞争,没有竞争到锁的时候就要阻塞等待。

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生 。
举个例子:
比如教务系统 .
每节课老师都要使用教务系统点名 , 点名就需要查看班级的同学列表 ( 读操作 ). 这个操作可能要每周 执行好几次 .而什么时候修改同学列表呢 ( 写操作 )? 就新同学加入的时候 . 可能一个月都不必改一次 .

4.自旋锁&挂起等待锁

自旋锁:不停的询问资源是否被释放,如果释放了第一时间可以获得锁资源

挂起等待锁:等待通知之后再去竞争锁,并不会第一时间获取到锁资源

举个例子:

 自旋锁是一种典型的 轻量级锁 的实现方式.

优点 : 没有放弃 CPU, 不涉及线程阻塞和调度 , 一旦锁被释放 , 就能第一时间获取到锁 .
缺点 : 如果锁被其他线程持有的时间比较久 , 那么就会持续的消耗 CPU 资源 . ( 而挂起等待的时候是 不消耗 CPU )

5.可重入锁&不可重入锁

可重入锁:对于同一个锁对象可以加多次锁

不可重入锁:不能对同一个锁对象加多次锁

6.公平锁&非公平锁

公平锁:先排队等待的线程先获取到锁资源

非公平锁:没有先来后到的说法,谁抢到就是谁的

2.CAS

1.什么是CAS

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

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

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

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

3.返回操作是否成功

CAS伪代码(工作流程):

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

用期望值与内存中的值比较,如果内存中的值与期望值相等,那么用swapValue覆盖内存中的值,如果期望值与内存中的值不等那么什么也不做。

2.CAS的应用

1.实现原子类

标准库中提供了 java.util.concurrent.atomic , 里面的类都是基于这种方式来实现的 。典型的就是 AtomicInteger .。 其中的 getAndIncrement 相当于 i++ 操作 .。
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();

伪代码实现:

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

假设两个线程同时调用getAndIncrement。

(1).两个线程都读取value的值到oldvalue中。

 (2).线程1先执行CAS操作,由于oldvalue和value值相同,直接对value进行赋值

注意 :
CAS 是直接读写内存的 , 而不是操作寄存器 .
CAS 的读内存 , 比较 , 写内存操作是一条硬件指令 , 是原子的 .

 (3)线程2再执行CAS操作,第一次CAS的时候发现oldvalue和value不相等,不能赋值,因此进入循环。在循环中重新读取value的值赋给oldvalue

 (4)线程2接下来第二次执行CAS,此时oldvalue和value相等,于是直接进行赋值操作

 (5)线程1和线程2返回各自oldvalue值即可。

通过形如上述代码就可以实现一个原子类 . 不需要使用重量级锁 , 就可以高效的完成多线程的自增操作。

2.实现自旋锁

自旋锁伪代码:

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;
   }
}

3.synchronized用到的锁策略

通过以上的锁策略可以知道,synchronized在不同的时期可能会用到不同的锁策略。

1.synchronized实现的锁策略

①既是乐观锁也是悲观锁

②既是轻量级锁也是重量级锁

  • 轻量级锁是基于自旋锁实现的
  • 重量级锁是基于挂起等待锁实现的

③是普通互斥锁

④既是自旋锁也是挂起等待锁

⑤是可重入锁

⑥是非公平锁

1. 开始时是乐观锁 , 如果锁冲突频繁 , 就转换为悲观锁 .
2. 开始是轻量级锁实现 , 如果锁被持有的时间较长 , 就转换成重量级锁 .
3. 实现轻量级锁的时候大概率用到的自旋锁策略

2.加锁的工作过程

 ①偏向锁

第一个进行加锁的线程,优先进入偏向锁状态。

偏向锁不是真的 " 加锁 ", 只是给对象头中做一个 " 偏向锁的 标记 ", 记录这个锁属于哪个线程 .
如果后续没有其他线程来竞争该锁 , 那么就不用进行其他同步操作了 ( 避免了加锁解锁的开销 )
如果后续有其他线程来竞争该锁 ( 刚才已经在锁对象中记录了当前锁属于哪个线程了 , 很容易识别当前申请锁的线程是不是之前记录的线程 ), 那就取消原来的偏向锁状态 , 进入一般的轻量级锁状态 .
偏向锁本质上相当于 " 延迟加锁 " . 能不加锁就不加锁 , 尽量来避免不必要的加锁开销 .
但是该做的标记还是得做的 , 否则无法区分何时需要真正加锁 .

②轻量级锁

随着其它线程进入竞争,偏向锁状态被消除,进入轻量级锁状态(自适应的自旋锁)。此时的轻量级锁就是通过CAS来实现的。

  • 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
  • 如果更新成功, 则认为加锁成功
  • 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).
自旋操作是一直让 CPU 空转 , 比较浪费 CPU 资源 .
因此此处的自旋不会一直持续进行 , 而是达到一定的时间 / 重试次数 , 就不再自旋了 .
也就是所谓的 " 自适应 "
③重量级锁
如果竞争进一步激烈,自旋不能快速获取到锁状态,就会膨胀为重量级锁。

3.一些优化操作

1.锁消除

在写代码时,程序会加入synchronized来保证线程安全。

如果加了synchronized的代码块中,只有读操作没有写操作,JVM就认为这个代码块没有必要加锁,JVM运行的时候就会被优化掉,这个现象叫做锁消除

2.锁粗化

一段逻辑中如果出现多次加锁解锁,编译器+JVM会自动进行锁的粗化。

执行一个业务逻辑发生了四次锁竞争,在保证程序正确的情况下,JVM会做出优化,只加一次锁,整个逻辑执行完后再释放,从而提高效率。

举个例子:

滑稽老哥当了领导 , 给下属交代工作任务 :
方式一 :
打电话 , 交代任务 1, 挂电话 .
打电话 , 交代任务 2, 挂电话 .
打电话 , 交代任务 3, 挂电话 .
方式二 :
打电话 , 交代任务 1, 任务 2, 任务 3, 挂电话 .
显然 , 方式二是更高效的方案 .
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值