一、乐观锁:

不会对资源加锁,只是更新共享资源时,判断是否允许更新。

1.1 CAS(Compare and Swap)思想:

它包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。

如果内存位置的值与预期原值相等,则将该位置的值更新为新值,否则不做任何操作

1.1.1 使用 Unsafe.compareAndSwapObject() 保证线程安全用例

如下的用例中 UNSAFE.compareAndSwapObject(unsafeCASTest, I_OFFSET, unsafeCASTest.i, unsafeCASTest.i + 1); 完成了多线程安全的 i+1 操作。

/*** @Author Snail* @Describe 通过Unsafe对多个线程操作i++实现线程安全问题* @CreateTime 2020/3/11*/
public class UnsafeCASTest {private int i = 0;private static Unsafe UNSAFE;static long I_OFFSET;//i的偏移量static {try {//该方法无法获取到UNSAFE类,需要通过下面的反射去获取
//            UNSAFE = Unsafe.getUnsafe();// 获取 Unsafe 内部的私有的实例化单例对象Field field = Unsafe.class.getDeclaredField("theUnsafe");// 无视权限field.setAccessible(true);UNSAFE = (Unsafe) field.get(null);I_OFFSET = UNSAFE.objectFieldOffset(UnsafeCASTest.class.getDeclaredField("i"));} catch (NoSuchFieldException | IllegalAccessException e) {e.printStackTrace();}}@Testpublic void main() {UnsafeCASTest unsafeCASTest = new UnsafeCASTest();new Thread(new ThreadAdd(unsafeCASTest)).start();new Thread(new ThreadAdd(unsafeCASTest)).start();new Scanner(System.in).nextLine();//bio阻塞当前执行线程}class ThreadAdd implements Runnable {private final UnsafeCASTest unsafeCASTest;public ThreadAdd(UnsafeCASTest unsafeCASTest) {this.unsafeCASTest=unsafeCASTest;}@Overridepublic void run() {while (true) {//                i++;boolean b = UNSAFE.compareAndSwapObject(unsafeCASTest, I_OFFSET, unsafeCASTest.i, unsafeCASTest.i + 1); try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "::" + i);}}}
}

1.1.2 Java基于 CAS 提供的操作类

java.util.concurrent.atomic 包下的 AtomicInteger、AtomicBoolean、AtomicLong 等,使用了 CAS 机制,保证了在多线程环境下,操作同一个变量的安全性。

//关于AtomicInteger的一个简单用例AtomicInteger atomicInteger=new AtomicInteger(0);int i = atomicInteger.addAndGet(2);System.out.println(i);//2System.out.println(atomicInteger);//2int andAdd = atomicInteger.getAndAdd(3);System.out.println(andAdd);//2System.out.println(atomicInteger);//5

CAS的思考:

Q1:CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?

A:CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。

Q2:CAS 有那些缺点?
A:

1. ABA问题2. 高竞争环境下,自旋对CPU资源的消耗3. 不够灵活,只能保证一个共享变量的原子操作,涉及到多个变量的同步时,CAS无法保证安全

Q3:如何解决 CAS 的缺点?
A:

1.ABA 问题的出现,是由于对变量的版本缺少了比较,所以我们可以使用 AtomicStampedReference(V initialRef,int initalStamp) ,加入了版本号对比,避免出现 ABA 问题2. 控制自旋的次数:JDK 6后引入了自适应型自旋锁,同时也可以在代码中控制自旋的时间或次数。

1.2 版本号控制:

   当要提交一个更新的时候,比较读取时候的版本号,如果版本号一致,允许提交更新,否则返回失败。多用于 DB 的数据安全控制,如MVCC机制。


二、悲观锁:

开始操作时,就对共享资源加锁,操作完成后再释放锁。

2.1 Lock(ReentrantLock)与 Synchronized 的对比

