文章目录
- 1.java本地自带锁介绍及应用
- synchronized
- (1)synchronized原理和优化
- (2)synchronized作用
- (3)synchronized的使用
- CAS
- (1) CAS原理
- (2)CAS和synchronized优缺点
- lock
- 2.分布式锁介绍及应用
- (1)数据库的分布式锁
- (3) zk做分布式锁
- (4)分布式锁的对比
1.java本地自带锁介绍及应用
synchronized
(1)synchronized原理和优化
在理解锁实现原理之前先了解一下Java的对象头和Monitor,在JVM中,对象是分成三部分存在的:对象头、实例数据、对其填充。
实例数据和对其填充与synchronized无关,这里简单说一下(我也是阅读《深入理解Java虚拟机》学到的,读者可仔细阅读该书相关章节学习)。实例数据存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐;对其填充不是必须部分,由于虚拟机要求对象起始地址必须是8字节的整数倍,对齐填充仅仅是为了使字节对齐。
对象头是我们需要关注的重点,它是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word 和 Class Metadata Address组成,其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。
锁也分不同状态,JDK6之前只有两个状态:无锁、有锁(重量级锁),而在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁,其中无锁就是一种状态了。锁的类型和状态在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。
每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。
从最近几个jdk版本中可以看出,Java的开发团队一直在对synchronized优化,其中最大的一次优化就是在jdk6的时候,新增了两个锁状态,通过锁消除、锁粗化、自旋锁等方法使用各种场景,给synchronized性能带来了很大的提升。
锁的膨胀
上面讲到锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。
偏向锁
一句话总结它的作用:减少统一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁。
核心思想:
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。
轻量级锁
轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。
重量级锁
重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。
锁粗化
锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁
自旋锁与自适应自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。
自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。
(2)synchronized作用
(1)、原子性:所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放。
(2)、可见性:**可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。 **synchronized和volatile都具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中,保证资源变量的可见性。
(3)、有序性:有序性值程序执行的顺序按照代码先后执行。 synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
(3)synchronized的使用
Synchronized主要有三种用法:
(1)、修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
synchronized void method() {//业务代码
}
(2)、修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
synchronized void staic method() {//业务代码
}
(3)、修饰代码块 :指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁
synchronized(this) {//业务代码
}
简单总结一下:
synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁。
synchronized 关键字加到实例方法上是给对象实例上锁。
接下来看一个 synchronized 使用经典实例—— 线程安全的单例模式:
public class Singleton {//保证有序性,防止指令重排private volatile static Singleton uniqueInstance;private Singleton() {}public static Singleton getUniqueInstance() {//先判断对象是否已经实例过,没有实例化过才进入加锁代码if (uniqueInstance == null) {//类对象加锁synchronized (Singleton.class) {if (uniqueInstance == null) {uniqueInstance = new Singleton();}}}return uniqueInstance;}
}
CAS
(1) CAS原理
CAS是什么:
CAS全称为Compare-And-Swap比较并交换,它是一条CPU并发原语。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。
原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
CAS底层原理是通过 Unsafe类(Native方法) + CAS思想(自旋锁) 实现的
CAS实现:
UnSafe类(Native方法)
UnSafe类是CAS的核心类,由于Java方法无法直接访问底层系统,则需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。
Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都是直接调用操作系统底层资源执行相应任务
CAS( CompareAndSwap):比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作否则继续比较直到主内存和工作内存中的值一致为止。
CAS应用:CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做
(2)CAS和synchronized优缺点
为什么使用CAS,不用synchronized?
CAS:保证了一致性,又兼顾了并发性
synchronized:只保证了一致性
CAS缺点:
循环时间长开销很大
只能保证一个共享变量的原子操作
引出ABA问题
ABA问题,原子引用更新?
AtomicInteger:CAS --> Unsafe --> CAS底层原理 --> ABA问题 --> 原子引用更新 --> 如何规避ABA问题
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差中会导致数据的变化
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
原子引用,AtomicReference
解决方案:带版本号(时间戳)的原子引用, AtomicStampedReference
lock
Lock 是 java.util.concurrent.locks 包 下的接口,Lock 实现提供了比 synchronized 关键字 更广泛的锁操作,它能以更优雅的方式处理线程同步问题。Lock提供了比synchronized更多的功能。
1.Lock和ReadWriteLock是两大锁的根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock。
2.Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。
3.ReadWriteLock 接口以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现 ReentrantReadWriteLock。
4 .Lock是可重入锁,可中断锁,可以实现公平锁和读写锁,写锁为排它锁,读锁为共享锁。ReentrantLock也是一种排他锁
使用:
private static Lock lock = new ReentrantLock();public static void main(String[] args) {lock.lock();try{System.out.println("获取锁成功!!");}catch(Exception e){e.printStackTrace();}finally{System.out.println("释放锁成功");lock.unlock();}}
synchonized和lock的区别:
1.lock(接口级别)需要开发者手动操作锁(加/释放);而 synchronized 是 JVM 层面提供的锁,自动进行加锁和释放锁操作,对于开发者是无感的。
2.Lock 只能修饰代码块;而 synchronized 可以修饰普通方法、静态方法和代码块。
3.锁类型不同:Lock 默认是非公平锁,但可以指定为公平锁;而 synchronized 只能是非公平锁。
4.synchronized 是可重入的,也就是说,线程可以多次获得同一个监视器锁。Lock 也是可重入的,但是它需要显式地调用 lock() 和 unlock() 方法来获取和释放锁。
5.synchronized 是Java关键字,用于实现同步代码块或方法。Lock 是一个接口,定义了锁的基本操作,如获取锁和释放锁。
2.分布式锁介绍及应用
(1)数据库的分布式锁
在MySQL中,FOR UPDATE 是一种锁定行的方式,通常与SELECT语句一起使用,用于在读取数据的同时锁定所选的行,以防止其他事务对这些行进行修改或删除,从而实现简单的行级锁。
下面是一个简单的示例,演示了如何在MySQL中使用 FOR UPDATE:
假设有一个表 employees 包含员工信息,我们要查询一个员工的信息并对其进行锁定以进行后续的更新操作。
-- 创建示例表
CREATE TABLE employees (id INT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(100),salary DECIMAL(10, 2)
);-- 插入示例数据
INSERT INTO employees (name, salary) VALUES ('Alice', 50000.00), ('Bob', 60000.00);-- 查询并锁定某个员工的信息
START TRANSACTION;
SELECT * FROM employees WHERE id = 1 FOR UPDATE;
-- 执行后续的更新操作
UPDATE employees SET salary = 55000.00 WHERE id = 1;
COMMIT;
在上面的示例中,SELECT * FROM employees WHERE id = 1 FOR UPDATE; 语句查询了 employees 表中 id 为1的员工信息,并在读取数据的同时锁定了这行数据。之后,我们可以在同一个事务中对该行数据进行更新操作。
需要注意的是,使用 FOR UPDATE 会在读取数据时对所选行进行排他锁定,因此其他事务将无法修改或删除这些行,直到当前事务释放了这些锁。此外,FOR UPDATE 通常与事务一起使用,以确保在执行锁定和后续操作期间的一致性。
##(2) redis来做分布式锁
分布式锁的实现:
1、使用命令介绍:
(1)SETNX
SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
(2)expire
expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
(3)delete
delete key:删除key。
在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。
2、实现思想:
(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁。锁的value值为一个随机生成的UUID,通过此UUID在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;public class RedisDistributedLock {private Jedis jedis;private String lockKey;private String lockValue;private int expireTime; // 锁的过期时间,单位:秒public RedisDistributedLock(Jedis jedis, String lockKey, String lockValue, int expireTime) {this.jedis = jedis;this.lockKey = lockKey;this.lockValue = lockValue;this.expireTime = expireTime;}public boolean acquireLock() {// 尝试获取锁String result = jedis.set(lockKey, lockValue, SetParams.setParams().nx().ex(expireTime));return "OK".equals(result);}public void releaseLock() {// 释放锁jedis.del(lockKey);}public static void main(String[] args) {// 创建Jedis客户端连接Jedis jedis = new Jedis("localhost", 6379);// 创建分布式锁实例RedisDistributedLock lock = new RedisDistributedLock(jedis, "my_lock", "lock_value", 10);// 尝试获取锁if (lock.acquireLock()) {try {// 获取到锁后执行业务逻辑System.out.println("获取到分布式锁,开始执行业务逻辑...");// 模拟业务逻辑执行时间Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();} finally {// 执行完业务逻辑后释放锁lock.releaseLock();System.out.println("释放分布式锁");}} else {System.out.println("未能获取到分布式锁");}// 关闭Jedis连接jedis.close();}
}
看门狗实现:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;import java.util.concurrent.TimeUnit;public class RedisDistributedLockWithWatchdog {private Jedis jedis;private String lockKey;private String lockValue;private int expireTime; // 锁的过期时间,单位:秒private boolean isLocked = false;private Thread watchdogThread;public RedisDistributedLockWithWatchdog(Jedis jedis, String lockKey, String lockValue, int expireTime) {this.jedis = jedis;this.lockKey = lockKey;this.lockValue = lockValue;this.expireTime = expireTime;}public boolean acquireLock() {// 尝试获取锁String result = jedis.set(lockKey, lockValue, SetParams.setParams().nx().ex(expireTime));if ("OK".equals(result)) {isLocked = true;startWatchdogThread();return true;}return false;}public void releaseLock() {jedis.del(lockKey);isLocked = false;stopWatchdogThread();}private void startWatchdogThread() {watchdogThread = new Thread(() -> {try {while (isLocked) {// 每隔一段时间续约锁jedis.expire(lockKey, expireTime);TimeUnit.SECONDS.sleep(expireTime / 2);}} catch (InterruptedException e) {e.printStackTrace();}});watchdogThread.start();}private void stopWatchdogThread() {if (watchdogThread != null && watchdogThread.isAlive()) {watchdogThread.interrupt();}}public static void main(String[] args) {// 创建Jedis客户端连接Jedis jedis = new Jedis("localhost", 6379);// 创建分布式锁实例RedisDistributedLockWithWatchdog lock = new RedisDistributedLockWithWatchdog(jedis, "my_lock", "lock_value", 10);// 尝试获取锁if (lock.acquireLock()) {try {// 获取到锁后执行业务逻辑System.out.println("获取到分布式锁,开始执行业务逻辑...");// 模拟业务逻辑执行时间TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();} finally {// 执行完业务逻辑后释放锁lock.releaseLock();System.out.println("释放分布式锁");}} else {System.out.println("未能获取到分布式锁");}// 关闭Jedis连接jedis.close();}
}
(3) zk做分布式锁
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryNTimes;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;/*** 分布式锁Zookeeper实现**/
@Slf4j
@Component
public class ZkLock implements DistributionLock {private String zkAddress = "zk_adress";private static final String root = "package root";private CuratorFramework zkClient;private final String LOCK_PREFIX = "/lock_";@Beanpublic DistributionLock initZkLock() {if (StringUtils.isBlank(root)) {throw new RuntimeException("zookeeper 'root' can't be null");}zkClient = CuratorFrameworkFactory.builder().connectString(zkAddress).retryPolicy(new RetryNTimes(2000, 20000)).namespace(root).build();zkClient.start();return this;}public boolean tryLock(String lockName) {lockName = LOCK_PREFIX+lockName;boolean locked = true;try {Stat stat = zkClient.checkExists().forPath(lockName);if (stat == null) {log.info("tryLock:{}", lockName);stat = zkClient.checkExists().forPath(lockName);if (stat == null) {zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(lockName, "1".getBytes());} else {log.warn("double-check stat.version:{}", stat.getAversion());locked = false;}} else {log.warn("check stat.version:{}", stat.getAversion());locked = false;}} catch (Exception e) {locked = false;}return locked;}public boolean tryLock(String key, long timeout) {return false;}public void release(String lockName) {lockName = LOCK_PREFIX+lockName;try {zkClient.delete().guaranteed().deletingChildrenIfNeeded().forPath(lockName);log.info("release:{}", lockName);} catch (Exception e) {log.error("删除", e);}}public void setZkAddress(String zkAddress) {this.zkAddress = zkAddress;}
}
(4)分布式锁的对比
数据库分布式锁实现
缺点:
1、db操作性能较差,并且有锁表的风险。
2、非阻塞操作失败后,需要轮询,占用cpu资源。
3、长时间不commit或者长时间轮询,可能会占用较多连接资源。
Redis(缓存)分布式锁实现
缺点:
1、锁删除失败,过期时间不好控制。
2、非阻塞,操作失败后,需要轮询,占用cpu资源。
ZK分布式锁实现
缺点:
性能不如redis实现,主要原因是写操作(获取锁释放锁)都需要在leader上执行,然后同步到follower。
总之:ZooKeeper有较好的性能和可靠性。
从理解的难易程度角度(从低到高):数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高):Zookeeper >= 缓存 > 数据库
从性能角度(从高到低):缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低):Zookeeper > 缓存 > 数据库