并发编程的关键——LOCK

news/2024/11/28 8:38:39/

并发编程的关键——LOCK

  • 锁的分类
  • synchronized
    • 万物即可为锁
    • synchronized的实现
    • 锁升级
  • Lock
    • AQS
      • LockSupport
      • CLH
      • CAS
  • Lock实现
    • ReentrantLock
    • 阻塞方法acquire
    • ReadWriteLock
      • ReentrantReadWriteLock
      • StampedLock

锁的分类

  • 公平锁/非公平锁:
    – 公平的意思是多个线程按照申请锁的顺序来获取锁。
    – 公平锁通常会造成系统更低的吞吐量
    – 非公平锁可能会出现线程饥饿的现象
    – ReentranLock:默认是非公平锁,可以通过构造参数指定为公平锁。
    – synchronized:默认是非公平锁,并且不能变为公平锁。
  • 独享锁/共享锁:
    – 独享的意思是一个锁只能被一个线程持有。
    – ReentranLock是独享锁
    – ReadWriteLocK的读锁是共享锁,写锁是独享锁。
    – synchronized是独享锁
  • 互斥锁/读写锁: 独享锁和共享锁的具体实现。
  • 乐观锁/悲观锁:
    – 乐观锁认为数据不一定会被修改,所以不会加锁,而是不断尝试更新。
    – 悲观锁认为数据会被修改,所以采用加锁的方式实现同步。
    – 乐观锁适合读操作多余写操作的系统。
  • 分段锁:一种锁的设计,例如ConcurentHashMap就使用了分段锁。
  • 偏向锁/轻量级锁/重量级锁:synchronized的三种状态
    – 偏向锁指同一段同步代码一直被一个线程访问,那么该线程就会自动获取这个锁,以降低获取锁的代价。
    – 轻量级锁:当前锁是偏向锁,并且被另一个线程访问,偏向锁会升级成轻量级锁,其他线程会通过自旋(CAS)的形式尝试获取锁。不会阻塞其他线程。
    – 重量级锁:当前锁是轻量级锁,另一个线程自旋到一定次数的时候还没获取到该锁,轻量级锁就会升级为重量级锁,会阻塞其他线程。
  • 自旋锁/调度锁:
    – 自旋锁:反复参数直到满足条件。
    – 调度锁:跟系统调度机构交互,实现并发控制。
  • 可重入锁:指同一线程再外层方法获取锁的时候,进入内层方法时会自动获取锁。

synchronized

synchronized是基于JVM实现的锁。

万物即可为锁

//给对象上锁----锁对象
synchronized Object obj = new Object();
//给方法上锁---锁对象
public synchronized void function(){}
//给静态方法上锁---锁class对象
public synchronized static void funciton(){}

同一个实例的两个方法都有synchronize锁,当方法1被一个线程上锁后,另一个线程能进入方法2吗?

方法上加锁,锁的是对象,静态方法上加锁,锁的是class对象,既然如此,答案肯定是不能进入。代码验证:

public class LockService {public synchronized void lock1(){System.out.println(Thread.currentThread().getName()+"进入lock1");try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"离开lock1");}public synchronized void lock2(){System.out.println(Thread.currentThread().getName()+"进入lock2");try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"离开lock2");}
}public class LockTest {public static void main(String[] args) {LockService lockService = new LockService();new Thread(lockService::lock1).start();new Thread(lockService::lock2).start();System.out.println("等待任务执行完成");}
}Thread-0进入lock1
等待任务执行完成
Thread-0离开lock1
Thread-1进入lock2
Thread-1离开lock2

疑问? synchronized(this) {}是锁代码块?这不是锁当前对象吗?

synchronized的实现

synchronized是通过对象头的锁标志位实现对象加锁的。
在这里插入图片描述
1.5以前synchronized是重量级锁,悲观锁,来就会加锁。
1.6以后优化了,有了锁升级的过程。

锁升级

1.6以后被synchronized标记的对象存在一个锁升级的过程,依次是:
在这里插入图片描述

注意:锁升级是按无锁>偏向锁>轻量级锁>重量级锁进行升级的,并且除了偏向锁可以重置为无锁外,锁是无法降级的。

Lock

lock是一个接口,用代码来实现锁。
在这里插入图片描述

AQS

