技术支持 中山网站建设,电商网站建设属于研发费用吗,建设网站需要什么资料,苏州建设厅网站一、概述
1.1 背景
概念#xff1a;锁是多线程编程中的机制#xff0c;用于控制对共享资源的访问。可以防止多个线程同时修改或读取共享资源#xff0c;从而保证线程安全。 作用#xff1a;锁用于实现线程间的互斥和协调#xff0c;确保在多线程环境下对共享资源的访问顺…一、概述
1.1 背景
概念锁是多线程编程中的机制用于控制对共享资源的访问。可以防止多个线程同时修改或读取共享资源从而保证线程安全。 作用锁用于实现线程间的互斥和协调确保在多线程环境下对共享资源的访问顺序和正确性。
1.2 锁分类
【按锁性质划分】
乐观锁认为一个线程获取共享数据的时候不会存在其他线程修改该共享数据的情况所以不会上锁。例如CAS机制、版本号机制等悲观锁认为一个线程获取共享数据时一定会存在其他线程修改该共享数据的情况因此获取共享数据时都会进行加锁。例如Synchronized锁、ReentrantLock锁。
【按锁被持有数量划分】
独占锁当前锁只有被一个线程持有。例如ReentrantLock锁共享锁当前锁可以被多个线程持有。例如Semaphore等。
【按公平性划分】
公平锁多个线程竞争锁时需要进行排队按照先来后到顺序获取锁。例如ReentrantLock公平锁。非公平锁多个线程竞争锁时先进行插队插入失败再排队。例如Synchronized锁、ReentrantLock锁
【按可重入性划分】
可重入锁允许一个线程多次加锁。例如Synchronized锁、ReentrantLock锁 。不可重入锁允许一个线程仅加锁一次。
【按锁范围划分】
单体锁仅能锁住当前JVM进程中的共享资源对其他JVM进程中的共享资源不起作用。例如 Synchronized锁和ReentrantLock锁分布式锁借助中间件对多个JVM进程中的同一共享资源都能锁住。例如Redis分布式锁。
二、单JVM进程锁
2.1 独占锁
2.1.1 synchronized锁
详情见深入解析Synchronized锁底层原理 局限性
是否释放锁开发者无法自己控制导致其他线程只能一直阻塞若获取锁的线程进入休眠或阻塞除了线程出现异常否则其他线程将会一直阻塞等待。 因此在JDK1.5后加入了Doug Lea大神贡献的java.util.concurrent包包内提供了Lock类提供了更加灵活控制锁的功能弥补了Synchronized的缺陷。
2.1.2 ReentrantLock锁
Lock完全是由Java编写提供了锁获取和释放的控制权、可中断的获取锁以及超时获取锁等多种高级特性。Lock只是一个接口常见的实现类有
1. 重入锁ReentrantLock;
2. 读锁ReadLock
3. 写锁WriteLock但底层都是通过聚合了一个java 同步器(AbstractQueueSynchronizer, AQS)来完成线程的访问控制的。因此需要提前了解AQS的底层原理。详情见深入解析AQS队列同步器的底层原理
ReentrantLock实现了Lock接口同时底层通过聚合AQS完成并发的功能【注意此时state只能为0或1】。主要有以下特点
1. 支持重进入的锁表示该锁能够支持一个线程对资源的重复加载同时还支持获取锁的公平和非公平性。
2. 构造方法会接收一个可选的公平参数默认是非公平锁。设置为true时表示公平锁否则为非公平锁。
3. 可重入性的体现任意线程获取锁之后再次获取该锁时不会被锁所阻塞。因为是可重入的有一个计数器记录重入次数n, 当n 0时表明锁完全被释放。ReentrantLock实现的公平锁和非公平锁的区别
1. 获取锁的时候是否按照FIFO的顺序来的。公平锁不仅会对state状态进行判断还会判断当前同步队列中是否有元素如果存在元素则插入到同步队列的尾部真正的先来后到
2. 非公平锁性能高于公平锁性能。非公平锁可以减少CPU唤醒线程的开销整体的吞吐率会高点CPU也不会唤醒所有的线程减少唤醒线程的数量。具体原因为
【公平锁获取锁】会将线程自己添加到等待队列的队尾并休眠当某线程用完锁之后会去唤醒等待队列中队首的线程尝试去获取锁锁的使用顺序也就是队列中的先后顺序。在整个过程中线程会从运行状态切换到休眠状态再从休眠状态恢复成运行状态但线程每次休眠和恢复都需要从用户态转换成内核态而这个状态的转换是比较慢的所以公平锁的执行速度会比较慢。
】非公平锁获取锁】当线程获取锁时会先通过 CAS 尝试获取锁如果获取成功就直接拥有锁如果获取锁失败才会进入等待队列等待下次尝试获取锁。这样做的好处是获取锁不用遵循先到先得的规则从而避免了线程休眠和恢复的操作这样就加速了程序的执行效率。
3. 非公平锁会存在线程饥饿的情况。但出现线程饥饿的机率非常低可以忽略不记。这就是默认非公平锁的原因。2.1.3 局限性
synchronized和ReentrantLock锁一次仅允许一个线程访问资源即属于独占性。对于多个线程同时访问共享资源的场景是无能为力的。不过Java也提供了对应的解决方案java Semaphore信号量、CountDownLatch以及CyclicBarrier共享锁。
2.2 共享锁
java Semaphore信号量、CountDownLatch以及CyclicBarrier共享锁底层的原理是相通的都是基于AQS队列同步器来实现的。相比于独占锁主要区别在于state值设置可由开发者进行控制这样就可以实现多个线程同时访问共享资源。AQS底层原理见深入解析AQS队列同步器的底层原理。
2.2.1 Semaphore
Semaphore信号量为多线程协作提供了更为强大的控制方法默认是非公平的。 常用场景限流尤其是公共资源有限的应用场景例如数据库连接停车场车位数等。
2.2.2 CountDownLatch
CountDownLatch称之为闭锁它可以使一个或一批线程在闭锁上等待等到其他线程执行完相应操作后闭锁打开这些等待的线程才可以继续执行。确切的说闭锁在内部维护了一个倒计数器。通过该计数器的值来决定闭锁的状态从而决定是否允许等待的线程继续执行是批量Join的实现方案。
2.2.3 CyclicBarrier
CyclicBarrier通常称为循环屏障。它和CountDownLatch很相似都可以使线程先等待然后再执行。不过CountDownLatch是使一批线程等待另一批线程执行完后再执行而CyclicBarrier只是使等待的线程达到一定数目后再让它们继续执行。故而CyclicBarrier内部也有一个计数器,计数器的初始值在创建对象时通过构造参数指定。 场景可循环使用的屏障。即等待一组线程到达一个屏障时被阻塞直到最后一个线程到达才会执行。例五个人一组玩游戏先到的进行等待直到凑齐五个人才开始执行任务。
2.2.4 CyclicBarrier与CountDownLatch的区别
CyclicBarrier的计数器可以重置而CountDownLatch不行这意味着CyclicBarrier实例可以被重复使用而CountDownLatch只能被使用一次CyclicBarrier还有getNumberWaiting()方法获取阻塞线程数量isBroken()方法用来了解阻塞的线程是否被中断。CountDownLatch指的是每个线程的主业务逻辑执行完成后再统一释放锁而CyclicBarrier指的是等指定数量线程准备好后再执行主业务逻辑。
2.3 总结
不管是独占锁还是共享锁解决的是共享资源的访问控制问题无法解决线程见的通信问题。对应的解决方案有
1. Synchronized锁配合Object的wait和notify等方法来实现线程通信;
2. ReentrantLock锁配合Condition实现多个条件下的线程通信。Condition的底层实现原理见深入解析Condition的底层实现原理。 上述相关的锁的实现底层都离不开CAS机制和Volatile。因此有必要了解CAS底层的实现原理详情见深入解析CAS的原理机制。
三、分布式锁
解决的问题保证一个方法在同一时间内只能被同一个线程执行在单体应用下单体锁只能锁住一个JVM进程其他进程不受影响显然是无法满足我们的要求的。要考虑非阻塞式分布式锁和阻塞式分布式锁要根据业务来进行考虑。 分布式锁的要求
保证在分布式部署的应用集群中同一个方法在同一时间只能被一台机器上的一个线程执行是一把可重入锁(防止死锁)是阻塞锁(根据业务考虑阻塞或非阻塞)高可用的获取锁和释放锁功能获取锁和释放锁的性能要好。
3.1 数据库分布式锁
多个进程多个线程访问共同组件数据库专门建立一个数据库一张表存放用户自定义锁。
3.1.1 基于数据库表
当想要锁住一个方法或资源时直接将方法或资源信息插入到表中, 同时在数据库层面对方法或资源信息添加唯一性约束这样当插入成功时,就表示获取到锁释放锁的时候直接删除信息即可。例如 锁信息表
CREATE TABLE methodLock (id int(11) NOT NULL AUTO_INCREMENT COMMENT 主键,method_name varchar(64) NOT NULL DEFAULT COMMENT 锁定的方法名,desc varchar(1024) NOT NULL DEFAULT 备注信息,update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 保存数据时间自动生成,PRIMARY KEY (id),UNIQUE KEY uidx_method_name (method_name ) USING BTREE
) ENG加锁
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)解锁
delete from methodLock where method_name method_name存在的问题
可用性强依赖数据库的可用性一旦数据库宕机会导致业务系统不可用自动释放由于无法设置失效时间一旦解锁失败那么其他线程将无法获取到锁阻塞性插入数据失败的线程会直接报错返回报错信息不会等待因此对某些业务来说是不可接受的可重入性该锁是非重入锁同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了
问题解决方法
问题1数据库集群部署但为了使用分布式锁多部署一个集群性价比低同时高并发情况下数据库会宕机问题2后台启动一个定时任务, 定期清理数据表无用的数据。某一时间点占用了大量的数据库连接问题3设置一个while循环直到insert成功。性能非常差产生大量无效insert行为问题4在数据库表中加个字段记录当前获得锁的机器的主机信息和线程信息那么下次再获取锁的时候先查询数据库如果当前机器的主机信息和线程信息在数据库可以查到的话直接把锁分配给他就可以了。
3.1.2 基于数据库排他锁
具体SQL语句
select ... for update
注意:
1要配合事务使用,才会有效使用事务将要加锁的地方包裹住,等执行完后,再进行提交。
因为如果select .. for update后就提交事务
2Innodb只针对根据索引查询来添加行锁,否则添加表级锁加锁
select ... for update解锁
应用层面自己实现事务的提交
public void unlock(){connection.commit();
}该中方式解决了基于数据库表的阻塞和无法释放锁的问题
阻塞性当select … for update时会被数据库阻塞住直到查询数据才会返回自动释放锁若数据库宕机会自动释放锁
存在的问题
单点故障以及可重入问题是否走索引不确定导致使用的是表锁MySql会对查询进行优化即便在条件中使用了索引字段但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的如果 MySQL 认为全表扫效率更高比如对一些很小的表它就不会使用索引这种情况下 InnoDB 将使用表锁而不是行锁数据库性能一个排他锁长时间不提交就会占用数据库连接。一旦类似的连接变得多了就可能把数据库连接池撑爆
注意 将作为锁的数据库与业务数据库分开。
3.2 基于缓存实现分布式锁
3.2.1 Redis分布式锁
使用Redis的setnx实现分布式锁。命令如下
set resource_name my_random_value NX PX 30000
resource_name: 资源名称,可根据不同业务区分不同的锁;
my_random_value: 随机值,每个线程的随机值都不同,用户释放锁时的校验。一般采用UUID
NXkey不存在时设置成功,key存在时则设置不成功
PX自动失效时间,出现异常情况,锁可以过期失效实现原理
利用NX的原子性多个线程并发时只有一个线程可以设置成功设置成功即可获得锁可以执行后续的业务处理如果出现异常过了锁的有效期锁自动释放释放锁采用delete命令释放锁时校验之前设置的随机值相同才释放;释放锁的LUA脚本【先校验后释放】。原因有A和B两个线程, 若A先获取锁,由于某些原因A超时了导致A的锁被释放此时B获取到了锁然后执行A释放锁的操作此时会释放掉B持有的锁。【产生并发问题所以释放和校验要使用LUA脚本来实现】
优点 可自动释放锁设置过期时间可靠性高集群部署 缺点 无法缓存层面实现阻塞只能应用层面实现无法实现可重入性 3.2.2 基于Redisson实现分布式锁
在Redis基础上利用Java对Redis客户端进行封装并对单体应用下的JDK并发包和JDK集合类等进行扩展提供分布式下的解决方案。
RLock lock redisson.getLock();3.3 基于Zookeeper分布式锁
3.3.1 基于Zookeeper的瞬时节点实现分布式锁
3.3.1.1 前言
取决于Zookeeper内部的命名空间模型结构。该命名空间模型类似于Linux文件结构采用树状结构各个节点被称为znode。每个节点可以存储路径以及与之相关的元数据还有子节点列表。
3.3.1.2 节点类型 3.3.1.3 基于临时顺序节点的分布式锁
核心思想临时顺序节点 Watch(观察器)机制 实现原理
多线程并发创建多个瞬时节点得到有序的瞬时节点列表选用序号最小的线程获取锁其他线程则利用Watch机制监听自己序号的前一个序号前一个线程执行完成删除自己序号的节点利用线程的wait和notify来阻塞和唤醒对象的线程获取锁。
优点 锁自动释放一旦zookeeper宕机或session断开瞬时节点就会被删除因此锁就被释放了可阻塞用Zookeeper可以实现阻塞的锁客户端可以通过在ZK中创建顺序节点并且在节点上绑定监听器一旦节点有变化Zookeeper会通知客户端客户端可以检查自己创建的节点是不是当前所有节点中序号最小的如果是那么自己就获取到锁便可以执行业务逻辑了。底层使用的是wait和notify机制因此是阻塞的 3.可重入客户端在创建节点的时候把当前客户端的主机信息和线程信息直接写入到节点中下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样那么自己直接获取到锁如果不一样就再创建一个临时的顺序节点参与排队单点被解决zookeeper通常是集群部署的。 3.3.2 基于Zookeeper的Curator客户端实现分布式锁
使用Java对Zookeeper客户端进行进一步封装并提供许多简单便利的功能比如分布式锁java InterProcessMutex。 缺点 频繁的创建和删除瞬时节点ZK中创建和删除节点只能通过Leader服务器来执行然后将数据同步到所有的Follower机器上性能有影响。
3.4 分布式锁对比
易于理解程度(从低到高)数据库 缓存 zookeeper
实现复杂度(从低到高)zookeeper 缓存 数据库
性能(从低到高): 缓存 zookeeper 数据库
可靠性(从低到高)数据库 缓存 zookeeper
不推荐使用自己编写的分布式锁推荐使用Redisson和Curator实现的分布式锁