-
乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候回判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
-
悲观锁
悲观锁就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。java 中的悲观锁就是 Synchronized,AQS 框架下的锁则是先尝试 CAS 乐观锁去获取锁,获取不到,才会转换为悲观锁,如 ReentantLock。
-
自旋锁
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等待持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核切换的消耗。
线程自旋是需要消耗 CPU 的,换句话说就是让 CPU 做无用功,如果一直获取不到锁,那线程也不能一直占用 CPU 自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
-
Synchronized 同步锁
Synchronized 它可以把任意一个非 NULL 的对象当做锁。它属于独占式的悲观锁,同时是可重入锁。
-
Synchronized 核心组件
- Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中
- Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中
- OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为 OnDeck
- Owner:当前已经获取到锁资源的线程被称为 Owner
- !Owner:当前释放锁的线程
-
Synchronized 实现
- JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程。
- Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定 EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
- Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在 JVM 中也把这种选择行为称之为“竞争切换”。
- OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的则仍然停留在 EntryList 中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify 或者 notifyAll 唤醒,会重新进去 EntryList 中。
- 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)
- Synchronized 是非公平锁。Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源
- 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分后加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
- Synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。
- JDK1.6,Synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 JDK1.7 与 JDK1.8 中,均对该关键字的实现机理做了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
- 锁可以从偏向锁升级到轻量级锁,在升级到重量级锁。这种升级过程称为锁膨胀。
- JDK1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。
-
-
ReentantLock
ReentantLock 继承接口 Lock 并实现了接口中定义的方法,它是一种可重入锁,除了能完成 Synchronized 锁能完成的所有工作之外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
Lock 接口的主要方法
- void lock():执行此方法时,如果锁处于空闲状态,当前线程将获取到锁,相反,如果锁已经被其他线程持有,将禁用当前线程,直到当前线程获取到锁。
- boolean tryLock():如果锁可用,则获取锁,并立即返回 true,否则返回 false,该方法和 lock()的区别在于,tryLock()只是“试图”获取锁,如果锁不可用,不会导致当前线程被禁用,当前线程仍然继续往下执行代码。而 lock()方法则是一定要获取到锁,如果锁不可用,就一直等待,在未获得锁之前,当前线程并不继续向下执行。
- void unlock():执行此方法时,当前线程将释放持有的锁,锁只能由持有者释放,如果线程并不持有锁,却执行该方法,可能导致异常的发生。
- Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将释放锁
- getHoldCount():查询当前线程保持此锁的次数,也就是此线程执行 lock 方法的次数。
- getQueueLength():返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个线程获得了锁,此时返回的是 9
- getWaitQueueLength():(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了 condition 对象的 await 方法,那么此时执行此方法返回 10。
- hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条件(Condition),对于指定 condition 对象,有多少线程执行了 condition.await()方法。
- hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
- hasQueuedThreads():是否有线程等待此锁
- isFair():该锁是否是公平锁
- isHeldByCurrentThread():当前线程是否保持锁锁定,线程执行 lock()方法的前后分别是 false 和 true。
- isLock():此锁是否有任意线程占用。
- lockInterruptibly():如果当前线程未被中断,获取锁。
- tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁
- tryLock(long timeout, TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,则获取该锁。
非公平锁
JVM 按随机、就近原则分配锁的机制被称为非公平锁,ReentrantLock 在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则常用非公平锁的分配机制
公平锁
公平锁指锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁,ReentrantLock 在构造函数中提供了是否公平锁的初始化方式来定义公平锁
ReentrantLock 和 Synchronized
- ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与 Synchronized 通过 JVM 自动解锁的机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。
- ReentrantLock 相比 Synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要使用 ReentrantLock。
-
Semaphore 信号量
Semaphore 是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore 可以用来构建一些对象池,资源池之类的,比如数据库连接池
实现互斥锁
我们可以创建技术为 1 的 Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态
Semaphore 与 ReentrantLock
Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也与之类似,通过 acquire()与 release()方法类获取和释放临界资源。经实测,Semaphore.acquire()方法默认为可响应中断锁,与 ReentrantLock.lockInterruptibly()作用效果一致,换句话说在等待临界资源的过程中可以被 Thread.interrupt()方法中断。
此外,Semaphore 也实现了可轮询的锁请求与定时锁的功能,除了方法名 tryAcquire 与 tryLock 不同,其使用方法与 ReentrantLock 几乎一致。Semaphore 也提供了公平与非公平锁的机制,也可在构造函数中进行设定。
Semaphore 的锁释放操作也由手动进行,因此与 ReentrantLock 一样,为避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在 finally 代码块中完成
-
AutomicInteger
此处 AutomicInteger,一个提供原子操作的 Integer 的类,常见的还有 AutomicBoolean、AutomicLong、AutomicReference 等,他们的实现原理相同,区别在于运算对象类型的不同。另外,可以通过 AutomicReference将一个对象的所有操作转换成原子操作。
在多线程程序中,诸如 ++i 或 i++ 等运算不具有原子性,是不安全的线程操作之一。通常我们会使用 Synchronized 将该操作变成一个原子操作,但 JVM 为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。通过资料显示,通常 AutomicInteger 的性能是 ReentrantLock 的好几倍。
-
可重入锁(递归锁)
这里讲的是广义上的可重入锁,而不单指 JAVA 下的 ReentrantLock。可重入锁,也叫递归锁,指的是统一线程,外层函数获得锁之后,内层递归函数仍然可以获取该锁,不受影响。在 JAVA 环境下 ReentrantLock 和 Synchronized 都是可重入锁。
-
公平锁与非公平锁
公平锁
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
非公平锁
加锁时不考虑排队等待的问题,直接尝试获取锁,获取不到自动到队尾等待
- 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
- Java 中的 Synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的也是非公平锁。
-
ReadWriteLock 读写锁
为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制。如果没有写锁的情况下,读是无阻塞,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁。多个读锁不互斥,读锁和写锁互斥,这是有 JVM 自己控制的,我们只需要上好对应的锁即可。
读锁
如果我们的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁
写锁
如果我们的代码修改数据,只能一个人写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁。
Java 中读写锁有个接口 java.util.concurrent.locks.ReadWriteLock,也有具体的实现 ReetrantReadWriteLock。
-
共享锁和独占锁
Java 并发包提供的加锁模式分为独占锁和共享锁。
独占锁
独占锁模式下,每次只能有一个线程持有锁,ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
共享锁
共享锁则允许多个线程同时获取锁,并发访问共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
- AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列等待线程的锁获取模式。
- java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。
-
重量级锁
Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖底层操作系统的 Mutex Lock 来实现。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁”。JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。
-
轻量级锁
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。
锁升级
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。
“轻量级锁”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
-
偏向锁
Hotspot 的作者经过以往的研究发现:大多数情况下锁不仅不存在多线程竞争,而且总是由一线程多次获得。**偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。**引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。上面说过,轻量级锁为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
-
分段锁
分段锁并非是一种实际的锁,而是一种思想。ConcurrentHashMap 是学习分段锁的最好实践
-
锁优化
-
减少锁持有时间
只用在有线程安全要求的程序上加锁
-
减小锁粒度
将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁竞争,偏向锁,轻量级锁成功率才会提高。最经典的较小锁粒度的案例就是 ConcurrentHashMap。
-
锁分离
最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离程读锁和写锁。这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如 LinkedBlockingQueue 从头部取出,从尾部放数据。
-
锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡是有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的字眼,反而不利于性能的优化。
-
锁消除
锁消除是在编译器级别的事情。在即使编译时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。
-
近期热议
推荐标签 标签
-
OAuth
36 引用 • 103 回帖 • 5 关注
OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 oAuth 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 oAuth 是安全的。oAuth 是 Open Authorization 的简写。
-
小说
28 引用 • 108 回帖
小说是以刻画人物形象为中心,通过完整的故事情节和环境描写来反映社会生活的文学体裁。
-
C++
106 引用 • 152 回帖 • 2 关注
C++ 是在 C 语言的基础上开发的一种通用编程语言,应用广泛。C++ 支持多种编程范式,面向对象编程、泛型编程和过程化编程。
-
Hexo
21 引用 • 140 回帖 • 26 关注
Hexo 是一款快速、简洁且高效的博客框架,使用 Node.js 编写。
-
WebSocket
48 引用 • 206 回帖 • 408 关注
WebSocket 是 HTML5 中定义的一种新协议,它实现了浏览器与服务器之间的全双工通信(full-duplex)。
- 链书
-
小薇
34 引用 • 467 回帖 • 688 关注
小薇是一个用 Java 写的 QQ 聊天机器人 Web 服务,可以用于社群互动。
由于 Smart QQ 从 2019 年 1 月 1 日起停止服务,所以该项目也已经停止维护了!
-
大疆创新
2 引用 • 14 回帖 • 2 关注
深圳市大疆创新科技有限公司(DJI-Innovations,简称 DJI),成立于 2006 年,是全球领先的无人飞行器控制系统及无人机解决方案的研发和生产商,客户遍布全球 100 多个国家。通过持续的创新,大疆致力于为无人机工业、行业用户以及专业航拍应用提供性能最强、体验最佳的革命性智能飞控产品和解决方案。
-
JetBrains
18 引用 • 54 回帖 • 1 关注
JetBrains 是一家捷克的软件开发公司,该公司位于捷克的布拉格,并在俄国的圣彼得堡及美国麻州波士顿都设有办公室,该公司最为人所熟知的产品是 Java 编程语言开发撰写时所用的集成开发环境:IntelliJ IDEA。
-
JavaScript
710 引用 • 1173 回帖 • 193 关注
JavaScript 一种动态类型、弱类型、基于原型的直译式脚本语言,内置支持类型。它的解释器被称为 JavaScript 引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在 HTML 网页上使用,用来给 HTML 网页增加动态功能。
-
爬虫
106 引用 • 275 回帖
网络爬虫(Spider、Crawler),是一种按照一定的规则,自动地抓取万维网信息的程序。
-
正则表达式
31 引用 • 94 回帖
正则表达式(Regular Expression)使用单个字符串来描述、匹配一系列遵循某个句法规则的字符串。
-
外包
26 引用 • 232 回帖 • 17 关注
有空闲时间是接外包好呢还是学习好呢?
-
Redis
284 引用 • 247 回帖 • 211 关注
Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。
-
Typecho
12 引用 • 60 回帖 • 467 关注
Typecho 是一款博客程序,它在 GPLv2 许可证下发行,基于 PHP 构建,可以运行在各种平台上,支持多种数据库(MySQL、PostgreSQL、SQLite)。
-
Ubuntu
123 引用 • 168 回帖
Ubuntu(友帮拓、优般图、乌班图)是一个以桌面应用为主的 Linux 操作系统,其名称来自非洲南部祖鲁语或豪萨语的“ubuntu”一词,意思是“人性”、“我的存在是因为大家的存在”,是非洲传统的一种价值观,类似华人社会的“仁爱”思想。Ubuntu 的目标在于为一般用户提供一个最新的、同时又相当稳定的主要由自由软件构建而成的操作系统。
-
MyBatis
170 引用 • 414 回帖 • 430 关注
MyBatis 本是 Apache 软件基金会 的一个开源项目 iBatis,2010 年这个项目由 Apache 软件基金会迁移到了 google code,并且改名为 MyBatis ,2013 年 11 月再次迁移到了 GitHub。
-
七牛云
25 引用 • 215 回帖 • 160 关注
七牛云是国内领先的企业级公有云服务商,致力于打造以数据为核心的场景化 PaaS 服务。围绕富媒体场景,七牛先后推出了对象存储,融合 CDN 加速,数据通用处理,内容反垃圾服务,以及直播云服务等。
-
gRpc
10 引用 • 8 回帖 • 50 关注
-
机器学习
76 引用 • 37 回帖
机器学习(Machine Learning)是一门多领域交叉学科,涉及概率论、统计学、逼近论、凸分析、算法复杂度理论等多门学科。专门研究计算机怎样模拟或实现人类的学习行为,以获取新的知识或技能,重新组织已有的知识结构使之不断改善自身的性能。
-
Markdown
163 引用 • 1446 回帖
Markdown 是一种轻量级标记语言,用户可使用纯文本编辑器来排版文档,最终通过 Markdown 引擎将文档转换为所需格式(比如 HTML、PDF 等)。
-
Unity
25 引用 • 7 回帖 • 249 关注
Unity 是由 Unity Technologies 开发的一个让开发者可以轻松创建诸如 2D、3D 多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。
-
微服务
96 引用 • 155 回帖
微服务架构是一种架构模式,它提倡将单一应用划分成一组小的服务。服务之间互相协调,互相配合,为用户提供最终价值。每个服务运行在独立的进程中。服务于服务之间才用轻量级的通信机制互相沟通。每个服务都围绕着具体业务构建,能够被独立的部署。
-
以太坊
34 引用 • 367 回帖 • 2 关注
以太坊(Ethereum)并不是一个机构,而是一款能够在区块链上实现智能合约、开源的底层系统。以太坊是一个平台和一种编程语言 Solidity,使开发人员能够建立和发布下一代去中心化应用。 以太坊可以用来编程、分散、担保和交易任何事物:投票、域名、金融交易所、众筹、公司管理、合同和知识产权等等。
-
创造
171 引用 • 988 回帖
你创造的作品可能会帮助到很多人,如果是开源项目的话就更赞了!
-
MongoDB
90 引用 • 59 回帖 • 4 关注
MongoDB(来自于英文单词“Humongous”,中文含义为“庞大”)是一个基于分布式文件存储的数据库,由 C++ 语言编写。旨在为应用提供可扩展的高性能数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似 JSON 的 BSON 格式,因此可以存储比较复杂的数据类型。
-
坑
69 引用 • 93 回帖
一些有用的避坑指南。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于