类别 synchronized Lock
存在层次 Java的关键字,在jvm层面上 是一个类
锁的获取 关键字即可完成加锁 lock.lock() , lock.tryLock() , lock.tryLock(timeout, unit)
锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 (在其底层原理可以看到) 必须在finally中释放锁,不然容易造成线程死锁
锁当前状态 无法判断(可能会出现A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 ) 可以判断(trylock,避免长时间等待)
锁类型 可重入、不可中断、非公平 可重入、可中断、可公平(初始化时传入布尔值)
唤醒 notify()/notifyAll()随机唤醒一个线程或唤醒全部线程 使用 Condition 结合 await()/singal() 实现线程的精确唤醒
适用场景 少量同步竞争 大量同步竞争

2.2 ReentrantLock

Lock 接口及其特点:

1. 可重入锁

   同一个线程可以对同一把锁,在不释放的前提下,反复加锁,而不会导致线程等待锁而出现卡死的情况。但需要开发人员保证加锁和解锁的操作成对出现。

   重入锁源码中,通过 private volatile int state 变量来记录重入次数。state==0 时,表示锁是空闲的(任意线程都可以加锁成功),大于零表示锁已经被占用, 其数值表示当前线程重复占用这个锁的次数。

java.util.concurrent.locks.ReentrantLock.NonfairSync#lockfinal void lock() {// compareAndSetState就是对state进行CAS操作,如果修改成功就占用锁if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());else//如果修改不成功,说明 有线程 已经在使用了这个锁,那么就 可能 需要等待acquire(1);
}java.util.concurrent.locks.AbstractQueuedSynchronizer#acquirepublic final void acquire(int arg) {//tryAcquire() 再次尝试获取锁,//如果发现锁就是当前线程占用的,则更新state,表示重复占用的次数,//同时宣布获得所成功,这正是重入的关键所在if (!tryAcquire(arg) &&// 如果获取失败,那么就在这里入队等待acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//如果在等待过程中 被中断了,那么重新把中断标志位设置上selfInterrupt();
}

2. 公平锁

   ReentrantLock 默认非公平锁,可在实例化时传入 boolean 值决定锁的类型。

   tryAcquire 是获取锁的一个操作,在公平锁的实现中,按进入锁队列的先后顺序为线程分配锁,先进入的线程优先获取锁。

java.util.concurrent.locks.ReentrantLock.FairSync#tryAcquireprotected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {//先看看有没有别人在等,没有人等我才会去抢,有人在我前面 ,我就不抢啦if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}

3. 可中断锁

   在等待锁的过程中可以响应中断,如果此时,程序正好收到关机信号,中断就会触发,进入中断异常后,线程就可以做一些清理工作,从而防止在终止程序时出现数据写坏,数据丢失等悲催的情况

   可中断锁的使用: lock.lockInterruptibly() 或者 lock.tryLock(timeout, unit) throws InterruptedException

关于Lock和synchronized的比较可参考

关于使用lock中的方法用例

2.2.1 ReentrantReadWriteLock

   ReentrantReadWriteLock 中包含读锁和写锁,其中读锁是可以多线程共享的,即共享锁,而写锁是排他锁,一把排他锁不予许与额外的锁同时存在。

ReentrantLock 与 ReentrantReadWriteLock 的区别:

   1. ReentrantReadWriteLock 细分读锁和写锁是为了提高效率,将读和写分离,对比 ReentrantLock 可以发现,无论并发读还是写,它总会先锁住全部再说。
   2. ReentrantReadWriteLock 进一步提高了多线程锁竞争环境下,代码的吞吐量。

2.2.2 底层的实现

   ReentrantLock:主要使用 AbstractQueuedSynchronizer(AQS)中的 volatile 修饰的同步状态位state、CAS 修改 state 和 CLH 的线程队列实现的一种独占锁。

Lock的更多分析与介绍

2.3 Synchronized

2.3.1 JDK 6 后对 Synchronized 的优化

   Synchronized 是从 JVM 层面为我们提供的单机锁,当线程访问同步块时首先需要获得锁并把相关信息存储在对象头中,对象头主要有由两部分组成:

  1. Mark Word :存储自身的运行时数据,例如 HashCode、GC 年龄、锁相关信息等内容。
  2. Klass Pointer:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

