并发编程的关键——LOCK

news2025/1/17 3:02:12

并发编程的关键——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;
         //将此节点排入队列的线程。在构造时初始化,使用后为null
         volatile 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) // overflow
                    throw 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());
            else
                acquire(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 GC
                    failed = 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.coloradmin.cn/o/960381.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uniapp项目实战系列(4):服务的异步请求,请求服务的二次封装

目录 系列往期文章&#xff08;点击跳转&#xff09;uniapp项目实战系列(1)&#xff1a;导入数据库&#xff0c;启动后端服务&#xff0c;开启代码托管&#xff08;点击跳转&#xff09;uniapp项目实战系列(2)&#xff1a;新建项目&#xff0c;项目搭建&#xff0c;微信开发工具…

Greenplum-segment镜像分布策略

Greenplum作为一款基于PostgreSQL的OLAP分布式MPP架构&#xff0c;其内部的角色可以通过配置冗余来保证高可用性&#xff0c;无论是管理节点还是计算节点。管理节点可以为Master配置一个Standby来保证高可用&#xff0c;而计算节点则可以为每个Primary segment配置一个对应的Mi…

Linux测开常用命令总结

文章目录 Linux系统中文件目录树 基本指令的使用&#xff1a; Linux命令的帮助信息查看 --help command --help 说明&#xff1a; 显示command 命令的帮助信息通过man命令查看帮助信息 man command( 命令的名称) man 命令查看的帮助信息更加详细ls&#xff0c;pwd&#xff0c…

Java 多线程系列Ⅱ(线程安全)

线程安全 一、线程不安全线程不安全的原因&#xff1a; 二、线程不安全案例与解决方案1、修改共享资源synchronized 使用synchronized 特性 2、内存可见性Java内存模型&#xff08;JMM&#xff09;内存可见性问题 3、指令重排列4、synchronized 和 volatile5、拓展知识&#xf…

Python学习 -- 异常堆栈追踪技术

在编写Python代码时&#xff0c;出现异常是不可避免的。异常堆栈追踪是一种强大的工具&#xff0c;可以帮助我们定位错误发生的位置以及调用栈信息。Python的traceback模块提供了多种方法来获取和展示异常的堆栈信息。本文将详细介绍traceback模块中的print_exc()方法&#xff…

spring:事务失效+事务传播行为

一、事务失效 1.Transactional作用在非public上 Transactionalvoid transferAccounts(){adminDao.sub();System.out.println(10/0);adminDao.add();} 只执行sub&#xff08;&#xff09; 2.异常被try catch捕获 Transactionalpublic void transferAccounts(){adminDao.sub(…

vue3项目导入异常Error: @vitejs/PLUGIN-vue requires vue (>=3.2.13)

vue3项目导入异常 1、异常提示如下&#xff1a; failed TO LOAD config FROM D:\ws-projects\vite.co nfig.js error WHEN STARTING dev SERVER: Error: vitejs/PLUGIN-vue requires vue (>3.2.13) OR vue/compiler-sfc TO be pre sent IN the dependency tree.2、解决办法…

校园气象站的功能和作用

校园气象站是一种用于监测和记录气象数据的设备&#xff0c;下面将从功能和作用两个方面来详细介绍校园气象站。 一、校园气象站的功能 ①气象数据监测 校园气象站可以监测多种气象数据&#xff0c;包括温度、湿度、气压、风速、风向等。这些数据会通过4G方式上传至环境监控…

金蝶云星空二开,公有云执行SQL

功能背景&#xff1b; 金蝶公有云执行sql工具&#xff0c;因官方为云部署 用户无法连接数据库增删改查 天梯维护网页仅支持增删改操作 二开单据已支持根据sql动态生成单据体 与sql可视化界面操作一致 功能实现及场景&#xff1a; 1.可用于公有云执行sql类操作 2.私有云部署&am…

OB Cloud助力泡泡玛特打造新一代分布式抽盒机系统

作为中国潮玩行业的领先者&#xff0c;泡泡玛特凭借 MOLLY、DIMOO、SKULLPANDA 等爆款 IP&#xff0c;以及线上线下全渠道营销收获了千万年轻人的喜爱&#xff0c;会员数达到 2600 多万。2022 年&#xff0c;泡泡玛特实现 46.2 亿元营收&#xff0c;其中线上渠道营收占比 41.8%…

java 浅谈ThreadLocal底层源码(通俗易懂)

目录 一、ThreadLocal类基本介绍 1.概述 : 2.作用及特定 : 二、ThreadLocal类源码解读 1.代码准备 : 1.1 图示 1.2 数据对象 1.3 测试类 1.4 运行测试 2.源码分析 : 2.1 set方法解读 2.2 get方法解读 一、ThreadLocal类基本介绍 1.概述 : (1) ThreadLocal&#xff0c;本…

YOLO数据集划分(训练集、验证集、测试集)

1.将训练集、验证集、测试集按照7:2:1随机划分 1.项目准备 1.在项目下新建一个py文件&#xff0c;名字就叫做splitDataset1.py 2.将自己需要划分的原数据集就放在项目文件夹下面 以我的为例&#xff0c;我的原数据集名字叫做hatDataXml 里面的JPEGImages装的是图片 Annota…

安达发|APS软件排程规则及异常处理方案详解

随着科技的发展&#xff0c;工业生产逐渐向智能化、自动化方向发展。APS(高级计划与排程)软件作为一种集成了先进技术和理念的工业软件&#xff0c;可以帮助企业实现生产过程的优化和控制。其中&#xff0c;排程规则是APS软件的核心功能之一&#xff0c;它可以帮助企业合理安排…

港联证券|什么是北上资金?北上资金连续流入的股票好不好?

一般在股市收盘之后&#xff0c;公司会对当日的股市资金变化做一个资金总结&#xff0c;比如说北上资金的流入或许流出。那么什么是北上资金&#xff1f;北上资金连续流入的股票好不好&#xff1f;下面就由港联证券为大家剖析&#xff1a; 什么是北上资金&#xff1f; 北上资金…

Java on VS Code 8月更新|反编译器用户体验优化、新 Maven 项目工作流、代码高亮稳定性提升

作者&#xff1a;Nick Zhu 排版&#xff1a;Alan Wang 大家好&#xff0c;欢迎来到 Visual Studio Code for Java 的 8 月更新&#xff01;在这篇博客中&#xff0c;我们将为您提供有关反编译器支持的更多改进。此外&#xff0c;我们将展示如何创建没有原型的 Maven 项目以及一…

LabVIEW计算测量路径输出端随机变量的概率分布密度

LabVIEW计算测量路径输出端随机变量的概率分布密度 今天&#xff0c;开发算法和软件来解决计量综合的问题&#xff0c;即为特定问题寻找最佳测量算法。提出了算法支持&#xff0c;以便从计量上综合测量路径并确定所开发测量仪器的测量误差。测量路径由串联的几个块组成&#x…

用于设计和分析具有恒定近心点半径的低推力螺旋轨迹研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

ShardingJDBC——基于JPA的读写分离实战

摘要 本博文主要介绍基于JPA的读写分离实战&#xff0c;帮助大家更好的学会使用读写分离。透明化读写分离所带来的影响&#xff0c;让使用方尽量像使用一个数据库一样使用主从数据库集群&#xff0c;是ShardingSphere读写分离模块的主要设计目标。 一、读写分离库的场景和设计…

一文了解tcp/ip协议的运行原理

接触代理ip的人都了解https/sock5等ip协议&#xff0c;那么TCP/IP 协议又是什么&#xff1f; 一、什么是TCP/IP 协议&#xff1f; TCP/IP 协议实际上是一系列网络通信协议的一个统称&#xff0c;他负责具体的数据传输工作&#xff0c;核心的两个协议包括TCP以及IP&#xff0c…

启动服务报错:Command line is too long Shorten command line for xxx or also for Spri

ommand line is too long. Shorten command line for ProjectApprovalApplication or also for Spring Boot default configuration. 启动springboot 项目的时候报错 解决方案&#xff1a; 点击提示中的&#xff1a;default&#xff1a;然后在弹出窗口中选择&#xff1a;JAR xx…