线程锁
锁是并发编程中的一个重要概念,用于控制对共享资源的访问,以避免数据不一致和竞争条件;
synchronized
java提供的关键字,用来标记方法、代码块、类。 特点: 使用简单、悲观锁、颗粒度大、可重入
Lock
-
ReentranLock 特点: 可选择公平锁(先到先得)、灵活、可重入、悲观锁; 提供了Condition可用于线程间相互等待和唤醒 tryLock提供了阻塞和非阻塞的方式
-
ReentrantReadWriteLock
-
ReadLock : 共享锁,允许多个线程同时持有读锁,写锁被持有时,不可读
-
WriteLock : 排他锁,只有一个线程可以持有写锁,且会阻塞其他读锁线程;相反如果有读锁被持有时,获取写锁需要等待所有读锁释放
-
分布式锁
sync关键字和locks包都只适用于单机应用,如果是集群或分布式则需要借助一些中间件,常用的有redis和zookeeper
redis:
可以直接使用Redisson,只需要导入依赖并配置就可以直接使用。实现上主要有两点
-
使用lua脚本来保证加锁/解锁操作的原子性, 其中加锁涉及两个命令
hincrby和pexpire,使用lua脚本来执行避免hincrby成功而pexpire失败下导致死锁的问题 编辑编辑编辑 这里使用hash,用线程id作为hash的key,value为持有锁的次数;对应的unLock时,会对value进行-1操作,以此来实现可重入锁;需要注意有一次加锁就要有一次解锁
-
锁的续期,通过守护线程定时的延迟锁的过期时间,避免应用处理还未完成,锁就已经超时释放的场景;使用守护线程可以避免程序因为异常原因导致锁无法释放
zookeeper:
zk适合做为分布式锁,是使用其中的临时节点。临时节点的特性
-
自动删除:临时节点在创建它的客户端会话结束时会自动删除。这意味着当客户端崩溃或失去连接时,临时节点会被 ZooKeeper 自动清理。
-
不可有子节点:临时节点不能创建子节点,这是为了确保节点的树结构简单和清晰。
-
短生命周期:因为临时节点的生命周期与客户端会话相关联,所以它们通常被用来存储短期数据。
乐观锁
乐观锁的核心思想是“先做再检查”,例如 update table set i=1 where id =1 and i=2; 类似这样的思想 满足i=2时才提交更新;
适用场景:
-
读多写少: 在这种场景下,数据冲突的概率较低,乐观锁可以减少锁定时间
-
不需要严格一致性的数据: 对于某些数据,可以接受一定程度的重试和更新失败,乐观锁的开销和复杂度较低。
优点:
-
减少锁争用
-
提高并发性能
缺点:
-
适用场景有限: 在写多读少的场景下,冲突概率高,重试次数多,反而降低性能。
-
实现复杂: 需要在应用程序中处理冲突和重试逻辑,增加了代码复杂度。
CAS
CAS(Compare-And-Swap)是一种无锁(lock-free)的原子操作,用于在多线程环境中进行并发控制。它用于实现无锁数据结构和算法,以提高并发性能并避免传统锁的开销。CAS 的核心思想是通过比较和交换操作来确保数据的一致性。
CAS 操作包括三个主要步骤:
-
比较(Compare):检查内存位置的当前值是否与预期值相等。
-
交换(Swap):如果当前值与预期值相等,则将该内存位置的值更新为新值。
-
返回:返回操作的结果,指示操作是否成功(即是否执行了交换)。
例子:
AtomicInteger/ConcurrentLinkedQueue: 使用Unsafe提供的原子操作,至于底层有人说是通过C++库调用CPU指令集实现的;
volatile关键字
它用于保证多线程环境下的变量可见性和禁止指令重排序;
变量可见性:
正常线程修改一个变量时,修改结果会在线程本地缓存中立即生效,但在主内存中看到的可能还是旧值;
valatile则是保证该变量的值在被修改时,会立即被刷新到主内存中