java四种上锁方式原理及适用场景区分
synchronized(monitor)、ReentantLock(AQS)、AtomicLong(CAS)、LongAdder(XADD)
针对代码块需要同步的锁
synchronized---锁竞争不激烈的场景
竞争不激烈的时候,各种锁优化机制会发挥作用(如轻量级锁、偏向锁、自旋锁、锁消除等),如果竞争激烈,会使用重量级锁,性能下降。
ReentantLock---锁竞争激烈的场景
通过维护state,同步队列和等待对列,性能均衡,竞争激烈也不会有问题
针对某个变量需要同步的场景
AtomicLong(CAS)---锁竞争不激烈的场景
锁竞争激烈的场景,每次CAS都只有一个线程能成功,竞争失败的线程会非常多。失败次数越多,循环次数就越多,很多线程的CAS操作越来越接近 自旋锁(spin lock),导致严重的性能问题
LongAdder(Cells+baseValue)---锁竞争激烈的场景
通过维护baseValue和Cells数组,允许多核场景下多个线程同时在cells中修改变量,极大减少了锁争抢和阻塞导致的性能问题
synchronized ReentranLock AtomicInteger LongAdder
头中的monitor AQS(CAS+queue) CAS Striped64的多Cell
上锁 取消了锁 取消了锁 分散操作
syn也用到了CAS,同时阻塞或者等待的线程放到集合中
ReentranLock中线程 CAS+queue,线程初次争抢锁的时候会进行几次CAS,失败则进入等待对列,不会因为锁竞争激烈时出现大量CAS消耗cpu
AtomicInteger 一直CAS直到操作成功,会因为锁竞争激烈时出现大量CAS消耗cpu
CAS是Unsafe类提供的CPU层级的原子性操作指令
reentrantLock和synchronized的区别
使用方式上:reentrantLock是显式锁,而synchronized是隐式锁,异常情形下,synchronized自动释放锁,reentrantLock则必须在finally中手动释放。
是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中
功能上:reentrantlock 功能更丰富,支持公平锁、锁中断、尝试获取锁等功能,所以更强大
1 reentrantlock 的lockInterruptibly方法支持等待可中断
对于synchronized,如果一个线程正在等待锁,那么结果只有两种情况,要么获得这把锁继续执行 ,要么就保持等待。而使用ReentrantLock,如果一个线程正在等待锁,那么它依然可以收到通知,被告知无需再等待,可以停止工作了。
可中断性说明:synchronize仅仅在阻塞(sleep时)状态可以相应interrupt方法
ReentrantLock中的lockInterruptibly()方法使得线程可以在被阻塞时响应中断,比如一个线程t1通过lockInterruptibly()方法获取到一个可重入锁,并执行一个长时间的任务,另一个线程通过interrupt()方法就可以立刻打断t1线程的执行,来获取t1持有的那个可重入锁。而通过ReentrantLock的lock()方法或者Synchronized持有锁的线程是不会响应其他线程的interrupt()方法的,直到该方法主动释放锁之后才会响应interrupt()方法
————————————————
原文链接:https://blog.csdn.net/dongyuxu342719/article/details/94395877
2 reentrantlock支持公平锁-ReentrantLock(boolean fair)
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁,而非公平锁不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁,synchronized中的锁是非公平的,ReenrantLock默认情况下也是非公平的,但是可以在构造函数中设置为公平锁 。
3 ReentrantLock.tryLock(time,timeUnit)
性能上:Synchronized因为各种锁优化机制更适用于锁竞争不激烈,ReetrantLock适用激烈情形
在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
底层实现上:syn是利用了对象头中的monitor对象,依赖于底层的操作系统的Mutex Lock需要用户态和内核态的切换。lock则是 直接调用cpu指令,无需切换。
例如一个类对其内部共享数据data提供了get()和set()方法,如果用synchronized,则代码如下:
class syncData {
private int data;// 共享数据
public synchronized void set(int data) {
System.out.println(Thread.currentThread().getName() + "准备写入数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + "写入" + this.data);
}
public synchronized void get() {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取" + this.data);
}
}
然后写个测试类来用多个线程分别读写这个共享数据:
public static void main(String[] args) {
// final Data data = new Data();
final syncData data = new syncData();
// final RwLockData data = new RwLockData();
//写入
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5; j++) {
data.set(new Random().nextInt(30));
}
}
});
t.setName("Thread-W" + i);
t.start();
}
//读取
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5; j++) {
data.get();
}
}
});
t.setName("Thread-R" + i);
t.start();
}
}
运行结果:
Thread-W0准备写入数据
Thread-W0写入0
Thread-W0准备写入数据
Thread-W0写入1
Thread-R1准备读取数据
Thread-R1读取1
Thread-R1准备读取数据
Thread-R1读取1
Thread-R1准备读取数据
........
现在一切都看起来很好!各个线程互不干扰!等等。。读取线程和写入线程互不干扰是正常的,但是两个读取线程是否需要互不干扰??
对!读取线程不应该互斥!
那有一种错误的方案就是直接去掉get方法的synchronized,此方案虽然get的时候不再互斥了,但是同时会引入一个问题,我set的时候竟然也可以get,这就导致读取和写入会相互干扰。正确的方案是用读写锁ReadWriteLock实现:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
[java] view plain copy
class Data {
private int data;// 共享数据
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public void set(int data) {
rwl.writeLock().lock();// 取到写锁
try {
System.out.println(Thread.currentThread().getName() + "准备写入数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + "写入" + this.data);
} finally {
rwl.writeLock().unlock();// 释放写锁
}
}
public void get() {
rwl.readLock().lock();// 取到读锁
try {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取" + this.data);
} finally {
rwl.readLock().unlock();// 释放读锁
}
}
}
测试结果:
[plain] view plain copy
Thread-W1准备写入数据
Thread-W1写入9
Thread-W1准备写入数据
Thread-W1写入24
Thread-W1准备写入数据
Thread-W1写入12
Thread-W0准备写入数据
Thread-W0写入22
Thread-W0准备写入数据
Thread-W0写入15
Thread-W0准备写入数据
Thread-W0写入6
Thread-W0准备写入数据
Thread-W0写入13
Thread-W0准备写入数据
Thread-W0写入0
Thread-W2准备写入数据
Thread-W2写入23
Thread-W2准备写入数据
Thread-W2写入24
Thread-W2准备写入数据
Thread-W2写入24
Thread-W2准备写入数据
Thread-W2写入17
Thread-W2准备写入数据
Thread-W2写入11
Thread-R2准备读取数据
Thread-R1准备读取数据
Thread-R0准备读取数据
Thread-R0读取11
Thread-R1读取11
Thread-R2读取11
Thread-W1准备写入数据
Thread-W1写入18
与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)
从理论上讲,与互斥锁定相比,使用读-写锁定所允许的并发性增强将带来更大的性能提高。
在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。——例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁定的理想候选者。
StampedLock是ReadWriteLock的一个改进。StampedLock与ReadWriteLock的区别在于,StampedLock认为读不应阻塞写。StampedLock认为读写互斥应该是指——不让读重复读同一数据,而不是不让写线程写。StampedLock是一种偏向于写线程的改进,解决了读多写少时,使用ReadWriteLock会产生写线程饥饿现象。
下面将锁对比下
synchronized同步锁--锁竞争不激烈,可以充分利用jvm的锁优化机制,如锁消除、偏向锁、轻量级锁、自旋锁等
ReentrantLock可重入锁(Lock接口)--应用于锁竞争激烈
- 相对于synchronized更加灵活,可以控制加锁和放锁的位置
- 可以使用Condition来操作线程,进行线程之间的通信
- 核心类AbstractQueuedSynchronizer,通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。
ReentrantReadWriteLock可重入读写锁(ReadWriteLock接口)--应用于锁竞争激烈,读多并且读写锁互斥的场景
- 相对于ReentrantLock,对于大量的读操作,读和读之间不会加锁,只有存在写时才会加锁,但是这个锁是悲观锁
- ReentrantReadWriteLock实现了读写锁的功能
- ReentrantReadWriteLock是ReadWriteLock接口的实现类。ReadWriteLock接口的核心方法是readLock(),writeLock()。实现了并发读、互斥写。但读锁会阻塞写锁,是悲观锁的策略。
StampedLock戳锁 --应用于锁竞争激烈,读多并且读写锁不互斥的场景
- ReentrantReadWriteLock虽然解决了大量读取的效率问题,但是,由于实现的是悲观锁,当读取很多时,读取和读取之间又没有锁,写操作将无法竞争到锁,就会导致写线程饥饿。所以就需要对读取进行乐观锁处理。
- StampedLock加入了乐观读锁,不会排斥写入
- 当并发量大且读远大于写的情况下最快的的是StampedLock锁
ReentranLock读写均互斥
ReentrantReadWriteLock(读之间不互斥,读写互斥)
StampedLock(读多并且读写锁不互斥,写会立即执行)
jdk对synchronized进行了哪些优化?
锁消除 :不存在所竞争,StringBuffer在非静态方法内部声明,一定不会竞争,此时会去除锁
偏向锁 线程获取锁后,增加偏向锁标志和当前拥有锁的线程标志;同一线程再次进来时无需再次争抢锁,直接查看标志,为偏向锁并且所拥有者为本线程,则直接执行同步代码;如果是偏向锁,但锁拥有者不是本线程,通过CAS锁owner竞争,如果成功还是偏向锁,否则升级为轻量级锁。
场景:不存在线程争抢,一直某线程拥有某锁
轻量级锁 当前线程建立名为锁记录(Lock record=displaced hdr+owner)的空间,然后将对象头中的Mark Word复制到锁记录空间中;执行CAS:将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。
CAS成功,锁标志修改为轻量级锁,此时线程堆栈与对象头的状态如图:
CAS失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,不是则升级为重量级锁
场景:偶尔存在线程争抢锁
重量级锁 :锁标志的状态值变重量级锁,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。轻量级锁在向重量级锁膨胀的过程中,一个操作系统的互斥量(mutex)和条件变量( condition variable )会和这个被锁的对象关联起来。
重量级锁依赖于操作系统的互斥量(mutex) 实现,该操作会导致进程从用户态与内核态之间的切换, 是一个开销较大的操作。
适应性自旋锁 :当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
syn也用到了CAS,同时阻塞或者等待的线程放到集合中
ReentranLock中线程 CAS+queue,线程初次争抢锁的时候会进行几次CAS,失败则进入等待对列,不会因为锁竞争激烈时出现大量CAS消耗cpu
AtomicInteger 一直CAS直到操作成功,会因为锁竞争激烈时出现大量CAS消耗cpu
CAS是Unsafe类提供的CPU层级的原子性操作指令
synchronized 到底慢在哪里?
reentrantLock在几十个线程场景下也一直尝试CAS吗?很耗CPU吗?
并不是一直CAS,仅仅是初次尝试获取锁的时候会进行多次CAS尝试,如果失败,会加入等待队列中,而后在执行,所以对CPU影响有限
synchronized相比reentrantLock性能不好原因分析:
synchronized为重量级锁时:
猜测1 syn锁的获取和释放需要操作系统的互斥量(mutex)实现,均需要用户态和内核态的切换;lock是CAS获取和释放的锁,无需切换
猜测2 syn和lock底层都有等待的线程集合,都是阻塞。syn的阻塞是系统来处理的,需用户态和内核态的准换;lock的阻塞是执行jdk中的代码来阻塞的,应该是一直是在用户态的
所以两者性能差别主要是syn中用户态和内核态的切换导致的性能更差。
ReentrantLock底层实现依赖于特殊的CPU指令,比如发送lock指令和unlock指令,不需要用户态和内核态的切换,所以效率高(这里和volatile底层原理类似),而synchronized底层由监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock需要用户态和内核态的切换,所以效率低。
什么是自旋锁? AtomicInteger在锁竞争十分激烈的场景下 性能上 退化为自旋锁
AtomicReference ? 原子更新类 Unsafe+ volatile(value)
信号量? 同时允许多个线程获取锁同时执行,AQS-state初始为允许的线程数,然后线程执行acquire方法获取,state>0且CAS设置state-1成功则获取锁,否则失败放到对列中。
借鉴:
https://www.cnblogs.com/baizhanshi/p/7211802.html
https://www.cnblogs.com/paddix/p/5405678.html