详解Java锁的升级与对比(1)——锁的分类与细节(结合部分源码)
前言
之前只是对Java各种锁都有所认识,但没有一个统一的整理及总结,且没有对“锁升级”这一概念的加深理解,今天趁着周末好好整理下之前记过的笔记,并归纳为此博文,主要参考资源为《Java并发编程的艺术》与《Java多线程编程核心技术》,有需要的朋友可以私信评论我,这个是有书签的PDF电子版!
一、Java锁的分类及简单介绍
平时大家都知道的锁一般都有:CAS锁,synchronized锁,ReentranLock锁等,但是并没有了解各自的用处与一些细节,这里用XMind画一个图,并做一个简单的总结。
1、悲观锁与乐观锁
乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。
先说概念。对于同一个数据的并发操作:
- 悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
- 而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试),乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的
根据从上面的概念描述我们可以发现:
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
代码举例:ReentranLock中采用lock()与unlock方法锁住同步资源块
注意事项:
正确的写法:一开始就ock()方法,紧跟着try...finally,unlock()方法一定要在finally{}中的第一行。
错误的写法:lock没有紧跟着try...语句;没有一开始就lock()方法锁住资源
2、自旋锁与适应性自旋锁
(1)自旋锁
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
自旋锁的缺点:
从概念上来看,就知道自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。简单来说就是自旋的次数跟时间超过一定的阈值就可能浪费处理器的资源。
自旋锁的CAS实现:
在AtomicInteger源码中就使用了CAS思想(实际上就是调用unsafe中方法),采用do-while循环(这是一个CAS常用的do{}while(){},还有就是for(;;){if(...) return}),这里就是一个CAS操作,首先do{...}读取值,之后在通过循环while中CAS自旋修改值,直到成功为止。
自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
(2)自适应自旋锁
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。
如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源,即根据实际情况,决定自旋的时间。
3、公平锁与非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁(排队获取锁)。
- 公平锁的优点是等待锁的线程不会饿死。
- 缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景(尝试获取锁,不行就重新进队尾等待)。
- 非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。
- 缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
我们先结合ReentranLock中的源码结构及部分源码分析下,可以得到以下两点:
(1)实际上,ReentranceLock中有一个内部类Sync,ReentranceLock添加锁/释放锁等关键操作都是由它完成的,并且它继承了AQS(AbstractQueuedSynchronizer,这是一个很重要的能学到很多知识的需要好好分析源码的类,之后会抽时间好好分析),源码注释有有这么一句话:Synchronizer providing all implementation mechanics。
(2)ReentrantLock它还有公平锁FairSync和非公平锁NonfairSync两个子类,ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。
接下来我们分别看公平锁与非公平锁加锁的实现对比:
公平锁加锁方法: 非公平锁加锁方法:
我们可以清晰的看出有一个公平锁中有一个hasQueuePredecessors()方法:判断当前线程是否是队头,不是的话不会去处理。这也就是公平锁与非公平锁最大的区别。
4、可重入锁与非可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁 。
关键字synchronized在使用时,当一个线程得到一个对象锁后,再次请求对象锁时是可以再次得到该对象锁的,这也证明synchronized块/方法的内部调用本类的其它synchronized方法/块时,是可以永远得到锁的。
编写测试类:
运行结果:
service1()
service2()
service3()
从结果上来说,“可重入锁”概念就是:自己能够再次获取自己的内部锁,同时当存在子类继承关系时,子类也完全可以通过“可重入锁”调用父类的同步方法的。
好了,以上都是通过synchronized关键字的举例,接下来我们同样采用对比的方法对比ReentranLock部分关键源码来说明可重入锁与非可重入锁细节。实际上ReentranLock是没有非可重入锁的实现的,那么我们可以对比下类比就行。
ReentranLock中可重入锁与非可重入锁与区别(结合源码)
ReentrantLock继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。当线程尝试获取锁时:
可重入锁:
- 可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。
- 判断如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // state为0锁处于空闲状态
if (compareAndSetState(0, acquires)) {
// 获取成功之后,当前线程是该锁的持有者
setExclusiveOwnerThread(current);
return true;
}
}
// 锁不是空闲状态,但是当前线程是该锁的持有者的话,实现可重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // state+1 可重入数
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true; // 返回true,表示获取锁成功(可重入的)
}
return false;
}
非可重入锁:
- 是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞
/**
* 类似可重入操作类比出非可重入操作
* @param acquires
* @return
*/
final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 非重入锁直接尝试获取该锁
if (compareAndSetState(0, acquires)) { // 这里acquires实际就是1,对state+1
// 获取之后设置为持有者,返回true,表示成功,其它情况都是false,即不能被重入了
setExclusiveOwnerThread(current);
return true;
}
// 锁不是空闲状态,但是当前线程是该锁的持有者的话,实现可重入
else if {
return false;
}
}
释放锁时,同样都是线程先尝试获取当前status的值,并判断当前线程是不是持有锁的线程的前提下
可重入锁:
- 执行判断status-1==0,如果true则说明所有重复所有锁的操作已经完成,接下来就是真正的释放锁,如果为false说明还有内部持有锁的操作未完成。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 保证释放锁的必须是当前线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 释放后state为0,则持有者置为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 否则设置重置后的state
setState(c);
return free;
}
非可重入锁:
- 只需要判断是不是当前持有锁的线程,是的话status=0,锁释放操作完成
/**
* 类似可重入锁释放锁操作 得到非可重入锁操作
* @param releases
* @return
*/
protected final boolean tryRelease(int releases) {
// 保证释放锁的必须是当前线程
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
} else {
// 非可重入锁释放锁直接将持有者置为null
setExclusiveOwnerThread(null);
// state直接置为0
setState(0);
return true;
}
}
---------------------------------------------未完待续,下一个继续介绍共享锁与排它锁(结合部分源码),同时重点介绍锁升级------------------------------------------------------------
详解Java锁的升级与对比(1)——锁的分类与细节(结合部分源码)的更多相关文章
- 实例详解 Java 死锁与破解死锁
锁和被保护资源之间的关系 我们把一段需要互斥执行的代码称为临界区.线程在进入临界区之前,首先尝试加锁 lock(),如果成功,则进入临界区,此时我们称这个线程持有锁:否则呢就等待,直到持有锁的线程解锁 ...
- 「跬步千里」详解 Java 内存模型与原子性、可见性、有序性
文题 "跬步千里" 主要是为了凸显这篇文章的基础性与重要性(狗头),并发编程这块的知识也确实主要围绕着 JMM 和三大性质来展开. 全文脉络如下: 1)为什么要学习并发编程? 2) ...
- 详解Java GC的工作原理+Minor GC、FullGC
详解Java GC的工作原理+Minor GC.FullGC 引用地址:http://www.blogjava.net/ldwblog/archive/2013/07/24/401919.html J ...
- Protocol Buffer技术详解(Java实例)
Protocol Buffer技术详解(Java实例) 该篇Blog和上一篇(C++实例)基本相同,只是面向于我们团队中的Java工程师,毕竟我们项目的前端部分是基于Android开发的,而且我们研发 ...
- 详解Java中的clone方法
详解Java中的clone方法 参考:http://blog.csdn.net/zhangjg_blog/article/details/18369201/ 所谓的复制对象,首先要分配一个和源对象同样 ...
- java基础(十五)----- Java 最全异常详解 ——Java高级开发必须懂的
本文将详解java中的异常和异常处理机制 异常简介 什么是异常? 程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常. Java异常的分类和类结构图 1.Java中的所 ...
- 异常处理器详解 Java多线程异常处理机制 多线程中篇(四)
在Thread中有异常处理器相关的方法 在ThreadGroup中也有相关的异常处理方法 示例 未检查异常 对于未检查异常,将会直接宕掉,主线程则继续运行,程序会继续运行 在主线程中能不能捕获呢? 我 ...
- Mysql加锁过程详解(9)-innodb下的记录锁,间隙锁,next-key锁
Mysql加锁过程详解(1)-基本知识 Mysql加锁过程详解(2)-关于mysql 幻读理解 Mysql加锁过程详解(3)-关于mysql 幻读理解 Mysql加锁过程详解(4)-select fo ...
- 第三节:带你详解Java的操作符,控制流程以及数组
前言 大家好,给大家带来带你详解Java的操作符,控制流程以及数组的概述,希望你们喜欢 操作符 算数操作符 一般的 +,-,*,/,还有两个自增 自减 ,以及一个取模 % 操作符. 这里的操作算法,一 ...
- 第十八节:详解Java抽象类和接口的区别
前言 对于面向对象编程来说,抽象是它的特征之一. 在Java中,实现抽象的机制分两种,一为抽象类,二为接口. 抽象类为abstract class,接口为Interface. 今天来学习一下Java中 ...
随机推荐
- 标签中colgroup的属性
如需对全部列应用样式,<colgroup> 标签很有用,这样就不需要对各个单元和各行重复应用样式了. <colgroup> 标签只能在 table 元素中使用. 可选的属性 属 ...
- centos7源码编译安装LNMP+ZABBIX4.0LTS(1)——nginx
环境:192.168.117.132--zabbix server192.168.117.133--zabbix proxy 安装路径为/zabbix 安装nginx 1.安装包下载http://ng ...
- BMP位图调色板说明
网上一搜,可以看到BMP位图结构的详细说明,这篇文章专门谈一下其中的调色板. 多少位位图并不是指每一个颜色该用多少位表示,对于颜色来说,它始终都是24位(RGB),或者是32位(RGBA),而是指该位 ...
- 手写webpack核心原理,再也不怕面试官问我webpack原理
手写webpack核心原理 目录 手写webpack核心原理 一.核心打包原理 1.1 打包的主要流程如下 1.2 具体细节 二.基本准备工作 三.获取模块内容 四.分析模块 五.收集依赖 六.ES6 ...
- better-scroll 与 Vue 结合
什么是 better-scroll better-scroll 是一个移动端滚动的解决方案,它是基于 iscroll 的重写,它和 iscroll 的主要区别在这里.better-scroll 也很强 ...
- Spring 最常用的 7 大类注解,哪些你还不知道?
随着技术的更新迭代,Java5.0开始支持注解.而作为java中的领军框架spring,自从更新了2.5版本之后也开始慢慢舍弃xml配置,更多使用注解来控制spring框架. 而spring的的注解那 ...
- 当浏览器窗口大小发生变化时,重新绘制JsPlumb中的线条、端点
1 window.addEventListener('resize', () => { 2 this.plumbInstance.repaintEverything() 3 }) 参考文章:ht ...
- 运行Apache时出现the requested operation has failed
在修改自己主机E:\wamp\apache\conf中的httpd.conf中的站点位置后,重新运行Apache时,出现对话框提示"the requested operation has f ...
- modbus协议开关量采集模块
modbus协议开关量采集模块是指的使用Modbus协议的进行信号的采集与控制的一种设备. Modbus 协议设备都具有唯一的 Modbus 地址,众山 DTU 默认 Modbus 地址为 100,用 ...
- Java学习的第十一天
1. 方法递归 2.this()不理解和类型传递不太理解 3.明天学习完第四章