锁分类

  • 悲观锁与乐观锁

    • 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题
    • 乐观锁则认为对于同一个数据的并发操作,有可能不会发生修改的。在更新数据的时候,会采用尝试更新,不加锁的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的
  • 可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,可一定程度避免死锁
  • 共享锁与独占锁
    • 独享锁是指该锁一次只能被一个线程所持有
    • 共享锁是指该锁可被多个线程所持有
  • 互斥锁与读写锁:独享锁/共享锁的具体实现
  • 公平锁与非公平锁
    • 公平锁是指多个线程按照申请锁的顺序来获取锁
    • 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,可能会造成优先级反转或者饥饿现象
  • 分段锁:一种锁的设计,并不是具体的一种锁,对于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

java 指令集

  1. 对于同步方法
  • JVM采用ACC_SYNCHRONIZED标记符来实现同步。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁(monitor),然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被堵塞。
  • 同步方法是隐式的,会在运行时常量池中的method_info结构体中存放ACC_SYNCHRONIZED标识符access_flags
  1. 对于同步代码块,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对象头

  1. Klass Word(类指针):存储对象的类型指针,该指针指向它的类元数据。从JDK 1.6 update14开始,64位的JVM正式支持了-XX:+UseCompressedOops(默认开启),可以压缩指针,起到节约内存占用的作用。oop(ordinary object pointer)即普通对象指针,下列指针将压缩至32位:
  • 每个Class的属性指针(静态成员变量)
  • 每个对象的属性指针(对象变量)
  • 普通对象数组的每个元素指针
  1. 指针压缩:
  • 如果GC堆大小在4G以下,直接砍掉高32位,避免了编码解码过程(偏移量除以/乘以8)
  • 如果GC堆大小在4G以上32G以下,则启用-XX:+UseCompressedOops命令
  • 如果GC堆大小大于32G,压指失效,使用原来的64位
  1. -XX:+UseCompressedClassPointers
  • Java8使用Metaspace存储元数据,开启后类元信息中的指针也用32bit的Compressed版本,即Klass Word
  • 依赖-XX:+UseCompressedOops
  1. 数组长度64位JVM的情况下也被压缩至32位
  2. 对齐字节:HotSpot VM的自动内存管理要求对象大小必须是8字节的整数倍,不足时需要对齐填充来补全

  1. 32位虚拟机占用32个字节,不同状态下各个比特位区间大小有变化
  2. biased_lock:偏向锁标记,为1时表示对象启用偏向锁
  3. age:默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15
  4. identity_hashcode
  • 采用延迟加载技术,只有在需要时使用System.identityHashCode(Object x)计算后写到该对象头中
  • 偏向锁没有存储HashCode的地方,偏向锁期间调用System.identityHashCode(x)会造成锁升级
  • 轻量级锁和重量级锁所指向的lock record或monitor都有存储HashCode的空间
  • 用户自定义hashCode()方法所返回的值不存在Mark Word中,只针对identity hash code
  1. thread:持有偏向锁的线程ID
  2. epoch:偏向锁的时间戳
  3. ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针
  4. 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("!");
}
}
  1. 逃逸分析缺点:不能保证逃逸分析的性能收益必定高于它的消耗
  2. 逃逸分析还可以用于:
  • 标量替换:把不存在逃逸的对象拆散,将成员变量恢复到基本类型,直接在栈上创建若干个成员变量
  • 栈上分配:目前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进行设置

同步状态获取流程图

  1. 右下方【线程进入等待状态】的流程图中“结束”不是真正意思上的结束,外层是一个死循环。只有前驱节点为头结点,且获取同步状态成功才会退出循环。节点的就绪、挂起、获取同步都是在循环里完成的,很重要!!!
  2. 响应中断获取同步状态只是在中断检测的处理方式上不同,Thread.interrupted()检测到中断状态后直接抛出了InterruptedException
  • LockSupport.park(),线程挂起
  • 调用Thread的interrupt方法,设置中断标识为true,且内部会调用Parker::unpark(),唤醒挂起线程
  • 调用Thread.interrupted()返回并清除中断标识
  • 中断状态为true抛出异常
  1. 超时限制获取同步状态在循环中计算超时时间,超时中断循环,并且线程挂起使用的是限时挂起LockSupport.parkNanos,1000 nanoseconds内自旋不挂起,时间较短,没有必要挂起和唤醒)
公平和非公平模式下的区别
graph TB
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((进队列))