Mark Word在不同的锁状态下存储的内容不同,64 位虚拟机 Mark Word 是 64bit ,其结构如下:

  • biased_lock:是否偏向锁,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
  • 锁标志位(lock):区分锁状态,11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
  • 对象的hashcode(hash):运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中。
  • 分代年龄(age):表示对象在 YoungGeneration 被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。4bit表示最大GC次数为1111(二进制)=15次。
  • 偏向锁的线程ID(JavaThread):偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
  • epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。
  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。

从 Mark Word 中可以看到,锁有3种状态:

1. 偏向锁:减少同一线程多次获取同一个锁的性能消耗

   检查 Mark Word 中的线程 id 是否为本线程,是则加锁成功,否则撤销偏向锁

   适用带有同步但只有一个线程处理的程序性能,但如果存在竞争会带来额外的锁撤销操作。

   偏向锁在 JDK 6 及以后的 JVM 里是默认启用的。可以通过参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

2. 轻量级锁:多个线程竞争偏向锁升级为轻量级锁

   是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

   在 JVM 中的实现是:在当前线程的栈帧中创建 Lock Reocrd,并将对象头中的 Mark Word 复制到 Lock Reocrd 中。然后使用 CAS 尝试将 Mark Word 替换为指向 Lock Reocrd 的指针。

   适用多线程交替执行同步块的情况,但如果多个线程同时 抢占锁 就会升级为重量级锁

3. 重量级锁:将除了拥有锁的线程以外的线程都阻塞

锁状态升级总结:

  1. 上述的3种锁状态,只能升级不能降级
  2. 偏向锁通过对比 Mark Word 解决加锁问题,避免执行 CAS 操作。而轻量级锁是通过用 CAS 操作来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞

除了上述对锁状态的优化,还有如下的一些处理:

自适应性自旋锁:为避免线程间的频繁切换,JVM按情况给出适合的自旋次数

   自旋是一种获取锁的机制,它会占用 CPU 的时间。

   自旋避免了线程被挂起,提高了线程的响应速度,但如果线程长期无法获取锁,自旋时间过长时,会升级为重量级锁。

锁消除:对检测到不可能存在共享数据竞争的锁进行消除

锁粗化:当一连串的操作都在对同一个对象加锁时,JVM会扩大锁范围

参考资料:
更多锁优化信息的详细介绍
美团-锁特性的对比
小米-synchronized的分析

底层的实现

   synchronized 为重量级锁时,没有竞争到锁的对象会 park 被挂起,退出同步块时 unpark 唤醒后续线程

   ObjectMonitor 为其底层使用的对象,包含两个同步队列,一个阻塞队列。多线程竞争时,不断的从同步队列中取任务来执行

适用场景

   乐观锁适用于线程间竞争较少的情况,因为这种场景下,乐观锁可以通过重试的机制,去避免了线程的阻塞和加锁操作。

   悲观锁适用线程间竞争较多的情况,因为这种场景下,乐观锁的大量重试,会导致 CPU 被占用,故而可以使用悲观锁提前锁住资源,会有更高的效率。