AQS是jdk种Lock的实现基础,AQS使用模板方法模式,提供一个框架来实现依赖于FIFO等待队列的阻塞锁和相关的同步器(信号量、事件等),底层实现是volatile修饰的state和CAS。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {/*** 等待队列的NODE节点类,等待队列是LCH锁队列的一种变种。CLH锁通常用于自旋锁。* 相反,等待队列将它们用于阻塞同步器,但使用相同的基本策略,即在上一节点中保留有关线程的一些控制信息。* 当一个线程在等待某个资源时,它会被放入等待队列中,等待队列中的每个元素都是一个NODE对象。*      +------+  prev +-----+       +-----+* head |      | <---- |     | <---- |     |  tail*     +------+       +-----+       +-----+*/static final class Node{/** 用于指示节点正在共享模式下等待的标记 */static final Node SHARED = new Node();/** 用于指示节点正在独占模式下等待的标记 */static final Node EXCLUSIVE = null;    /*** waitStatus是AQS的重要属性,表示当前节点的状态。有五种取值:* CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。* SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。* CONDITION(-2):表示节点在条件等待队列中,当其他线程调用了Condition的signal()方法之后,该节点就会从条件等待队列移动到同步队列中。* PROPAGATE(-3):表示当前节点需要被传播或者传递到下一个节点。*  当一个线程在获取资源时,如果当前节点不是头节点,并且前继节点的状态为SIGNAL或CONDITION,那么当前节点的状态将被更新为PROPAGATE。*  这个状态的作用是让当前节点能够将前继节点的状态传递下去,从而让下一个节点有机会获取资源。*  0:表示线程节点进入就绪状态,可以继续尝试获取锁了。*/volatile int waitStatus;//连接到前一个节点,当前节点的waitStatus检查依赖前一节点。入队时分配,出队时设为null(为了GC)	。volatile Node prev;//连接到下一节点,volatile Node next;//将此节点排入队列的线程。在构造时初始化,使用后为nullvolatile Thread thread;Node nextWaiter;}//等待队列的头和尾private transient volatile Node head;private transient volatile Node tail;/*** 同步状态,有三种状态:*  0:没有线程持有*  1:已被线程持有*  >1: 被线程多次持有* 当一个线程来尝试加锁时,会对state进行CASE操作,将state从0改为1,如果操作成功,则将线程信息设置成自己。如果操作失败,则会进入等待队列。如果线程需要释放锁时会对state进行减1,如果减1后为0,则会彻底释放锁,将加锁线程置为null,然后从等待队列唤醒下一个线程。*/private volatile int state;/*** Condition同样时一个队列,用于维护等待队列(条件队列)。Condition是基于Node实现的。* 每一个Condition都拥有一个等待队列(NODE队列),一个Lock可以有多个Condition,每一个Condition都是对应了一个单独的条件。*/public class ConditionObject implements Condition, java.io.Serializable {private transient Node firstWaiter;private transient Node lastWaiter;}
}

LockSupport

LockSupport通过许可(permit)实现线程挂起、挂起线程唤醒功能。许可可以理解为每个已启动线程维持的一个int类型状态位counter。线程分别通过执行LockSupport静态方法park()、unPark()方法来完成挂起、唤醒操作2。LockSupport通常不会被直接使用,更多是作为锁实现的基础工具类1。

CLH

CLH是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制1。

CAS

CAS全称为Compare And Swap,是unsafe的一个方法。CAS机制的原理是利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,该操作时原子的。
CAS操作包括三个操作数:内存位置V、预期原值A和新值B。当内存位置V的值与预期原值A相匹配时,将内存位置V的值设置为新值B,并且返回true;否则,不做任何操作,并返回false。

Lock实现

上面对AQS的概念还是比较模糊,下面将从锁的具体实现来理解下AQS

ReentrantLock

//提供锁实现的同步基础,分为以下公平和非公平版本。使用AQS状态表示锁上的挂起次数。private final Sync sync;abstract static class Sync extends AbstractQueuedSynchronizer {//……final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}}
//非公平锁static final class NonfairSync extends Sync {/*** 立即尝试获取锁,否则以独占锁形式进入队列获取锁。*/final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}//公平锁static final class FairSync extends Sync {/*** 直接进入队列获取锁*/final void lock() {acquire(1);}/*** Fair version of tryAcquire.  Don't grant access unless* recursive call or no waiters or is first.*/protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}}public void lock() {sync.lock();}public boolean tryLock() {return sync.nonfairTryAcquire(1);}

注意看lock()方法,非公平锁和公平锁的差异就是:

  • 非公平锁会在lock的时候不管三七二十一,先尝试获取锁一次,如果获取失败,则会再查看下state是不是0,如果是0,再抢一次锁。如果没抢到才会乖乖入队。
  • 而公平锁则是查看state是否为0,并且是否其他线程在等待,如果没有才会尝试加锁。

lock和tryLock的区别是:

  • lock:获取锁失败会阻塞。
  • tryLock:获取锁失败则返回false,成功则返回true。不会阻塞。

阻塞方法acquire

	/*********AQS*****************/public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}/***  在一个死循环里面,不断检测上一个节点的状态,直到上一个节点状态为SIGNAL则将当前线程挂起来等待唤醒或被中断*/final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}/***shouldParkAfterFailedAcquire只有检测到前一个节点状态为SIGNAL才会返回true,此时当前节点才能安全的挂起。*parkAndCheckInterrupt负责将当前线程挂起,并判断是否中断。如果线程未中断,则继续阻塞;如果线程中断,则抛出InterruptedException异常*/if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}

ReadWriteLock

ReadWriteLock读写锁是一个接口,定义了两个获取锁的接口readLockwriteLock,在jdk里面其有两个实现:
在这里插入图片描述

