synchronized,ReentrantLock、ReentrantReadWriteLock和StampedLock

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 是使用读-写锁定的理想候选者。

 StampedLockReadWriteLock的一个改进。StampedLockReadWriteLock的区别在于,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

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值