JVM中的锁
每个对象都有一个对象头mark 4个字节(32位),
mark主要存放:
对象的hash, 锁信息, 垃圾回收标记, 对象年龄
指向锁记录的指针
指向monitor的指针(synchronized关键字对应的指令码)
偏向锁线程id
偏向锁
偏向锁是一个线程获取锁以后,当该线程再次获取该锁, 不需要做monitor等判断, 可以直接获取, 性能较高.
偏向锁使用只在资源不存在或者存在十分小的竞争情况下使用,能有效提高性能,
在资源竞争激烈(多线程)的情况下, 每次线程都试图获取偏向锁,但是每次都获取不到, 所以性能下降.
偏向锁在jdk1.6开始默认启用, 参数为:-XX:+UseBiasedLocking
-XX:BiasedLockingStartupDelay=n 为偏向锁启用时间, 默认会在jvm启动几秒后启用, jvm团队认为 jvm刚启动时, 资源竞争高,所以会在后续几秒后启用.
轻量级锁(BasicObjectLock)
嵌入在线程栈中的对象
BasicObjectLock有(BasicLock)对象头和指向持有该锁的对象指针组成.
普通的锁性能不理想, 轻量级锁是一种快速锁定方法.
使用轻量级锁, 在判断是否一个线程只有对象锁, 只需要判断对象头的锁指针是否指向该线程栈即可.
轻量级锁失败,表示存在竞争, 升级为重量级锁(常规锁)
在没有锁竞争情况下, 减少传统互斥量产生的性能消耗
在激烈竞争时, 轻量级锁会多做额外操作, 导致性能下降
自旋锁
在竞争存在时, 如果线程可以很快获得锁, 可以不在OS(操作系统)层挂起线程(性能损耗高),
可以多几个空操作,这就叫自旋
自旋锁jdk1.6 -XX:+UseSpinning 开启自旋锁, jdk 1.7中默认内置实现了自旋锁, 参数废弃.
如果同步块很长,自选失败, 会降低系统性能
如果同步块很短, 自旋成功, 节省挂起时的切换时间,提升性能
偏向锁/轻量级锁/自旋锁不是java语言层锁优化方法.
内置于JVM中的获取锁的优化方法和获取锁的步骤
偏向锁可用会先尝试偏向锁
轻量级锁可用会先尝试轻量级锁
以上失败,会尝试自旋锁
再失败, 尝试普通锁, 使用OS互斥量在操作系统曾挂起
Java语言层锁优化
1. 减少锁持有时间(尽量再最小的区域内使用锁(synchronized)).
就是只对需要同步块那部分代码使用同步块, 不必要代码不要放到同步块中
2. 减小锁的粒度
将需要锁定的大对象拆成小对象, 增加并行度, 较少锁竞争, 这样以来就可以增加偏向锁和轻量级锁的成功率
该种实现例子就是ConcurrentHashMap
HashMap实现同步可以通过Collections.synchronizedMap(map)获得一个同步SynchronizedMap,
SynchronizedMap的方法进行了同步块实现
ConcurrentHashMap的实现就是减小锁粒度
将内容分为若个segment片, 每个segment, 存储的k-v具体值.
在put操作时,先锁定某个segment块在进行put操作, 而不是锁定整个map.
减小锁粒度以后, ConcurrentHashMap就可以允许多个线程同时进入操作,只要多个线程
操作的是不同的segment.
3.锁分离
根据功能分离锁
如读写锁ReadWriteLock,在读多写少情况时, 能提高性能.
允许多读,但是只能一个写, 只要有一个线程获取读锁,其他读线程可读.写线程等待
在有一个线程写时, 其他读线程和写线程等待.
LinkedBlockingQueue 链表队列 头take,尾put, 头尾操作互不影响,
可以一个线程put,一个take, 也是一种锁分离的应用
4. 锁粗化
通常情况下, 保证并发多线程有效, 要求每个线程持有锁的时间尽量少, 以便尽早释放锁, 其他线程得以获得.
但是,如果对同一个锁频繁请求和释放过多也会导致资源消耗, 此时需要对锁进行粗化.
比如: 下面的锁需要粗化, 放大同步块区域(synchronized时可重入锁)
public void method() {
synchronized(lock) {do ..}
synchronized(lock) {do ..}
}
public void method() {
for (x…) {
synchronized(lock) {do ..}
}
}
5. 锁消除
方法内不可能被多线程使用的变量, 但是出现了加锁, 此时编译器发现对象不可能被共享, 将会消除这些锁操作.
例如: StringBuffer 是个线程安全的方法都有同步块, 但是此处不需要加锁, 编译器编译时会去掉锁操作
public String method(String s) {
StringBuffer sb = new StringBuffer();
sb.append(s);
return sb.toString();
}
锁消除的JVM参数
-XX:+DoEscapeAnalysis 逃逸分析, 锁对象是否逃逸(一个对象如果在方法内创建被使用,
但是此后方法外也有使用, 被称为逃逸)
-XX:+EliminateLocks 启用锁消除
可以对上面方法循环2000000次计算耗时
6.无锁
锁是悲观操作, 悲观操作认为操作是多线程不安全的,存在竞争 ,所以使用锁
无锁就是乐观操作, 乐观操作认为所有操作都是安全的不存在竞争关系
无锁的实现方式
CAS(Compare And Swap) 比较交换, 进来就比较并交换值, 不等待, 成功继续执行,
否则重试或者放弃.
CAS有三个参数V(variable), E(expected), N(new),
分别是 V需要操作的变量, E期望变量的值, N操作后的新值,
交换过程为变量V的值与预期值E相等, 则将变量V赋值为N新值.
if (v == e) v = n;
CAS是cpu的一条指令, 能保证线程安全, 该种性能远远高于锁机制
通过该方式应用在具体实现中, 判断是否多线程干扰, 如果有就重试.
java.util.concurrent.atomic包内存在很多原子操作的对象就是采用CAS如:AtomicInteger