【重难点】【JUC 04】synchronized 原理、ReentrantLock 原理、synchronized 和 Lock 的对比、CAS 无锁原理
【重难点】【JUC 04】synchronized 原理、ReentrantLock 原理、synchronized 和 Lock 的对比、CAS 无锁原理
文章目录
- 【重难点】【JUC 04】synchronized 原理、ReentrantLock 原理、synchronized 和 Lock 的对比、CAS 无锁原理
- 一、synchronized 原理
- 1.概述
- 2.实现原理
- 二、ReentrantLock 原理
- 1.概述
- 2.重入性实现原理
- 3.公平锁和非公平锁
- 三、synchronized 和 Lock 的对比
- 1.synchronized 和 ReentrantLock
- 2.synchronized 和 Lock
- 四、CAS 无锁原理
- 1.为什么要使用 CAS
- 2.CAS 原理分析
- 3.JDK 中的 CAS 实现
- 4.JVM 中的 CAS
- 5.数据库中的乐观锁机制
一、synchronized 原理
1.概述
synchronized 是 Java 最常用的保证线程安全的方式,synchronized 的作用主要有两方面
- 确保线程互斥地访问代码块,同一时刻只有一个方法可以进入到临界区
- 保证共享变量的修改的可见性
synchronized 有三种用法:
- 修饰普通方法,锁的是当前实例对象
- 修饰静态方法,锁的是当前 Class 对象
- 修饰代码块,锁的是小括号里的对象
public class SynTest{private static List<String> list = new ArrayList<String>();//当前实例的锁public synchronized void add1(String s){list.add(s);}//SynTest.class 锁public static synchronized void add2(String s){list.add(s);}//Synchronized.class 锁public void add3(String s){synchronized(SynTest.class){list.add(s);}}//当前实例的锁public void add4(String s){synchronized(this){list.add(s);}}
}
2.实现原理
synchronized 同步代码块的语义底层是基于对象内部的监视器锁(monitor),通过 monitorenter 和 monitorexit 指令完成。wait 和 notify 方法也依赖于 monitor 对象,所以其必须要在 synchronized 同步代码块内使用。monitorenter 指令在编译为字节码后插入到同步代码块的开始位置,monitorexit 指令在编译为字节码后插入到同步代码块结束处和异常处。JVM 保证每个 monitorenter 必须有对应的 monitorexit
monitorenter
每个对象都有一个 monitor,当 monitor 被某个线程占用时就会处于锁定状态,线程执行 monitorenter 指令时会尝试获取 monitor 的所有权,即尝试获取对象的锁,过程如下:
- 如果 monitor 的进入数为 0,则该线程进入 monitor,然后将进入数设置为 1,该线程成为 monitor 的所有者
- 如果线程已经是 monitor 的所有者,只是重新进入,则 monitor 的进入数 +1
- 如果其它线程已经占用 monitor,则该线程进入阻塞状态,直至 monitor 的进入数为 0,再重新尝试获取 monitor 的所有权
monitorexit
执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者。执行指令时,monitor 的进入数减 1,如果减 1 后进入数为 0,则该线程退出 monitor,不再是该 monitor 的所有者,其它被这个 monitor 阻塞的线程可以尝试获取这个 monitor 的所有权
线程状态和状态转化
在 HotSpot JVM 中,monitor 由 ObjectMonitor 实现,其主要数据结构如下:
ObjectMonitor() {_header = NULL;_count = 0; //记录个数_waiters = 0,_recursions = 0;_object = NULL;_owner = NULL; //持有monitor的线程_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet_WaitSetLock = 0 ;_Responsible = NULL ;_succ = NULL ;_cxq = NULL ;FreeNext = NULL ;_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表_SpinFreq = 0 ;_SpinClock = 0 ;OwnerIsThread = 0 ;
}
ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,用来保存 ObjectWaiter 对象列表(每个等待锁的线程都会被封装成 ObjectWaiter 对象),_owner 指向持有 ObjectMonitor 对象的线程
- 当多个线程同时访问一段同步代码时,首先会进入 _EntryList,等待锁处于阻塞状态
- 当线程获取到对象的 monitor 后进入 The Owner 区域,并把 ObjectMonitor 中的 _owner 变量设置为当前线程,同时 monitor 中的计数器 count 加 1
- 若线程调用 wait() 方法,将释放当前持有的 monitor,_owner 变量恢复为 null,count 减 1,同时该线程进入 _WaitSet 集合中等待被唤醒,处于 waiting 状态
- 若当前线程执行完毕,将释放 monitor 并复位变量的值,以便其它线程进入获取 monitor
二、ReentrantLock 原理
1.概述
ReentrantLock 重入锁,实现了 Lock 接口,在实际编程中使用频率很高,支持重入性,即当前线程获取锁后再次获取同一把锁不会被阻塞。此外,ReentrantLock 还支持公平锁和非公平锁。因此,想要弄懂 ReentrantLock 的原理就是要弄懂其重入性的实现原理以及公平锁和非公平锁
2.重入性实现原理
支持重入性,需要解决两个问题:
- 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接获取成功
- 由于锁会被获取很多次,那么在锁被释放的时候也要释放同样的次数
ReentrantLock 的实现依赖于 AQS,以非公平锁为例,ReentrantLock 判断是否可以获取锁的核心方法为 nonfairTryAcquir
fianl bolean nonfairTryAcquire(int acquires){fianl Thread current = Thread.currentThread();int c = getState();//1.如果该锁未被任何线程占有,该锁能被当前线程获取if(c == 0){if(compareAndSetState(0, acquires)){setExclusiveOwnerThread(current);return true;}}//2.若被占有,检查占有线程是否是当前线程else if(current == getExclusiveOwnerThread()){//3.再次获取,计数加一int nextc = c + acquires;if(nextc < 0) //overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}
释放锁的核心方法为 tryRelease
protected final boolean tryRelease(int releases){//1.同步状态减 1int c = getState() - releases;if(Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if(c == 0){//2.只有当同步状态为 0 时,锁成功释放,返回 truefree = true;setExclusiveOwnerThread(null);}//3.锁未被完全释放,返回 falsesetState(c);return free;
}
3.公平锁和非公平锁
何为公平,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序
ReentrantLock 的无参构造默认非公平
public ReentrantLock(){sync = new NonfairSync();
}
有参构造提供了一个 boolean 类型的 fair 参数,true 为公平锁,false 为非公平锁
public ReentrantLock(boolean fair)Psync = fair ? new FairSync() : new NonfairSync();
}
nonfairTryAcquire 方法只是简单地获取了当前状态进行了一些逻辑判断,并没有考虑当前同步队列中线程等待的情况,我们对比一下公平锁的处理逻辑,其核心方法为 tryAcquire
protected 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;
}
这段代码的逻辑与 nonfairTryAcquire 基本上一致,唯一的不同在于增加了 hasQueuedPredecessors 的逻辑判断。hasQueuedPredecessors 方法用于判断当前节点在同步队列中是否有前驱节点,如果有前驱节点,说明有线程比当前线程更早地请求资源,根据公平性,当前线程请求资源失败
- 公平锁每次获取到锁的总是同步队列中的第一个节点,保证了线程请求资源按照时间上的绝对顺序,只要程序一直正常运行下去,每一个请求资源的线程都可以获取到资源。反之,非公平锁会导致某些线程长时间获取不到资源,造成 “饥饿” 现象
- 公平锁为了保证时间上的绝对顺序,需要频繁地进行上下文切换,而非公平锁会降低一定的上下文切换,从而降低性能开销。因此,ReentrantLock 默认选择的是非公平锁,保证了系统更大的吞吐量
三、synchronized 和 Lock 的对比
1.synchronized 和 ReentrantLock
synchronized | ReentrantLock | |
---|---|---|
性能 | 相对较差 | 相对较好 |
公平性 | 只支持非公平锁 | 同时支持公平锁与非公平锁 |
是否可重入 | 支持 | 支持 |
尝试获取锁的支持 | 不支持,一旦到了同步块,且没有获取到锁,就阻塞在这里 | 支持,通过 tryLock 方法实现,可通过其返回值判断是否成功获取锁,所以即使获取锁失败也不会阻塞在这里 |
超时的获取锁 | 不支持,如果一直获取不到锁,就会一直等待下去 | 支持,通过 tryLock(time, TimeUnit) 方法实现,如果超时了还没获取锁,就放弃获取锁,不会一直阻塞下去 |
是否可响应中断 | 不支持,不可响应线程的 interrupt 信号 | 支持,通过 lockInterruptibly 方法实现,通过此方法获取锁之后,线程可响应 interrupted 信号,并抛出 InterruptedException 异常 |
等待条件的支持 | 支持,通过 wait、notify、notifyAll 来实现 | 支持,通过 Condition 接口实现,支持多个 Condition,比 synchronized 更加灵活 |
2.synchronized 和 Lock
相同点
- 都是用来保护资源线程安全的
- 都可以保证可见性
不同点
- 用法不同
synchronized 可以作用于方法和代码块上,且 synchronized 的加锁和解锁是隐式的,尤其是抛异常的时候也能保证释放锁;Lock 接口必须显式使用 Lock 对象加锁 lock() 和解锁 unlock(),一般需要在 finally 块中 unlock(),反之抛异常的时候发生死锁 - 加解锁顺序不同
对于 Lock 而言,如果有多把 Lock 锁,Lock 可以不完全按照加锁的反序解锁;synchronized 无法做到,synchronized 解锁的顺序必须和加锁的顺序完全相反 - synchronized 不够灵活
一旦 synchronized 锁已经被某个线程获得了,此时其他线程如果还想获得,那它只能被阻塞,直到持有锁的线程运行完毕或者发生异常从而释放这个锁。如果持有锁的线程持有很长时间才释放,那么整个程序的运行效率就会降低;相比之下,Lock 类在等待的过程中,如果使用的是 lockInterruptibly 方法,那么可以不等待,直接中断退出,还可以调用 tryLock 方法尝试获取锁,如果获取不到锁也可以做别的事 - synchronized 锁只能同时被一个线程拥有,但是 Lock 锁没有这个限制
例如读写锁的读锁,是可以同时被多个线程持有的 - 原理不同
synchronized 是内置锁,由 JVM 实现获取锁和释放锁;Lock 根据实现不同,有不同的原理,例如 ReentrantLock 内部是通过 AQS 来获取和释放锁的 - synchronized 是非公平锁,且不可以设置公平锁,而 Lock 的某些实现类可以选择设置公平锁还是非公平锁
- 性能区别
在 JDK 5 之前,synchronized 的性能比较低,但是到了 JDK 6,synchronized 进行了很多优化,比如自适应自选、锁消除、锁粗化、轻量级锁、偏向锁等,所以后期版本的 synchronized 的性能并不比 Lock 差
如何选择?
- 如果能不用,最好既不使用 Lock 也不使用 synchronized。因为在许多情况下你可以使用 JUC 包中的机制,它会为你处理所有的加锁和解锁操作,也就是推荐优先使用工具类来加解锁
- 如果 synchronized 关键字适合你的程序,那么请尽量使用它,这样可以减少代码量,减少出错的概率。因为一旦忘记在 finally 里 unlock,代码可能会出很大的问题,而使用 synchronized 更安全
- 如果特别需要 Lock 的特殊功能,比如尝试获取锁、可中断、超时功能,才使用 Lock
四、CAS 无锁原理
1.为什么要使用 CAS
在多线程高并发编程的时候,最关键的问题就是保证临界区的对象的安全访问。通常是用加锁来处理,其实加锁本质上是将并发转变为串行来实现的,势必会影响吞吐量。而且线程的数量是有限的,依赖于操作系统,而且线程的创建和销毁带来的性能损耗不可忽视
对于并发控制而言,锁是一种悲观策略,会阻塞线程执行。而无锁是一种乐观策略,它会假设对资源的访问不会发生冲突,既然没有冲突,就不需要等待,线程也不会阻塞。但实际上是会发生冲突的,那该怎么办呢?无锁的策略采用一种比较交换技术 CAS(Compare And Swap)来鉴别线程冲突,一旦检测到冲突,就重试当前操作,直到没有冲突
与锁相比,CAS 会使得程序设计比较复杂,但是由于其优越的性能优势、天生免疫死锁,没有线程竞争开销以及线程间频繁调度开销,所以在目前被广泛应用
2.CAS 原理分析
CAS 算法
一个 CAS 方法包含三个参数 CAS(V, E, N),V 表示要更新的变量,E 表示预期的值,N 表示新值。只有当 V 的值等于 E 时,才会将 V 的值修改为 N。如果 V 的值不等于 E,说明已经被其它线程修改了,当前线程可以放弃此操作,也可以再次尝试操作直至修改成功。基于这样的算法,CAS 操作即使没有锁,也可以发现其它线程对当前线程的干扰(可以通过 volatile 保证可见性),并进行恰当的处理
AtomicInteger
我们借助这个类讲解 CAS 原理,查看一下 AtomicInteger 源码的核心部分
private volatile int valuepublic final int getAndSet(int newValue){\for(;;){int current = get();if(compareAndSet(current, newValue))return current;}
}public fianl boolean compareAndSet(int expect, int update){return unsafe.compareAndSwaoInt(this, valueOffset, expect, update);
}
- AromicInteger 中真正存储数据的是 value 变量,且被 volatile 修饰,保证了线程之间的可见性。而 Integer 中的 value 是被 final 修饰的,是不可变对象
- getAndSet 方法通过一个死循环不断尝试赋值操作。而真正的赋值操作交给了 unsafe 类实现
unsafe
从名字可知,这个类是不安全的,我们看一下它的构造方法
public static Unsafe getUnsafe(){Class var0 = Reflection.getCallerClass();if(var0,getClassLoader() != null){throw new SecurityException("Unsafe");}else{return theUnsafe;}
}
如果类加载器不是 null 则直接抛出异常,也就是说我们无法在应用程序中使用这个类
我们再来看一个 compareAndSwapInt 的方法声明
public final native boolean compareAndSwapInt(Object var1, long var2, int var3, int var4);
第一个参数是给定的对象,第二个参数是对象内的偏移量(其实就是一个字段到对象头部的偏移量,通过这个偏移量可以快速定位字段),第三个参数是期望值,最后一个是要设置的值
其实这里 Unsafe 封装了一些类似于 C++ 中指针的东西,该类中的方法都是 native 的,而且是原子操作
3.JDK 中的 CAS 实现
- JUC 下的 atomic 包
该包下的类都是采用 CAS 实现的无锁 - JUC 下的跳跃表
ConcurrentSkipListMap 采用典型的空间换时间策略,它是一个有序的,支持高并发的 Map,它对节点的操作都是通过 CAS 机制实现的 - JUC 下的无锁队列
ConcurrentLinkedQueue
4.JVM 中的 CAS
堆中对象的分配
详见 【JVM】第二章 JVM类加载、JVM对象
在单线程情况下,分配对象有两种策略:
- 指针碰撞
- 空闲列表
但是 JVM 不可能一直在单线程状态下运行,那样效率太差了。由于再给一个对象分配内存的时候不是原子性的操作,至少需要以下几步:查找空闲列表、分配内存、修改空闲列表等,这是不安全的。解决并发时的安全问题也有两种策略:
- CAS
采用 CAS 配合上失败重试的方式保证更新操作的原子性 - TLAB
使用 CAS 对性能有一定的影响,所以 JVM 又提出了一种更高级的优化策略:每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲区(TLAB),线程内部需要分配内存时直接在 TLAB 上分配就行,避免了线程冲突,只有当缓冲区的内存用光需要重新分配内存的时候才进行 CAS 操作分配更多空间
5.数据库中的乐观锁机制
数据库中有悲观锁和乐观锁
悲观锁(Pressimistic Locking)
对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)
乐观锁(Optimistic Locking)
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。
乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据
【重难点】【JUC 04】synchronized 原理、ReentrantLock 原理、synchronized 和 Lock 的对比、CAS 无锁原理相关推荐
- 原理剖析(第 012 篇)Netty之无锁队列MpscUnboundedArrayQueue原理分析
原理剖析(第 012 篇)Netty之无锁队列MpscUnboundedArrayQueue原理分析 - 一.大致介绍 1.了解过netty原理的童鞋,其实应该知道工作线程组的每个子线程都维护了一个任 ...
- 【JUC多线程与高并发】线程进阶,性能优化之无锁
多线程进阶,性能优化之无锁 比较交换(CAS) 线程安全整数类:AtomicInteger CAS底层原理:Unsafe类 CAS缺点: ABA问题的解决 原子引用:AtomicReference 原 ...
- 12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁
小陈:呼叫老王...... 老王:来了来了,小陈你准备好了吗?今天我们来讲synchronized的锁重入.锁优化.和锁升级的原理 小陈:早就准备好了,我现在都等不及了 老王:那就好,那我们废话不多说 ...
- cas无法使用_【漫画】CAS原理分析!无锁原子类也能解决并发问题!
本文来源于微信公众号[胖滚猪学编程].转载请注明出处 在漫画并发编程系统博文中,我们讲了N篇关于锁的知识,确实,锁是解决并发问题的万能钥匙,可是并发问题只有锁能解决吗?今天要出场一个大BOSS:CAS ...
- synchronized和ReentrantLock区别浅析 (转载地址:http://blog.csdn.net/zmx729618/article/details/51594166)
一.什么是sychronized sychronized是java中最基本同步互斥的手段,可以修饰代码块,方法,类. 在修饰代码块的时候需要一个reference对象作为锁的对象. 在修饰方法的时候默 ...
- Java并发编程—无锁互斥机制及CAS原理
目录 一.CAS简介 二.AtomicInteger代码演示 三.CAS 实现 四.弊端 一.CAS简介 在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令 ...
- 【重难点】【Java基础 05】说一说你平时遇到的异常、什么情景下会用到反射、反射的底层原理
[重难点][Java基础 05]说一说你平时遇到的异常.什么情景下会用到反射.反射的底层原理 文章目录 [重难点][Java基础 05]说一说你平时遇到的异常.什么情景下会用到反射.反射的底层原理 一 ...
- reentrantlock原理_分享:synchronized和ReentrantLock的实现原理知识点
前言 通常呢,会在并发情况下,同时操作某一业务从而造成数据重复提交,业务混乱等问题,通常呢,遇到解决类似问题可采用加锁,限流等问题来解决,那么看看这篇关于java中关于锁中synchronized和R ...
- java多线程:9、synchronized、Lock的底层实现原理以及和volatile、Lock、ReentrantLock的区别?
文章目录 0.1.线程中安全性问题的体现: 0.2.线程安全问题的解决办法 1.synchronized的底层实现原理分析 2.Lock的底层实现原理分析? 3.synchronized和volati ...
最新文章
- guice google_与Google Guice的动手实践
- 电脑主板维修_自学电脑主板维修第45讲
- JavaScriptSerializer进行JSON序列化,得到字符串
- 4.5-4.9 磁盘格式化,磁盘挂载,手动增加swap空间
- 内存泄漏了,咋回事?
- 简而言之,JUnit:另一个JUnit教程
- Windows 任务栏缩略图自定义程序[更新 Build20100830]
- 集合python_python集合访问的方法
- 写代码抽取代码的技巧
- 如何使Git使用我选择的编辑器进行提交?
- windows下用navicat远程链接虚拟机Linux下MySQL数据库
- matlab 波束图,Matlab波束形成程序
- 学习笔记(02):程序员的数学:微积分-常用导数(二):最常用到的技巧
- c#窗体开发俄罗斯方块小游戏
- 交互媒体专题设计大作业
- [NIPS 2018] Delta Encoder: An Effective Sample Synthesis Method for Few Shot Object Recognition
- html天气js,H5 实现天气效果(心知天气插件)
- HFDS 内部工作机制
- javascript实现页面刷新
- ssm报错:No qualifying bean of type ‘com.hr.service.LoginService‘ available
热门文章
- ubuntu 安装java_Hadoop3.1.3安装教程_单机/伪分布式配置
- python编写es脚本_Elasticsearch 参考指南(如何使用脚本)
- 实木地板被机器人弄成坑_防腐木地板怎选择 防腐木地板怎样安装
- jenkins 集成java搅拌_java-Jenkins中的集成测试
- Asp.Net Core 轻松学-玩转配置文件
- 浅谈相对定位与绝对定位
- 炎热天气看书还是钓鱼?隐马尔科夫模型教你预测!
- WPF MVVM模式
- win7 远程桌面 复制粘贴
- 照片转换为动画 html5,如何使用html5让图片转圈的动画效果