Java并发编程之锁机制
锁分类
- 悲观锁与乐观锁
- 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题
- 乐观锁则认为对于同一个数据的并发操作,有可能不会发生修改的。在更新数据的时候,会采用尝试更新,不加锁的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的
- 可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,可一定程度避免死锁
- 共享锁与独占锁
- 独享锁是指该锁一次只能被一个线程所持有
- 共享锁是指该锁可被多个线程所持有
- 互斥锁与读写锁:独享锁/共享锁的具体实现
- 公平锁与非公平锁
- 公平锁是指多个线程按照申请锁的顺序来获取锁
- 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,可能会造成优先级反转或者饥饿现象
- 分段锁:一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作
- 偏向锁/轻量级锁/重量级锁
- 自旋锁:当锁被占用时,当前想要获取锁的线程不会被立即挂起,而是做几个空循环,看持有锁的线程是否会很快释放锁。默认次数为10次,可以通过参数-XX:PreBlockSpin来调整
- 自适应自旋锁:自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
Java中锁实现
synchronized
原理探究
synchronized加锁的四种方式
public class SynchronizedTest {
// 静态方法上加synchronized关键字,锁是当前类的class对象
public static synchronized void sync1() {
}
// 普通方法上加synchronized关键字,锁是当前实例对象,与sync1仅作用域不同
public synchronized void sync2() {
}
// synchronized代码块,锁是括号里面的对象
public void sync3() {
synchronized (this) {
}
}
// synchronized代码块,class上加锁,与sync3仅作用域不同
public void sync4() {
synchronized (SynchronizedTest.class) {
}
}
}
查看汇编代码,执行javac -encoding UTF-8 SynchronizedTest.java、javap -v SynchronizedTest.class
public static synchronized void sync1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED // ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 19: 0
public synchronized void sync2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 23: 0
public void sync3();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0 // 将第一个引用类型本地变量推送至栈顶
1: dup // 复制栈顶一个字长的数据,将复制后的数据压栈
2: astore_1
3: monitorenter // 加锁
4: aload_1
5: monitorexit // 释放锁
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return
public void sync4();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
// ldc将常量池中#2推送至栈顶,即class com/SynchronizedTest
0: ldc #2 // class com/SynchronizedTest
2: dup
3: astore_1
4: monitorenter // 加锁
5: aload_1
6: monitorexit // 释放锁
7: goto 15
10: astore_2
11: aload_1
12: monitorexit // 释放锁
13: aload_2
14: athrow
15: return
- 对于同步方法
- JVM采用ACC_SYNCHRONIZED标记符来实现同步。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁(monitor),然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被堵塞。
- 同步方法是隐式的,会在运行时常量池中的method_info结构体中存放ACC_SYNCHRONIZED标识符access_flags
- 对于同步代码块,JVM采用monitorenter、monitorexit两个指令来实现同步
Monitor
无论是同步方法还是同步代码块都是基于监视器Monitor实现
在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)
ObjectMonitor中有几个关键属性:
- _owner:指向持有ObjectMonitor对象的线程
- _WaitSet:存放处于wait状态的线程队列
- _EntryList:存放处于等待锁block状态的线程队列
- _recursions:锁的重入次数
- _count:用来记录该线程获取锁的次数
ObjectMonitor中有几个关键方法:
- enter(TRAPS)
- exit(TRAPS)
- wait(jlong millis, bool interruptable, TRAPS)
- nofity(TRAPS)
- notifyAll(TRAPS)
解决三大问题
- 保证原子性:同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到
- 保证有序性:单线程重排序要遵守as-if-serial语义,而synchronized修饰的代码,同一时间只能被同一线程访问
- 保证可见性
- 线程解锁前,必须把共享变量的最新值刷新到主内存中
- 线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值
synchronized锁优化
Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到内核态,状态转换需要花费很多的处理器时间。
Java对象头
- Klass Word(类指针):存储对象的类型指针,该指针指向它的类元数据。从JDK 1.6 update14开始,64位的JVM正式支持了-XX:+UseCompressedOops(默认开启),可以压缩指针,起到节约内存占用的作用。oop(ordinary object pointer)即普通对象指针,下列指针将压缩至32位:
- 每个Class的属性指针(静态成员变量)
- 每个对象的属性指针(对象变量)
- 普通对象数组的每个元素指针
- 指针压缩:
- 如果GC堆大小在4G以下,直接砍掉高32位,避免了编码解码过程(偏移量除以/乘以8)
- 如果GC堆大小在4G以上32G以下,则启用-XX:+UseCompressedOops命令
- 如果GC堆大小大于32G,压指失效,使用原来的64位
- -XX:+UseCompressedClassPointers
- Java8使用Metaspace存储元数据,开启后类元信息中的指针也用32bit的Compressed版本,即Klass Word
- 依赖-XX:+UseCompressedOops
- 数组长度64位JVM的情况下也被压缩至32位
- 对齐字节:HotSpot VM的自动内存管理要求对象大小必须是8字节的整数倍,不足时需要对齐填充来补全
- 32位虚拟机占用32个字节,不同状态下各个比特位区间大小有变化
- biased_lock:偏向锁标记,为1时表示对象启用偏向锁
- age:默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15
- identity_hashcode
- 采用延迟加载技术,只有在需要时使用System.identityHashCode(Object x)计算后写到该对象头中
- 偏向锁没有存储HashCode的地方,偏向锁期间调用System.identityHashCode(x)会造成锁升级
- 轻量级锁和重量级锁所指向的lock record或monitor都有存储HashCode的空间
- 用户自定义hashCode()方法所返回的值不存在Mark Word中,只针对identity hash code
- thread:持有偏向锁的线程ID
- epoch:偏向锁的时间戳
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针
// 引入依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.11</version>
</dependency>
// 查看对象布局信息
ClassLayout layout = ClassLayout.parseInstance(new A());
System.out.println(layout.toPrintable());
可通过ClassLayout查看对象布局信息,即对象占用空间情况
锁升级机制
锁升级是单向的: 无锁 -> 偏向锁 -> 轻量级锁(自旋锁和自适应自旋锁) -> 重量级锁
持有偏向锁的线程不会主动释放偏向锁,而是等待其他线程来竞争才会释放锁。这样偏向锁保证了总是同一个线程多次获取锁的情况下,每次只需要检查标志位就行,效率很高
当检测到偏向锁且不归属当前线程,会暂停原持有偏向锁线程,检测其执行状态:
- 如果偏向锁线程已退出同步代码块,清除偏向锁标识,转为无锁状态,由当前线程获取轻量级锁
- 如果偏向锁线程未退出同步代码块,由其膨胀为轻量级锁
锁消除
JIT编译器借助逃逸分析(Escape Analysis)技术来判断同步块所使用的锁对象是否只能够被一个线程访问,如果被证实,就会取消对这部分代码的同步
public class EscapeAnalysis {
public static Object object;
public Object methodEscape1() { // 方法逃逸:方法返回值逃逸
return new Object();
}
public Object methodEscape2() { // 方法逃逸:作为参数传递到其它方法中
Object object=new Object();
xxx(object)
}
public void threadEscape1() {// 线程逃逸:赋值给类变量
object = new Object();
}
public void threadEscape2() { // 线程逃逸:其他线程中访问的实例变量
Object obj=new Object();
new Thread(() -> xxx(obj)).start();
}
public void eliminate1() { // o未逃逸,可自动清除锁
Object o=new Object();
synchronized (o){
xxx();
}
}
public void eliminate2() { // buffer未逃逸,append操作加锁可自动清除锁
StringBuffer buffer=new StringBuffer();
buffer.append("hello");
buffer.append("world");
buffer.append("!");
}
}
- 逃逸分析缺点:不能保证逃逸分析的性能收益必定高于它的消耗
- 逃逸分析还可以用于:
- 标量替换:把不存在逃逸的对象拆散,将成员变量恢复到基本类型,直接在栈上创建若干个成员变量
- 栈上分配:目前Hotspot并没有实现真正意义上的栈上分配,实际上是标量替换。栈上分配随着方法结束而自动销毁,垃圾回收压力减小
锁粗化
尽量减小锁的粒度,可以避免不必要的阻塞。但是如果在一段代码中连续的用同一个监视器锁反复的加锁解锁,甚至加锁操作出现在循环体中的时候,就会导致不必要的性能损耗,这种情况就需要锁粗化。
for(int i=0;i<100000;i++){
synchronized(this){
do();
}
会被粗化成:
synchronized(this){
for(int i=0;i<100000;i++){
do();
}
显式锁Lock
特性
特性 | API |
---|---|
能响应中断 | lockInterruptbly() |
非阻塞式的获取锁 | tryLock() |
支持超时 | tryLock(long time, timeUnit) |
可实现公平锁 | ReentrantLock(ture) |
可以绑定多个条件 | newCondition() |
使用范式
Lock lock = new ReentrantLock();
lock.lock();
try{
...
}finally{
lock.unlock();
}
原理分析
基于AbstractQueuedSynchronizer,队列同步器实现
好处
可重写方法
同步状态获取/修改
setState():拥有锁的线程调用本身具有原子性,不需要使用cas进行设置
同步状态获取流程图
- 右下方【线程进入等待状态】的流程图中“结束”不是真正意思上的结束,外层是一个死循环。只有前驱节点为头结点,且获取同步状态成功才会退出循环。节点的就绪、挂起、获取同步都是在循环里完成的,很重要!!!
- 响应中断获取同步状态只是在中断检测的处理方式上不同,Thread.interrupted()检测到中断状态后直接抛出了InterruptedException
- LockSupport.park(),线程挂起
- 调用Thread的interrupt方法,设置中断标识为true,且内部会调用Parker::unpark(),唤醒挂起线程
- 调用Thread.interrupted()返回并清除中断标识
- 中断状态为true抛出异常
- 超时限制获取同步状态在循环中计算超时时间,超时中断循环,并且线程挂起使用的是限时挂起LockSupport.parkNanos,1000 nanoseconds内自旋不挂起,时间较短,没有必要挂起和唤醒)
公平和非公平模式下的区别
A((非公平模式))-->B{CAS抢锁}
B-->|成功|D((成功))
B-->|失败|C{state=0}
C-->|是|G{抢锁}
G-->|成功|D
G-->|失败|F
C-->|否|E{重入}
E-->|是|D
E-->|否|F((进队列))
A1((公平模式))-->C1{state=0}
C1-->|是|B1{AQS队列}
B1-->|为空|D1{CAS抢锁}
B1-->|不为空|F1
D1-->|成功|E1((成功))
D1-->|失败|F1
C1-->|否|G1{重入}
G1-->|是|E1
G1-->|否|F1((进队列))
同步状态释放
A((开始))-->B{tryRelease}
B-->|成功|D{后继节点}
D-->|为空或者取消状态|E[倒序查找有效后继节点]
D-->|否|F[唤醒后继节点]
B-->|失败|C[失败]
E-->F
- tryRelease:getState() - releases是否等于0,即释放后state==0
- 后继节点:头结点的后继节点,修改head的waitStatus=0并唤醒后继节点(head的waitStatus=0不执行唤醒后继节点)。非公平模式下,被唤醒的后继节点有可能抢锁失败,会再次把head的waitStatus修改为-1,自旋再次抢锁,若再失败线程挂起,等待下次唤醒
- 倒序查找有效后继节点
- 节点加入双向队列时,双向链表的建立非原子操作,先建立的是Prev指针(正常查找可能找不到该节点)
- 取消节点时,先断开的是Next指针,Prev指针并未断开(查找可能中断)
同步队列与等待队列
ReentrantReadWriteLock
- 支持写锁降级,不支持读锁升级
- 非公平读锁下为了避免写锁饥饿,会判断头节点的下一个节点是否为排他节点(即写请求),如果是,当前的读锁堵塞
- 每个线程持有读锁的次数,使用ThreadLocal记录
- 使用写锁时需要先释放读锁,如果有两个读取锁试图获取写入锁,且都不释放读取锁时,就会发生死锁。因为写锁是排它锁,两个线程都会因为有其他线程持有读锁而无法获取写锁
- 读锁不支持Condition(读锁在某一时刻最多可以被多个线程拥有,对于读锁而言,其他线程没有必要等待获取读锁,等待唤醒是毫无意义的)
StampedLock
JDK 8新增的读写锁StampedLock,跟读写锁ReentrantReadWriteLock不同,它并不是由AQS实现,有三种访问模式:
- 写锁writeLock:功能和读写锁的写锁类似
- 悲观读锁readLock:功能和读写锁的读锁类似
- 乐观读锁Optimistic reading:一种优化的读模式,解决读锁和写锁互斥问题
StampedLock可以将三种模式是锁进行有条件的互相转换
- tryConvertToWriteLock():将其他锁转换为写锁
- 当前邮戳为持有写锁模式,直接返回当前的邮戳;
- 当前邮戳为持有读锁模式,则会释放读锁并获取写锁,并返回写锁邮戳;
- 当前邮戳持有乐观锁,通过CAS立即获取写锁,成功则返回写锁邮戳;失败则返回0;
- tryConvertToReadLock:将其他锁转换为读锁
- 当前邮戳为持有写锁模式,则会释放写锁并获取读锁,并返回读锁邮戳;
- 当前邮戳为持有读锁模式,则直接返回当前读锁邮戳;
- 当前邮戳持有乐观锁,通过CAS立即获取读锁,则返回读锁邮戳;否则,获取失败返回0;
- tryConvertToOptimisticRead:将其他锁转换为乐观锁
- 当前邮戳为持有读或写锁,则直接释放读写锁,并返回释放后的观察者邮戳值;
- 当前邮戳持有乐观锁,若乐观锁邮戳有效,则返回观察者邮戳;
Oracle 官方的例子:
class Point {
private double x, y;// 成员变量
private final StampedLock sl = new StampedLock();// 锁实例
/**
* 写锁writeLock
* 添加增量,改变当前point坐标的位置。
* 先获取到了写锁,然后对point坐标进行修改,然后释放锁。
* 写锁writeLock是排它锁,保证了其他线程调用move函数时候会被阻塞,直到当前线程显示释放了该锁,也就是保证了对变量x,y操作的原子性。
*/
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
/**
* 乐观读锁tryOptimisticRead
* 计算当前位置到原点的距离
*/
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // 尝试获取乐观读锁(1)
double currentX = x, currentY = y; // 将全部变量拷贝到方法体栈内(2)
// 检查票据是否可用,即写锁有没有被占用(3)
if (!sl.validate(stamp)) {
// 如果写锁被抢占,即数据进行了写操作,则重新获取
stamp = sl.readLock();// 获取悲观读锁(4)
try {
// 将全部变量拷贝到方法体栈内(5)
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);// 释放悲观读锁(6)
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);// 真正读取操作,返回计算结果(7)
}
/**
* 悲观读锁readLock
* 如果当前坐标为原点则移动到指定的位置
*/
void moveIfAtOrigin(double newX, double newY) {
long stamp = sl.readLock();// 获取悲观读锁(1)
try {
// 如果当前点在原点则移动(2)
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp);// 尝试将获取的读锁升级为写锁(3)
if (ws != 0L) {
// 升级成功,则更新票据,并设置坐标值,然后退出循环(4)
stamp = ws;
x = newX;
y = newY;
break;
} else {
// 读锁升级写锁失败,则释放读锁,显示获取独占写锁,然后循环重试(5)
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);// 释放写锁(6)
}
}
}
参考:
Java并发编程之锁机制的更多相关文章
- Java并发编程:锁的释放
Java并发编程:锁的释放 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: #839496;} Ja ...
- 并发编程的锁机制:synchronized和lock
1. 锁的种类 锁的种类有很多,包括:自旋锁.自旋锁的其他种类.阻塞锁.可重入锁.读写锁.互斥锁.悲观锁.乐观锁.公平锁.可重入锁等等,其余就不列出了.我们重点看如下几种:可重入锁.读写锁.可中断锁. ...
- Java并发编程-各种锁
安全性和活跃度通常相互牵制.我们使用锁来保证线程安全,但是滥用锁可能引起锁顺序死锁.类似地,我们使用线程池和信号量来约束资源的使用, 但是缺不能知晓哪些管辖范围内的活动可能形成的资源死锁.Java应用 ...
- java并发编程:锁的相关概念介绍
理解同步,最好先把java中锁相关的概念弄清楚,有助于我们更好的去理解.学习同步.java语言中与锁有关的几个概念主要是:可重入锁.读写锁.可中断锁.公平锁 一.可重入锁 synchronized和R ...
- Java并发编程:Concurrent锁机制解析
Java并发编程:Concurrent锁机制解析 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: # ...
- Java并发编程(05):悲观锁和乐观锁机制
本文源码:GitHub·点这里 || GitEE·点这里 一.资源和加锁 1.场景描述 多线程并发访问同一个资源问题,假如线程A获取变量之后修改变量值,线程C在此时也获取变量值并且修改,两个线程同时并 ...
- 转: 【Java并发编程】之二十:并发新特性—Lock锁和条件变量(含代码)
简单使用Lock锁 Java5中引入了新的锁机制--Java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作.Lock接 ...
- [转载] java并发编程:Lock(线程锁)
作者:海子 原文链接: http://www.cnblogs.com/dolphin0520/p/3923167.html 出处:http://www.cnblogs.com/dolphin0520/ ...
- Java并发编程(十一)-- Java中的锁详解
上一章我们已经简要的介绍了Java中的一些锁,本章我们就详细的来说说这些锁. synchronized锁 synchronized锁是什么? synchronized是Java的一个关键字,它能够将代 ...
随机推荐
- POJ 2778 DNA Sequence(AC自动机 + 矩阵快速幂)题解
题意:给出m个模式串,要求你构造长度为n(n <= 2000000000)的主串,主串不包含模式串,问这样的主串有几个 思路:因为要不包含模式串,显然又是ac自动机.因为n很大,所以用dp不太好 ...
- vue项目webpack打包后修改配置文件
从webpack打包结构中我们知道,vue中有一个存放外部资源的文件夹static,它里面的文件是不会被打包编译的,所以我们就可以利用外部引入js的方式将我们的想要的数据在index.html中以js ...
- Set DSL in Ubuntu 18.04
Reference Solutions: Ctrl+Atl+t Type nmcli con edit type pppoe con-name ANY_NAME_OF_DSL_YOU_LIKE, wh ...
- javascript & call & apply & bind & new
javascript & call & apply & bind & new Javascript call() & apply() vs bind()? ht ...
- Arctic Code Vault Contributor
Arctic Code Vault Contributor GitHub Archive Program https://archiveprogram.github.com/ Preserving o ...
- ASCII Art
ASCII Art https://npms.io/search?q=ASCII art ASCII Art Text to ASCII Art Generator (TAAG) http://pat ...
- how to write string to file in bash
how to write string to file in bash https://stackoverflow.com/questions/22713667/shell-script-how-to ...
- H5 下拉刷新、加载更多
H5 下拉刷新.加载更多 demos const autoLoadMore = (url = ``) => { // todo ... } refs xgqfrms 2012-2020 www. ...
- taro & Block
taro & Block https://nervjs.github.io/taro/docs/children.html#注意事项-1 import Taro, { Component, E ...
- 图解 git 流程
图解 git 流程 Github 开源项目 1 动画 2 web repl 3 online git cli & create remote branch # Create a new bra ...