同步状态释放
graph TB
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并发编程之锁机制的更多相关文章

  1. Java并发编程:锁的释放

    Java并发编程:锁的释放 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: #839496;} Ja ...

  2. 并发编程的锁机制:synchronized和lock

    1. 锁的种类 锁的种类有很多,包括:自旋锁.自旋锁的其他种类.阻塞锁.可重入锁.读写锁.互斥锁.悲观锁.乐观锁.公平锁.可重入锁等等,其余就不列出了.我们重点看如下几种:可重入锁.读写锁.可中断锁. ...

  3. Java并发编程-各种锁

    安全性和活跃度通常相互牵制.我们使用锁来保证线程安全,但是滥用锁可能引起锁顺序死锁.类似地,我们使用线程池和信号量来约束资源的使用, 但是缺不能知晓哪些管辖范围内的活动可能形成的资源死锁.Java应用 ...

  4. java并发编程:锁的相关概念介绍

    理解同步,最好先把java中锁相关的概念弄清楚,有助于我们更好的去理解.学习同步.java语言中与锁有关的几个概念主要是:可重入锁.读写锁.可中断锁.公平锁 一.可重入锁 synchronized和R ...

  5. Java并发编程:Concurrent锁机制解析

    Java并发编程:Concurrent锁机制解析 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: # ...

  6. Java并发编程(05):悲观锁和乐观锁机制

    本文源码:GitHub·点这里 || GitEE·点这里 一.资源和加锁 1.场景描述 多线程并发访问同一个资源问题,假如线程A获取变量之后修改变量值,线程C在此时也获取变量值并且修改,两个线程同时并 ...

  7. 转: 【Java并发编程】之二十:并发新特性—Lock锁和条件变量(含代码)

    简单使用Lock锁 Java5中引入了新的锁机制--Java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作.Lock接 ...

  8. [转载] java并发编程:Lock(线程锁)

    作者:海子 原文链接: http://www.cnblogs.com/dolphin0520/p/3923167.html 出处:http://www.cnblogs.com/dolphin0520/ ...

  9. Java并发编程(十一)-- Java中的锁详解

    上一章我们已经简要的介绍了Java中的一些锁,本章我们就详细的来说说这些锁. synchronized锁 synchronized锁是什么? synchronized是Java的一个关键字,它能够将代 ...

随机推荐

  1. HDU4578 Transformation(多标记线段树)题解

    题意: 操作有:\(1\).区间都加\(a\):\(2\).区间都乘\(a\):\(3\).区间都重置成\(a\):\(4\).询问区间幂次和\(\sum_{i=l}^rnum[i]^p(p\in\{ ...

  2. how to auto open demo and create it in a new codesandbox

    how to auto open demo and create it in a new codesandbox markdown & iframe https://ant.design/do ...

  3. CSS Multiple Columns

    CSS Multiple Columns CSS layout column-count column-gap column-rule-style column-rule-width column-r ...

  4. HTML 5.3

    HTML 5.3 W3C Working Draft, 18 October 2018 https://www.w3.org/TR/html53/ refs https://www.w3.org/TR ...

  5. API 注解 & Java API annotation

    API 注解 & Java API annotation 注解 annotation

  6. 星空值、SPC、算力组成三元永动机制!VAST带你把握时代!

    目前中心化金融体系为用户提供的服务在便捷性和易用性方面已经达到了新高度,但随着时代发展,大众对于金融安全性和可控性的需求进一步提升,需要去中心化金融服务商来提供更具创意的解决方案.盛大公链为此在应用层 ...

  7. Spring—Document root element "beans", must match DOCTYPE root "null"分析及解决方法

    网上很多人说要把applicationContex.xml文件中加上如下第二行的<!DOCTYPE/>标签,说明DTD.<?xml version="1.0" e ...

  8. Django Admin 配置和定制基本功能(基本二次开发配置)

    一 列表显示页面  1. list_display,列表时,定制显示的列 @admin.register(models.UserInfo) class UserAdmin(admin.ModelAdm ...

  9. jenkins+docker+nginx+tomcat实现vue项目部署

    一.项目准备 1.新建一个vue的项目,确保能在浏览器正常访问.然后在项目的根目录下新建一个Dockerfile的文件,内容如下 FROM nginx COPY dist /usr/share/ngi ...

  10. 在 c++ 程序中出现CtrIsValidHeapPointer问题

    在c++程序中出现CtrIsValidHeapPointer问题, 我发现的原因是申请了大量动态数组但是并没有把他们初始化 为数组赋初始值便可以很好解决这一问题.