【多线程】常见的锁策略与锁

文章目录


一、常见的锁策略

锁策略,即当发生"锁冲突"时,该怎么办?(不只是阻塞

(一)乐观锁和悲观锁

乐观锁与悲观锁: 预测接下来发生锁冲突的概率(冲突概率角度)

  • 乐观锁:预测接下来锁冲突的概率不高,因此接下来对共享资源的操作就不会加锁,当提交数据更新时,才去检查是否发生冲突,发生了就执行相应的重试逻辑
  • 悲观锁:预测接下来锁冲突的概率较高,因此接下来对共享资源的操作会提前加锁

悲观的心态就是总是在以防万一,因此其总是会做很多事,避免意料之外的事。

synchronized既是乐观锁也是悲观锁(自适应),详细内容见后文。

以悲观锁的姿态工作,付出的代价更大,开销更大,更加稳健;

以乐观锁的姿态工作,付出的代价更小,开销更小,可能引入额外的风险;

(二)重量级锁和轻量级锁

重量级锁与轻量级锁: (开销角度)

  • 重量级锁:加锁的开销比较大 =》悲观锁
  • 轻量级锁:加锁的开销比较小 =》乐观锁

大部分情况下,重量级锁就是悲观锁,轻量级锁就是乐观锁;

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

(三)挂起等待锁和自旋锁

挂起等待锁与自旋锁:

  • 挂起等待锁:重量级锁的典型实现方式,出现锁冲突时,会将线程进入到阻塞状态(消耗的时间多,但会让出CPU资源);因为是阻塞,因此一个线程A释放锁,与其冲突的线程B无法第一时间感知到锁(B在做自己的事);
  • 自旋锁:轻量级锁的典型实现方式,出现锁冲突时,通过忙等的方式,等待锁被释放(消耗的时间更少,但会消耗更多CPU资源);因为是忙等,因此如果一个线程A释放锁,与其冲突的线程B能第一时间拿到锁(B一直在等)

synchronized是自适应,在冲突概率低的时候为自旋锁,冲突概率高的时候为挂起等待锁;

(四)公平锁和非公平锁

公平锁与非公平锁:

Java中,先来后到算作公平;

synchronized是非公平锁(概率均等),这是因为操作系统线程调度是随机的;

公平锁需要额外创建队列,维护多个线程加锁的先后顺序;

非公平锁,也能解决大部分情况,因此synchronized设计为非公平锁;

ReentrantLock提供了两个版本的锁,公平锁和非公平锁;

(五)可重入锁和不可重入锁

可重入锁和不可重入锁: 多个线程对能否对同一线程多次加锁

(六)互斥锁和读写锁

普通互斥锁和读写锁:

synchronized是普通互斥锁,就只有普通的加锁,解锁;

读写锁就是将读写操作进行分离,其针对的是多线程同时写/一个读一个写的情况(存在线程安全问题);

读写锁的核心操作有三种: 1.加读锁 2.加写锁 3.解锁 (写锁和写锁,读锁和写锁都要竞争,读锁和读锁不会竞争)

由于日常开发中,"一写多读"的情况很常见,读的频率远高于写,因此其实用性很强;

二、synchronized

1.synchronized的特性

  • synchronized是一把可重入锁,可以对该锁进行多次加锁
  • synchronized是一把不公平锁,意味着不会像公平锁那样引入额外的开销
  • synchronized是一把互斥锁,只进行普通的加锁和解锁
  • synchronized是一把自适应锁,会根据可能的锁竞争情况来对锁进行升级

2.synchronized加锁原理

偏向锁:并不是真的加锁,只是做一个标记

  1. 线程使用synchronized时,进入{}并不会直接加锁,而是先设置标记(标记过程比较轻量,快速)
  2. 当程序运行在{}中时,如果没有其他线程来竞争锁,则其会一直保持偏向锁的状态,直到"}"结束解除标记(解除标记也快);
  3. 有其他线程来竞争锁时,其会抢先一步将偏向锁升级为轻量级锁,使得另一个线程只能阻塞/忙等;
  4. 自旋锁会统计发生冲突的频率,如果冲突频率到达一定程度,则会从轻量级锁升级到重量级锁
    因为自旋锁发生冲突是采用的是忙等 ,会一直占用CPU资源,如果自旋锁线程多了,大家都忙等,显然消耗的资源就非常多,因此将其升级为重量级锁,重量级锁冲突时,会阻塞等待让出CPU资源,让其他线程先做,更加节省资源;

对于当前的JVM版本来说,锁升级是单向的,只能升不能降;

3.优化策略

除了如上的优化策略,synchronized还存在着其他的优化策略。

锁消除 :是编译器优化的一种。代码中加了锁,但编译器会自行判断这个"加锁"是否存在意义,如果没有意义,就会将这个锁消除;

编译器只会在非常确定无意义加锁的情况下才进行锁消除; 即便如此,即使有些锁无意义,但编译器也不一定能够消除,因此仍需要程序员自己判断;
锁粒度: 锁的粒度指加锁和解锁间包含的代码数 (实际代码数,包含了调用的代码数)。代码越多,锁粒度越粗,代码越少,锁粒度越细
锁粗化,是将细粒度的锁合并成更加粗粒度的锁;

java 复制代码
//多个细粒度,每次加锁都可能出现阻塞!!!
synchronized(locker){
	x++;
}
synchronized(locker){
	x++;
}
synchronized(locker){
	x++;
}
//合并成粗粒度,阻塞的概率更低!!!
synchronized(locker){
	x++;
	x++;
	x++;
}

三.ReentrantLock

ReentrantLock是一把可重入锁 。其为传统的锁,即通过 lock和unlock进行加锁和解锁

java 复制代码
private static int count=0;  
public static void main(String[] args) throws InterruptedException {  
    ReentrantLock locker=new ReentrantLock();  
    Thread t=new Thread(()->{  
        for(int i=0;i<=100000;i++){  
            try{  
                locker.lock();  
                count++;  
            //使用finally来避免忘记释放锁的情况
            }finally {  
                locker.unlock();  
            }  
        }  
    });  
    t.start();   
    t.join();  
    System.out.println(count);  
}

synchronized和ReentrantLock的区别:

  • synchronized可用来修饰方法和代码块,ReentrantLock只能用在代码块上
  • synchronized使用代码块来加/解锁,ReentrantLock使用lock/unlock来加/解锁
  • synchronized发生锁竞争时,对应线程阻塞;ReentrantLock提供了tryLock方法可以设置超时时间(甚至可以没有超时时间),可以转而去处理其他事情;
  • ReentratLock还提供了公平锁的实现,可以通过构造方法(true公平,false非公平)切换;
  • synchronized是Java的关键字,其底层是JVM通过C++实现的;ReentrantLock是Java标准库的类,核心逻辑是在Java层面实现的(底层调用C++的逻辑,调用操作系统api完成系统层面的加锁);
  • synchronized的等待通知是基于Object类的wait和notify方法;ReentrantLock则是搭配Condition类来实现的;
相关推荐
黎雁·泠崖2 小时前
C 语言的内存函数:memcpy/memmove/memset/memcmp 精讲(含模拟实现)
c语言·开发语言
aini_lovee2 小时前
基于C# 和 NModbus 库的 Modbus TCP 通信示例源码
开发语言·tcp/ip·c#
吃喝不愁霸王餐APP开发者2 小时前
使用Mockito与WireMock对美团霸王餐接口进行契约测试与集成验证
java·json
明洞日记2 小时前
【设计模式手册023】外观模式 - 如何简化复杂系统
java·设计模式·外观模式
独自归家的兔2 小时前
面试实录:三大核心问题深度拆解(三级缓存 + 工程规范 + 逻辑思维)
java·后端·面试·职场和发展
HUST2 小时前
C 语言 第八讲:VS实用调试技巧
运维·c语言·开发语言·数据结构·算法·c#
毕设源码-郭学长2 小时前
【开题答辩全过程】以 共享单车后台管理系统为例,包含答辩的问题和答案
java·开发语言·tomcat
北城以北88882 小时前
SpringBoot--SpringBoot集成RabbitMQ
java·spring boot·rabbitmq·java-rabbitmq
hqwest2 小时前
码上通QT实战01--创建项目
开发语言·qt·sqlite3·qt项目·qwidget·qwindow