关于java的一些锁事
一、锁的总览
从对待竞争的态度来分
:乐观锁、悲观锁从抢锁的态度来分
:公平锁、非公平锁从是否可重入来分
:可重入锁,不可重入锁- 独享锁、共享锁
- 互斥锁、读写锁
- synchronized 锁升级(无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁) JDK1.6
二、内置锁:synchronized
在Java中,synchronized 锁可能是我们最早接触的锁了,在 JDK1.5之前 synchronized
是一个重量级锁,相对于 juc 包中的Lock,synchronized 显得比较笨重。
庆幸的是在 JDK1.6 之后 Java 官⽅对从 JVM 层⾯对 synchronized
进行⼤优化,所以现在的 synchronized 锁效率也优化得很不错。
2.1 用法:
synchronized
关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁。
synchronized
关键字加到实例方法上是给对象实例
上锁。
2.2 Java对象结构
Java对象结构包括三部分:对象头、对象体和填充字节。
java对象结构图:
2.3 synchronized锁升级:
锁升级过程(无锁 ->
偏向锁 ->
轻量级锁 ->
重量级锁)
synchronized锁升级过程:
三、逃逸分析 Escape Analysis
我们知道Java对象是在堆里分配的,在调用栈中,只保存了对象的指针。当对象不再使用后,需要依靠GC来遍历引用树并回收内存,如果对象数量较多,将给GC带来较大压力。因此,减少临时对象在堆内存分配的数量是非常有效的优化方法。
3.1 逃逸分为两种:
- 方法逃逸:当一个对象在方法中被定义后,可能作为调用参数被外部方法说引用。
- 线程逃逸:通过复制给类变量或者作为实例变量在其他线程中可以被访问到。
3.2 逃逸分析相关优化:
如果证明一个对象不会逃逸方法外或者线程外,则可针对此变量进行一下三种优化:
- 栈上分配stack allocation:如果对象不会逃逸到方法外,则对此对象在栈上分配内存,则对象所占用的空间可以随栈出栈而别销毁。
- 同步消除synchronization Elimination:如果一个对象不会逃逸出线程,则对此变量的同步措施可消除。
- Scalar replacement:标量scalar是不可再分解的量,比如基本数据类型,聚合量Aggregate是可以在被分解的,比如java中的对象。标量替换是将一个聚合量拆散,根据程序对此聚合量的访问情况,将其使用到的成员变量恢复到原始变量来访问就是标量替换。
逃逸分析如果证明一个对象不会被外部访问,并且此对象可以被拆散,则程序执行时可能不会创建此对象。
3.3 参数开启
-XX:+DoEscapeAnalysis
开启逃逸分析-XX:+EliminateLocks
开启同步消除-XX:+EliminateAllocations
开启标量替换-XX:+PrintEscapeAnalysis
显示分析结果
3.4 锁消除和锁粗化:
3.4.1 锁消除:
锁消除是发生在编译器级别的一种锁优化方式。
锁消除即删除不必要的加锁操作。JVM在运行时,对一些“在代码上要求同步,但是被检测到不可能存在共享数据竞争情况”的锁进行消除。
根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么就可以认为这段代码是线程安全的,无需加锁。
public String method() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 10; i++) {
sb.append("i:" + i);
}
return sb.toString();
}
3.4.2 锁粗化:
假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。
public void doSomethingMethod(){
synchronized(lock){
//do some thing
}
//这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
synchronized(lock){
//do other thing
}
}
四、并发包:juc锁
JUC锁机制:
- 核心接口:Lock,ReadWriteLock;
- 抽象类
- AbstractOwnableSynchronizer (排它锁);
- AbstractQueuedSynchronizer (为实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架);
- AbstractQueuedLongSynchronizer (64位同步器)
- 工具类:
- Reentrantock互斥锁、ReadWriteLock读写锁、Condition控制队列
- LockSupport阻塞原语、Semaphore信号量、CountDownLatch闭锁
- CyclicBarrier栅栏、Exchanger交换机、CompletableFuture线程回调
五、volatile 关键字
5.1 主要作用
volatile 的主要作用有两点:
保证变量的内存可见性
禁止指令重排序
5.2 总线嗅探机制
嗅探机制工作原理:每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。
注意:
基于 CPU 缓存一致性协议,JVM 实现了 volatile 的可见性,但由于总线嗅探机制,会不断的监听总线,如果大量使用 volatile 会引起总线风暴。所以,volatile 的使用要适合具体场景。
5.3 volatile 在单例模式中的应用
懒汉式单例双重检测模式中就使用到了 volatile 关键字
public class Singleton {
// volatile 保证可见性和禁止指令重排序
private static volatile Singleton singleton;
public static Singleton getInstance() {
// 第一次检查
if (singleton == null) {
// 同步代码块
synchronized(this.getClass()) {
// 第二次检查
if (singleton == null) {
// 对象的实例化是一个非原子性操作
singleton = new Singleton();
}
}
}
return singleton;
}
}
上面代码中, new Singleton() 是一个非原子性操作,对象实例化分为三步操作:
(1)分配内存空间 ->
(2)初始化实例 ->
(3)返回内存地址给引用
所以,在使用构造器创建对象时,编译器可能会进行指令重排序。
假设线程 A 在执行创建对象时,(2)和(3)进行了重排序,如果线程 B 在线程 A 执行(3)时拿到了引用地址,并在第一个检查中判断 singleton != null
了,但此时线程 B 拿到的不是一个完整的对象,在使用对象进行操作时就会出现问题。
所以,这里使用 volatile 修饰 singleton 变量,就是为了禁止在实例化对象时进行指令重排序。
六、分布式锁
6.1 概述
分布式的 CAP 理论告诉我们:任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。
6.2 什么是分布式锁?
分布式锁是指分布式环境下,系统部署在多个机器中,实现多进程分布式互斥的一种锁。为了保证多个进程能看到锁,锁被存在公共存储(比如 Redis、Memcache、数据库等三方存储中),以实现多个进程并发访问同一个临界资源,同一时刻只有一个进程可访问共享资源,确保数据的一致性。
简言之:分布式锁就是多个JVM之间共享的锁
6.3 分布式锁一般有如下的特点:
- 互斥性: 同一时刻只能有一个线程持有锁
- 可重入性: 同一节点上的同一个线程如果获取了锁之后能够再次获取锁
- 锁超时:和J.U.C中的锁一样支持锁超时,防止死锁
- 高性能和高可用: 加锁和解锁需要高效,同时也需要保证高可用,防止分布式锁失效
- 具备阻塞和非阻塞性:能够及时从阻塞状态中被唤醒
6.4 基于redis的分布式锁:
- 指定一个 key 作为锁标记,存入 Redis 中,指定一个唯一的用户标识作为 value
- 当 key 不存在时才能设置值,确保同一时间只有一个客户端获得锁,满足互斥性特性
- 设置一个过期时间,防止因系统异常导致没能删除这个 key,满足防死锁特性
- 当处理完业务之后需要清除这个 key 来释放锁。
- 清除 key 时需要校验 value 值,需要满足只有加锁的人才能释放锁
获取锁:
SET mylock userId NX PX 10000
mylock
:锁对应的 keyuserId
:为唯一的用户标识,用于删除时校验NX
:表示只有当 key 不存在时才能 set 成功,确保只有一个客户端能够请求成功PX
:10000 表示这个锁有一个10秒的自动过期时间
释放锁
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
执行以上脚本时,需要将mylock作为KEYS[1]
传进去,将userId
作为ARGV[1]
传进去
注意点
- 必须要给锁加一个
过期时间
:这样即使中间系统异常了,等过期时间到了,也可以自动释放锁,防止出现死锁现象- 获取锁时不能分成先设置 key,再设置过期时间两步去执行
缺陷
从上面的描述可以看出来,当出现系统阻塞或者网络延迟等情况下,可能业务还没有执行完成,锁就过期自动释放了,
这时它的业务操作时不受保护的。
总结
以上就是今天要说的内容,欢迎点赞收藏关注🤭。