锁lock
或互斥mutex
是一种同步机制,主要用于在存在多线程的环境中强制对资源进行访问限制。锁的主要作用为强制实施互斥排他以及并发控制策略。锁一般需要硬件支持才可以有效的实施。这种支持通常采取一个或多个原子指令的形式,如test-and-set
,fetch-and-add
或者compare-and-swap
。这些指令允许单个进程测试锁是否空闲,如果空闲,则通过单个原子操作获取锁。
使用锁可以保证多线程的环境下同步执行,可以解决可见性/有序性/原子性问题。
CAS(Compare And Swap
),比较并交换,为乐观锁。
CAS只能针对单个变量进行原子操作;不能操作代码块。(AtomicReference
原子引用类可解决多属性变量的问题),适用于资源竞争较少(线程冲突较轻)的情况,自旋(循环)时间不会很长,不会浪费cpu
太多资源。
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicityDemo {
private static final AtomicInteger number = new AtomicInteger(1);
public static void main(String[] args) throws InterruptedException {
ArrayList<Thread> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
number.getAndIncrement();
}
});
thread.start();
list.add(thread);
}
for (Thread thread : list) {
thread.join();
}
number.decrementAndGet();
System.out.println("number:" + number);
}
}
悲观锁
乐观锁
CAS
有三个操作数,即内存值 v
,旧操作数a
,新操作数b
。当需要更新v
为b
时,需要先判断v
值是否与之前的所见值a
相同,若相同则将v
赋值为b
,若不同,则什么都不做。CAS
是一种非阻塞算法。利用JNI
机制和CPU
的lock cmpxchg...
指令来完成。
JNI
机制:https://www.cnblogs.com/kexinxin/p/11689641.html
当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断 锁是否能够被成功获取,直到获取到锁才会退出循环。
阻塞或唤醒一个Java
线程需要操作系统切换 CPU
状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。所以自旋锁就可以避免 CPU
切换带来的性能、时间等影响。
自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。
一个基于CAS
实现的自旋锁:AtomicInteger
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4
return var5;
}
do while
循环操作中的判断条件compareAndSwapInt
方法。整个“比较+更新”操作封装在compareAndSwapInt()
中,整个过程就是自旋的去重试这个操作。
定义锁
import java.util.concurrent.atomic.AtomicReference;
/**
* @program: Demo
* @description: 自定义自旋锁
* @author: 岚樱时
* @date: 2020-05-26 09:43
*/
public class CustomSpinLock {
/**
* 存放当前持有锁的线程
* 当reference为null,锁没有被线程获取
*/
private AtomicReference<Thread> reference = new AtomicReference<>();
/**
* 获取锁
*/
public void lock(){
Thread currentThread = Thread.currentThread();
while (!reference.compareAndSet(null, currentThread)) {
// 自旋判断锁是否被其他线程持有
// reference为null时,当前锁没有被线程获取,compareAndSet返回true
// TODO
}
}
/**
* 释放锁
*/
public void unLock(){
Thread currentThread = Thread.currentThread();
if (reference.get() != currentThread) {
// 锁被其他线程占用,当前线程无法释放锁
return;
}
// 释放锁
reference.set(null);
}
}
测试自定义锁
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @program: Demo
* @description: 自定义自旋锁测试类
* @author: 岚樱时
* @date: 2020-05-26 10:15
*/
public class TestCustomSpinLock {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(100);
// 创建计数器
// 计数器的初始值是线程的数量,每当一个线程执行完成之后,计数器的值-1
// 当计数器的值为0,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作
CountDownLatch countDownLatch = new CountDownLatch(100);
// 创建自定义自旋锁
CustomSpinLock customSpinLock = new CustomSpinLock();
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
customSpinLock.lock();
++count;
customSpinLock.unLock();
// 释放锁后,计数器值-1
countDownLatch.countDown();
});
}
// 调用await()方法,线程被挂起,等待计数器为0后继续执行
countDownLatch.await();
System.out.println(count);
executorService.shutdown();
}
}
lock()
方法利用的CAS
,当第一个线程A
获取锁的时候,成功获取到锁,不会进入while
循环。A
没有释放锁,另一个线程B
想获取锁,此时由于不满足CAS
,所以就会进入while
循环,不断判断是否满足CAS
,直到线程A
调用unlock
方法释放锁,线程B
才会获取锁。参数 | 默认值或限制 | 说明 |
---|---|---|
-XX:-UseSpinning | java1.4.2和1.5需要手动启用,java6默认已启用 | 启用多线程自旋锁优化 |
-XX:PreBlockSpin=10 | -XX:+UseSpinning必须先启用。对于jdk6来说已经默认启用了,默认自旋10次。 | 控制多线程自旋锁优化的自旋次数 |
自适应意味着对于一个锁对象,线程的自旋时间是根据上一个持有该锁的线程的自旋时间以及状态来确定的。
例如:对于锁A
对象来说,如果一个线程刚刚通过自旋获得了锁,并且该线程也在运行中,那么JVM
会认为此次自旋操作也是有很大的机会可以拿到锁,因此它会让自旋的时间相对延长。但是如果对于锁B
对象自旋操作很少成功的话,JVM
甚至可能直接忽略自旋操作,将线程挂起或堵塞。
线程1准备用CAS
修改变量A
,在此之前,其他线程将变量的值由A
替换为B
,又由B
替换为A
,然后线程1执行CAS
时发现变量的值仍然为A
,所以CAS
成功。但是实际上这时的现场已经和最初不同。
代码示例
import java.util.concurrent.atomic.AtomicInteger;
/**
* @program: Demo
* @description: 测试ABA问题案例
* @author: 岚樱时
* @date: 2020-05-26 11:00
*/
public class AtomicDemo {
public static AtomicInteger atomicInteger = new AtomicInteger(1);
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println("当前线程:" + Thread.currentThread() + "\n初始值:" + atomicInteger);
try {
// 线程1等待,线程2对数据进行修改
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// CAS 期望1->2
boolean CASStatus = atomicInteger.compareAndSet(1, 2);
System.out.println("当前线程:" + Thread.currentThread() + "\nCAS操作结果:" + CASStatus);
}, "线程1");
Thread thread2 = new Thread(() -> {
try {
// 线程等待,确保线程1先执行
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// atomicInteger+1=>2
atomicInteger.incrementAndGet();
System.out.println("当前线程:" + Thread.currentThread() + "\nincrementAndGet结果:" + atomicInteger);
// atomicInteger-1=>1
atomicInteger.decrementAndGet();
System.out.println("当前线程:" + Thread.currentThread() + "\ndecrementAndGet结果:" + atomicInteger);
}, "线程2");
thread1.start();
thread2.start();
}
}
AtomicStampedReference
主要维护一个对象引用以及一个可以自动更新的整数stamp
的pair
对象来解决ABA
问题。
AtomicStampedReference源码
package java.util.concurrent.atomic;
/**
* An {@code AtomicStampedReference} maintains an object reference
* along with an integer "stamp", that can be updated atomically.
*
* Implementation note: This implementation maintains stamped
* references by creating internal objects representing "boxed"
* [reference, integer] pairs.
*
* @since 1.5
* @author Doug Lea
* @param The type of object referred to by this reference
*/
public class AtomicStampedReference<V> {
private static class Pair<T> {
// 维护对象引用
final T reference;
// 用于标志版本
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
/****/
/**
* expectedReference 更新之前的原始值
* newReference 将要更新的新值
* expectedStamp 期待更新的标志版本
* newStamp 将要更新的标志版本
*/
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
/****/
}
测试代码
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @program: Demo
* @description: 解决ABA案例
* @author: 岚樱时
* @date: 2020-05-26 12:50
*/
public class AtomicDemo1 {
private static final AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 0);
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println("当前线程:" + Thread.currentThread() + "\n初始值a:" + atomicStampedReference.getReference() + "\n");
// 获取当前标识版本
int stamp = atomicStampedReference.getStamp();
try {
// 线程等待3s,保证干扰线程执行
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 判断reference&&stamp
boolean CASStatus = atomicStampedReference.compareAndSet(1, 2, stamp, stamp + 1);
System.out.println("当前线程:" + Thread.currentThread() + "\nCAS判断结果:" + CASStatus + "\n");
}, "线程1");
Thread thread2 = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println("当前线程:" + Thread.currentThread() + "\nincrement值:" + atomicStampedReference.getReference()+"\n");
atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println("当前线程:" + Thread.currentThread() + "\ndecrement值:" + atomicStampedReference.getReference() + "\n");
},"线程2");
thread1.start();
thread2.start();
}
}
使用synchronized
关键字,可以解决可见性/有序性/原子性问题。
/**
* @program: Demo
* @description: Synchronized关键字使用方式案例
* @author: 岚樱时
* @date: 2020-05-26 13:38
*/
public class SynchronizedDemo {
static int counter = 0;
static final Object lock = new Object();
public static void main(String[] args) {
// 锁是括号里面的对象
synchronized (lock) {
counter++;
}
}
/**
* 锁是当前类对象
*/
public synchronized static void m1(){
counter++;
}
}
原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
synchronized
对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新都主存当中,保证资源变量的可见性,如果某个线程占用了该锁,其他线程就必须在锁池中等待锁的释放。
synchronized
保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。就是一个线程拥有了锁仍然还可以重复申请该锁。
在没有多线程竞争的情况下,轻量级锁的获取以及释放多次CAS
原子指令,而偏向锁只依赖一次CAS
原子指令置换ThreadID
,不过一旦出现多个线程竞争时必须撤销偏向锁,所以撤销偏向锁消耗的性能必须小于之前节省下来的CAS
原子操作的性能消耗。
JDK1.6
之后默认开启偏向锁XX:BiasedLockingStartupDelay=0
参数关闭延迟-XX:-UseBiasedLocking
参数关闭偏向锁。通过markup mark = obj->mark()
获取对象的markOop
数据mark
,即对象头的Mark Word
判断mark
是否为可偏向状态,即mark
的偏向锁标志位为1,锁标志位为01
判断mark
中JavaThread
的吗状态
通过CAS
原子指令设置mark
中的JavaThread
为当前线程ID
,执行CAS
成功,执行同步代码块
如果执行CAS
失败,表示当前存在多个线程竞争锁,火的偏向锁的线程达到全局安全点(safepoint
)时被挂起,撤销偏向锁,并升级为轻量级。升级完成后被阻塞在安全点的线程继续执行同步代码块
只有当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,偏向锁的撤销由BiasedLocking::revoke_at_safepoint
方法实现。
依赖包
<dependency>
<groupId>org.openjdk.jolgroupId>
<artifactId>jol-coreartifactId>
<version>0.10version>
dependency>
代码案例实现
import org.openjdk.jol.info.ClassLayout;
/**
* @program: Demo
* @description: 偏向锁Demo
* @author: 岚樱时
* @date: 2020-05-26 14:35
*/
public class BiasedLockDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("开启偏向锁");
biasedLockStart();
System.out.println("关闭偏向锁");
biasedLockClose();
}
/**
* 开启偏向锁
* @throws InterruptedException
*/
public static void biasedLockStart() throws InterruptedException{
// 加锁前,偏向锁生效有延迟,不会立即生效,输出001
String result = ClassLayout.parseInstance(new BiasedLockDemo.Model()).toPrintable();
System.out.println("result:" + result);
Thread.sleep(5000);
// 加锁前,延迟一定时间后,偏向锁生效,输出101
Model model = new BiasedLockDemo.Model();
result = ClassLayout.parseInstance(model).toPrintable();
System.out.println("result:" + result);
synchronized (model) {
result = ClassLayout.parseInstance(model).toPrintable();
System.out.println("result:" + result);
}
// 解锁后,对象头不变
result = ClassLayout.parseInstance(model).toPrintable();
System.out.println("result:" + result);
}
/**
* 关闭偏向锁
* @throws InterruptedException
*/
public static void biasedLockClose() throws InterruptedException {
// 加锁前,输出001
String result = ClassLayout.parseInstance(new Model()).toPrintable();
System.out.println(result);
Thread.sleep(5000);
System.out.println("================================================");
// 加锁前,延迟i一定时间后,仍输出001
Model model = new Model();
result = ClassLayout.parseInstance(model).toPrintable();
System.out.println(result);
System.out.println("================================================");
// 加锁时,输出轻量级锁信息 线程栈中的lock record锁记录指针和00状态位
synchronized (model) {
result = ClassLayout.parseInstance(model).toPrintable();
System.out.println(result);
System.out.println("================================================");
}
// 解锁后,输出001,无锁状态
result = ClassLayout.parseInstance(model).toPrintable();
System.out.println(result);
}
public static class Model{
}
}
开启偏向锁
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 90 00 0b (00000101 10010000 00000000 00001011) (184586245)
4 4 (object header) b6 7f 00 00 (10110110 01111111 00000000 00000000) (32694)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 90 00 0b (00000101 10010000 00000000 00001011) (184586245)
4 4 (object header) b6 7f 00 00 (10110110 01111111 00000000 00000000) (32694)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
关闭偏向锁
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
================================================
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
================================================
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 90 00 0b (00000101 10010000 00000000 00001011) (184586245)
4 4 (object header) b6 7f 00 00 (10110110 01111111 00000000 00000000) (32694)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
================================================
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 90 00 0b (00000101 10010000 00000000 00001011) (184586245)
4 4 (object header) b6 7f 00 00 (10110110 01111111 00000000 00000000) (32694)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,直接使用轻量级锁。但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。
当关闭偏向锁功能,或多个线程竞争偏向锁导致锁升级为轻量级锁,会尝试获取轻量级锁,其入口位于ObjectSynchronizer::slow_enter
轻量级锁的释放通过ObjectSynchronizer::fast_exit
完成
BasicLock
对象的mark
数据dhw
CAS
尝试把dhw
替换到当前的Mark Word
,如果CAS
成功,说明成功的释放了锁,否则执行步骤3CAS
失败,说明有其他线程在尝试获取锁,这是该锁已经升级为重量级锁。执行重量级锁释放流程。重量级锁通过对象内部的监视器monitor
实现,其中monitor
的本质是依赖于底层操作系统的MutexLock
实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
锁膨胀过程通过ObhectSynchronizer::inflate
函数实现。
锁 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加锁不需要额外消耗,执行非同步方法仅有纳秒差距。 | 如果线程见存在竞争,会带来额外的撤销锁的消耗。 | 只有一个线程访问同步块场景。 |
轻量级锁 | 竞争的线程不会堵塞,提高了程序的响应速度。 | 如果始终得不到锁,竞争的线程使用自旋,会消耗CPU | 追求响应时间,同步块指向性速度非常快。 |
重量级锁 | 线程竞争不会自旋,不消耗CPU。 | 线程堵塞,响应时间慢。 | 追求吞吐量,同步块执行时间较长。 |
锁消除是虚拟机对锁的优化处理,JIT即使编译器(把热点代码编译成与本地平台相关的机器码,并且进行各种层次的优化)对运行上下文进行扫描,去除不可能存在竞争的锁,比如下面两个方法的执行效率是一样的,由于object锁是私有变量,不存在竞争关系。
public void lockElimination(){
Object o = new Object();
synchronized (o) {
System.out.println("Say Hello!");
}
}
public void lockEliminationExt(){
Object o = new Object();
System.out.println("Sqy Hello!");
}
锁粗化湿 虚拟机对锁的另一种优化处理。通过扩大锁的范围,避免反复加锁和释放锁。比如下面两个方法经过锁粗化优化之后执行效率一样了。
public void lockCoarse(){
for (int i = 0; i < 10000; i++) {
synchronized (SynchronizedDemo.class) {
System.out.println("Say Hello!");
}
}
}
public void lockCoarseExt(){
synchronized (SynchronizedDemo.class) {
for (int i = 0; i < 10000; i++) {
System.out.println("Say Hello!");
}
}
}