ReentrantReadWriteLock

ReentrantReadWriteLock故名思意,可重入读写锁。
读写锁的读锁和写锁的关系是什么?

  • 读锁之间不互斥,可多个线程同时获取;
  • 读锁与写锁互斥,即写锁已经被某个线程获取时,其他线程不能获取读锁;读锁被某个线程获取时,其他线程不能获取写锁。
  • 写锁之间互斥,即一次只能有一个线程获取写锁;
  • 这两种锁之间会有优先级,当有等待的线程时,写锁锁住优先于读锁锁住;

锁降级是什么?

锁降级是说一个线程在拥有写锁的情况下可以不释放写锁,直接获取读锁,然后再释放写锁。这个过程就完成了锁降级。锁降级的好处是可以在写操作后直接进行读操作,避免了无谓的锁竞争和线程阻塞,提高了程序的并发性能。需要注意的是,锁降级是可逆的,即读锁可以再升级为写锁。但是锁升级的操作会导致死锁的风险,因此在使用锁降级时需要谨慎处理锁的获取和释放顺序。

StampedLock

StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的读性能的优化。
StampedLock有一个stamp变量(戳记,long类型)代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值1。StampedLock有以下3种模式:

  • 悲观读锁。与ReadWriteLock的读锁类似(这里的读锁不可重入),多个线程可以同时获取悲观读锁,悲观读锁是一个共享锁。
  • 乐观读锁。相当于直接操作数据,不加任何锁,连读锁都不要。在操作数据前并没有通过CAS设置锁的状态,仅仅通过位运算测试。
  • 写锁。与ReadWriteLock的写锁类似,写锁和悲观读锁是互斥的。

http://www.ppmy.cn/news/1078767.html

相关文章

Android DataBinding 基础入门(学习记录)

目录 一、DataBinding简介二、findViewById 和 DataBinding 原理及优缺点1. findViewById的优缺点2. DataBinding的优缺点 三、Android mvvm 之 databinding 原理1. 简介和三个主要的实体DataViewViewDataBinding 2.三个功能2.1. rebind 行为2.2 observe data 行为2.3 observe …

堆的 shift up(Java 实例代码)

目录 堆的 shift up Java 实例代码 src/runoob/heap/HeapShiftUp.java 文件代码&#xff1a; 堆的 shift up 本小节介绍如何向一个最大堆中添加元素&#xff0c;称为 shift up。 假设我们对下面的最大堆新加入一个元素52&#xff0c;放在数组的最后一位&#xff0c;52大于父…

mysql连接查询与存储过程

一&#xff0c;连接查询 MySQL 的连接查询&#xff0c;通常都是将来自两个或多个表的记录行结合起来&#xff0c;基于这些表之间的 共同字段&#xff0c;进行数据的拼接。首先&#xff0c;要确定一个主表作为结果集&#xff0c;然后将其他表的行有选择 性的连接到选定的主表结果…

ViT论文Pytorch代码解读

ViT论文代码实现 论文地址&#xff1a;https://arxiv.org/abs/2010.11929 Pytorch代码地址&#xff1a;https://github.com/lucidrains/vit-pytorch ViT结构图 调用代码 import torch from vit_pytorch import ViTdef test():v ViT(image_size 256, patch_size 32, num_cl…

【前端demo】圣诞节灯泡 CSS动画实现轮流闪灯

文章目录 效果过程灯泡闪亮实现&#xff08;animation和box-shadow&#xff09;控制灯泡闪亮时间和顺序&#xff08;animation-delay&#xff09;按钮开关 代码htmlcssjs 参考代码1代码2 前端demo目录 效果 效果预览&#xff1a;https://codepen.io/karshey/pen/zYyBRWZ 参考…

spring AOP之代理

1.代理概念 什么是代理 为某一个对象创建一个代理对象&#xff0c;程序不直接用原本的对象&#xff0c;而是由创建的代理对象来控制原对象&#xff0c;通过代理类这中间一层&#xff0c;能有效控制对委托类对象的直接访问&#xff0c;也可以很好的隐藏和保护委托类对象&#x…

IBM安全发布《2023年数据泄露成本报告》,数据泄露成本创新高

近日&#xff0c;IBM安全发布了《2023年数据泄露成本报告》&#xff0c;该报告针对全球553个组织所经历的数据泄露事件进行深入分析研究&#xff0c;探讨数据泄露的根本原因&#xff0c;以及能够减少数据泄露的技术手段。 根据报告显示&#xff0c;2023年数据泄露的全球平均成…

ransac拟合平面,代替open3d的segment_plane

0.open3d打包太大了&#xff0c;所以决定网上找找代码 使用open3d拟合平面并且求平面的法向量&#xff0c;open3d打包大概1个g的大小。 import open3d as o3dpcd o3d.geometry.PointCloud()pcd.points o3d.utility.Vector3dVector(points)## 使用RANSAC算法拟合平面plane_m…