锁系列:一、悲观 / 乐观锁原理与运用相关推荐

  1. 字节二面 —— 什么是同步锁、死锁、乐观锁、悲观锁

    马上就要到金三银四佳季了,是找工作的好时候,小伙伴们一定要把握好时机,找到心仪的高薪工作.找工作就少不了面试,那我们从现在开始,多刷刷面试题,查缺补漏!!! 目录 1. 面向对象的特征 2. Java ...

  2. Java多线程锁技术漫谈:乐观锁VS悲观锁

    Java多线程技术一直是Java程序员必备的核心技能之一.在Java多线程编程中,为了保证数据的一致性和安全性,常常需要使用锁的机制来防止多个线程同时修改同一个共享资源.锁是实现并发访问控制的一种机制 ...

  3. 五分钟学会悲观乐观锁-java vs mysql vs redis三种实现

    1 悲观锁乐观锁简介 乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果 ...

  4. 分布式锁系列--04关于分布式锁的选型分析02-Redlock的实现原理

    欢迎关注公众号:java4all 上一文分布式锁系列–03关于分布式锁的选型分析01中,我们看到了单节点的redis分布式锁在failover时产生了无法解决的安全问题,因此,Redis的作者anti ...

  5. 悲观|乐观锁、自旋|互斥锁、公平|非公平锁

    解析锁--悲观|乐观锁.自旋|互斥锁.公平|非公平锁 悲观锁 总认为最坏的情况可能会出现,即认为数据很可能会被他人修改,因此在持有数据时总是先把资源或数据锁住.这样其他线程要请求这个资源时就会阻塞,直 ...

  6. mysql 悲观锁 性能_mysql的乐观锁和悲观锁

    悲观锁与乐观锁是两种常见的资源并发锁设计思路,也是并发编程中一个非常基础的概念.本文将对这两种常见的锁机制在数据库数据上的实现进行比较系统的介绍. 悲观锁(Pessimistic Lock) 悲观锁的 ...

  7. 常⻅锁策略(1. 乐观锁 悲观锁2. 公平锁 非公平锁3. 读写锁4. 可重入锁 自旋锁)

    目录 1. 乐观锁 & 悲观锁 1.1乐观锁定义 1.2 乐观锁实现 -- CAS 1.3 悲观锁定义和应⽤ 2. 公平锁 & 非公平锁 3. 读写锁 3.1 读写锁 3.2 独占锁 ...

  8. java中乐观锁_java中的乐观锁的研究总结

    前段时间有人问我java中的乐观锁和悲观锁的问题,我被问愣神了,乐观锁和悲观锁我到是听说过,在数据库里面应用极广,但是java里面就好像没有听说过,后来我详细去看了下<java编程思想>, ...

  9. java oracle 乐观锁,oracle为什么默认乐观锁

    本帖最后由 bfc99 于 2015-7-6 11:08 编辑 1.无论是选择悲观锁策略,还是乐观锁策略.如果一个对象被上了锁,那么该对象都会受这个锁的控制和影响.如果这个锁是个排它锁,那么其它会话都 ...

最新文章

  1. Java 时间和日期类型的 Hibernate 映射
  2. php域名黑名单,thinkphp 6 IP 黑名单功能
  3. No module named ‘tensorflow_hub‘
  4. spring mvc事务没有生效的原因
  5. try not let others think you are good enough
  6. 51nod---无法表示的数
  7. 文献记录(part18)--3D neuron tip detection in volumetric microscopy images using an adaptive ...
  8. 消息(6)——WCF,构建简单的WCF服务,MTOM编码
  9. [C++]MySQL数据库操作实例
  10. mysql中交集,并集,差集,左连接,右连接
  11. POJ-10031004
  12. swagger 使用
  13. Exception in thread http-apr-8080-exec-
  14. selenium窗口截图操作
  15. oracle 数据库为nomount状态,oracle 数据库为nomount状态
  16. 《哥德尔、艾舍尔、巴赫——集异璧之大成》
  17. 在css表格怎么居中对齐,css居中和对齐方法集锦
  18. ssm框架搭建连接mysql_搭建SSM框架(一) - xiaoqi__y的个人空间 - OSCHINA - 中文开源技术交流社区...
  19. 如何更改python界面颜色_pycharm修改界面主题颜色的方法 pycharm怎么恢复默认设置...
  20. [教程]配置青鸟云Web服务器

热门文章

  1. vuetify图标大全
  2. 【OpenGL】实现三维空间漫游和立方体、球体贴图
  3. 安卓中的虚拟键盘实现,KeyEvent的事件分发、处理机制。EditText是如何将KeyEvent事件转为字符输入的?
  4. Jquery each continu
  5. 304、bootstrap 之 图片样式
  6. kwgt 歌词_kwgt桌面插件美化下载-Eight for kwgt专业版主题包v3.9.136.1 最新版-腾飞网...
  7. LFtoolBox0.4工具包解码Lytro光场图像及子孔径图像获取
  8. 人生必读十大启迪故事
  9. 上海泛微软件面试经历
  10. 斧子演示:如何取消导出高清视频的限制