class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="3"> class="hljs-ln-code"> class="hljs-ln-line">class TrainTicketSale { class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="4"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="5"> class="hljs-ln-code"> class="hljs-ln-line"> int remainTicketNum = 1_000; class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="6"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="7"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="8"> class="hljs-ln-code"> class="hljs-ln-line"> public synchronized boolean sale() { class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="9"> class="hljs-ln-code"> class="hljs-ln-line"> if (remainTicketNum > 0 ) { class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="10"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="11"> class="hljs-ln-code"> class="hljs-ln-line"> try { class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="12"> class="hljs-ln-code"> class="hljs-ln-line"> TimeUnit.MILLISECONDS.sleep(1); class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="13"> class="hljs-ln-code"> class="hljs-ln-line"> } catch (InterruptedException e) { class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="14"> class="hljs-ln-code"> class="hljs-ln-line"> e.printStackTrace(); class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="15"> class="hljs-ln-code"> class="hljs-ln-line"> } class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="16"> class="hljs-ln-code"> class="hljs-ln-line"> System.out.println(System.currentTimeMillis() + "\t" + Thread.currentThread().getName() + "\t剩:" + --remainTicketNum); class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="17"> class="hljs-ln-code"> class="hljs-ln-line"> return true; class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="18"> class="hljs-ln-code"> class="hljs-ln-line"> } class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="19"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="20"> class="hljs-ln-code"> class="hljs-ln-line"> return false; class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="21"> class="hljs-ln-code"> class="hljs-ln-line"> } class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="22"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="23"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="24"> class="hljs-ln-code"> class="hljs-ln-line"> public int getRemainTicketNum() { class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="25"> class="hljs-ln-code"> class="hljs-ln-line"> return remainTicketNum; class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="26"> class="hljs-ln-code"> class="hljs-ln-line"> } class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="27"> class="hljs-ln-code"> class="hljs-ln-line">} class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="28"> class="hljs-ln-code"> class="hljs-ln-line">public class T05_Juc_SynchronizedAtomic { class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="29"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="30"> class="hljs-ln-code"> class="hljs-ln-line"> public static void main(String[] args) throws Exception { class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="31"> class="hljs-ln-code"> class="hljs-ln-line"> TrainTicketSale sale = new TrainTicketSale(); class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="32"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="33"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="34"> class="hljs-ln-code"> class="hljs-ln-line"> System.out.println("火车票还剩下:" + sale.getRemainTicketNum() + " 张"); class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="35"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="36"> class="hljs-ln-code"> class="hljs-ln-line"> Vector vector = new Vector<>(10); class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="37"> class="hljs-ln-code"> class="hljs-ln-line"> for (int i = 0; i < 10; i++) { class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="38"> class="hljs-ln-code"> class="hljs-ln-line"> Thread thread = new Thread(() -> { class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="39"> class="hljs-ln-code"> class="hljs-ln-line"> while (true) { class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="40"> class="hljs-ln-code"> class="hljs-ln-line"> if (!sale.sale()) break; class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="41"> class="hljs-ln-code"> class="hljs-ln-line"> } class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="42"> class="hljs-ln-code"> class="hljs-ln-line"> }, "线程名:" + i); class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="43"> class="hljs-ln-code"> class="hljs-ln-line"> vector.add(thread); class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="44"> class="hljs-ln-code"> class="hljs-ln-line"> thread.start(); class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="45"> class="hljs-ln-code"> class="hljs-ln-line"> } class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="46"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="47"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="48"> class="hljs-ln-code"> class="hljs-ln-line"> for (Thread thread : vector) { class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="49"> class="hljs-ln-code"> class="hljs-ln-line"> thread.join(); class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="50"> class="hljs-ln-code"> class="hljs-ln-line"> } class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="51"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="52"> class="hljs-ln-code"> class="hljs-ln-line"> class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="53"> class="hljs-ln-code"> class="hljs-ln-line"> System.out.println(" Finally 火车票还剩下:" + sale.getRemainTicketNum() + " 张"); class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="54"> class="hljs-ln-code"> class="hljs-ln-line"> } class="hljs-ln-numbers"> class="hljs-ln-line hljs-ln-n" data-line-number="55"> class="hljs-ln-code"> class="hljs-ln-line">} class="hide-preCode-box">
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}" onclick="hljs.signin(event)">
测试一:没有使用 Synchronized 关键字同步售卖 sale() 方法,火车票出现了超卖现象。 
测试二:使用 Synchronized 关键字同步售卖 sale() 方法,火车票没有出现超卖问题了,解决共享资源在多线程下数据同步问题

1.1 为什么需要上锁?
多个线程去访问同一个资源的时候,访问某一段代码或者某临界资源的时候是需要有一把锁的概念的。
比如:我们对一个数字做递增,两个程序对它一块儿来做递增,递增就是把一个程序往上加1,如果两个线程共同访问的时候,第一个线程读它是0,然后把它加1,在自己线程内部内存里面算还没有写回去的时候,第二个线程读到了它还是0,加1再写回去,本来加了两次,但是还是1。
那么我们在对这个数字递增的过程中上把锁,就是说第一个线程对这个数字访问的时候是独占的,不允许别的线程来访问,不允许别的线程来对它进行计算,我必须加完1再释放锁,其他线程才能对它继续加。实质上,这把锁并不是对数字进行锁定的,你可以任意指定,想锁谁就锁谁。
1.2 Synchronized 哪些方式同步
我第一个程序是这么写的,如果说你想上了把锁之后才能对count进行减减访问,你可以new一个Object,所以这里锁定就是lock,当我拿到这把锁的时候才能执行这段代码。
1. Synchronized 修饰同步代码块:锁对象是 Synchronized 后面括号里配置的对象,这个对象可以是某个对象(xLock)

2.Synchronized 修饰同步代码块:锁对象是 Synchronized 后面括号里配置的对象,这个对象可以是某个对象(xLock)

3.Synchronized 修饰普通同步方法:锁对象当前实例对象,实际上是对调用该方法的对象加锁,俗称“对象锁”

4.Synchronized 修饰静态同步方法:锁对象是当前的类 Class 对象,实际上是对该类 Class 对象加锁,俗称“类锁”。

总结一下:Synchronized 锁的3种使用形式(使用场景):
- Synchronized 修饰普通同步方法:锁对象当前实例对象;
- Synchronized 修饰静态同步方法:锁对象是当前的类 Class 对象;
- Synchronized 修饰同步代码块:锁对象是 Synchronized 后面括号里配置的对象,这个对象可以是某个对象(xLock),也可以是某个类(TLock.class);
二、Synchronized实现原理
2.1 markword 内存布局
Java对象由三部分构成:对象头、实例数据、对齐补充。
- 对象头:存储对象自身运行时数据,如:哈希码(HashCode)、GC分代年龄、锁状态标志等;
- class pointer:类型指针,存储的是对象的属性信息,包括父类的属性信息,按照4字节对齐;
- 对齐补充:8字节对齐补充;
通过第一部分可以知道,Synchronized 不论是修饰方法还是代码块,都是通过持有修饰对象的锁来实现同步,那么 Synchronized锁对象是存在哪里的呢?答案是存在锁对象的对象头的 MarkWord中。那么 MarkWord在对象头中到底长什么样,也就是它到底存储了什么呢?
Synchronized 优化的过程和 markword 息息相关,用 markword 中最低的三位代表锁状态 其中1位是偏向锁位 两位是普通锁位

上图中的偏向锁和轻量级锁都是在 jdk 6 以后对锁机制进行优化时引进的,下文的锁升级部分会具体讲解,Synchronized 关键字对应的是重量级锁,接下来对重量级锁在 Hotspot JVM中的实现锁讲解。
2.2 Synchronized 在 jvm 实现原理
在之前的博客文章中也有介绍 Synchronized 相关知识:Java 内存模型 —— 用示例剖析 ,下面我们做一个 Synchronized 同步代码块的代码及对应的字节码,如下:

可以看出同步方法块在进入代码块时插入了 monitorenter 语句,在退出代码块时插入了 monitorexit 语句,为了保证不论是正常执行完毕(第6行)还是异常跳出代码块(第12行)都能执行monitorexit语句,因此会出现两句monitorexit语句。
同时,我们得出这样一个结论:Synchronized自动上锁,自动释放锁
出于兴趣,继续深入 monitorenter 细节,发现 monitorenter 是由C++ 实现的,在 InterpreterRuntime.cpp文件中第608行moniterenter 方法,moniterenter 方法说明,如果使用了偏向锁则执行 fast_enter;如果没有使用偏向锁则执行slow_enter。
三、Synchronized 锁升级
在 JDK早期,Synchronized 叫做重量级锁,jvm 向操作系统申请一把锁,都需要从用户态向系统态的一个转换 Synchronized(lock) 在jdk1.2之前,由OS帮忙管理线程,整个实现过程复杂涉及用户空间向系统空间申请锁,属于重量级锁。
在 JDK后期(jdk 1.6),Synchronized 有锁升级过程,无锁 -> 偏向锁 -> 自旋锁 -> 重量级锁(操作系统OS管理)。这样一来,避免一上来就跟系统空间申请锁资源,比如:单线程场景 或 多线程下竞争不激烈时,先由 jvm 来实现共享资源数据的同步,属于轻量级锁。
话不多说,直接 Synchronized 锁升级流程图,如下:

如上图所示,图中有偏向锁。默认情况 偏向锁启动有个时延,默认是4秒
why? 因为 JVM 虚拟机自己有一些默认启动的线程,里面有多好 sync 代码,这些 sync 代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断地进行锁撤销和锁升级的操作,效率较低,可以通过参数:-XX:BiasedLockingStartDelay=0 调整偏向锁启动时间,不过一般我们不会调节它的。
Synchronized 锁升级流程:
- 如果有线程上锁,上偏向锁,指的就是,把 markword 的线程ID改为自己线程ID的过程,偏向锁不可重偏向;
- 如果有线程竞争,撤销偏向锁,升级轻量级锁,线程在自己的线程栈生成 LockRecord,用 CAS 操作将 markword 设置为指向自己这个线程的 LR 的指针,设置成功者得到锁。
- 如果竞争加剧,竞争加剧:有线程超过10次自旋, -XX:PreBlockSpin, 或者自旋线程数超过CPU核数的一半,1.6之后,加入自适应自旋 adapative Self Spinning,jvm 自己控制
总结:
- new - 偏向锁 - 轻量级锁(也叫无锁,自旋锁,自适应自旋)- 重量级锁
- Synchronized 优化的过程和 markword 息息相关
- 用 markword 中最低的三位代表锁状态 其中1位是偏向锁位 两位是普通锁位
3.1 锁升级为什么这样设计?
锁的4种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)
3.1.1 偏向锁
为什么要引入偏向锁?
- 因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。
偏向锁的升级
当线程1 访问代码块并获取锁对象时,会在 java对象头和栈帧中记录偏向的锁的 threadID,因为偏向锁不会主动释放锁,因此以后线程1 再次获取锁的时候,需要比较当前线程的 threadID 和 Java 对象头中的 threadID 是否一致,如果一致(还是线程1获取锁对象),则无需使用 CAS 来加锁、解锁;如果不一致(其他线程,如线程2 要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1 的 threadID),那么需要查看 Java 对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
偏向锁撤销
为什么频繁的偏向锁撤销会导致STW时间增加呢?阅读偏向锁源码可以知道:偏向锁的撤销需要等待全局安全点(safe point),暂停持有偏向锁的线程,检查持有偏向锁的线程状态。首先遍历当前JVM的所有线程,如果能找到偏向线程,则说明偏向的线程还存活,此时检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁。可以看出撤销偏向锁的时候会导致stop the word。
偏向锁的优化(取消或延迟):
因为偏向锁的撤销是非常重的操作,甚至会导致STW,那么我们可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁。偏向锁默认是系统启动几秒钟后启用偏向锁,默认为4秒,原因在于,系统刚启动时,一般数据竞争是比较激烈的,此时启用偏向锁会降低性能,可以使用参数进行设置:-XX:BiasedLockingStartupDelay=5
3.1.2 轻量级锁(自旋锁)
为什么要引入轻量级锁?
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋等待锁释放。
偏向锁是否一定比自旋锁效率高?
不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁;JVM启动过程,会有很多线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段儿时间再打开。
为什么有自旋锁,还需要重要级锁?
自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗;重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源,重量级锁会通过操作系统,将线程放入等待队列_WaitSet
轻量级锁什么时候升级为重量级锁?
线程1 获取轻量级锁时会先把锁对象的对象头 MarkWord 复制一份到线程1 的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用 CAS 把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;
如果在线程1 复制对象头的同时(在线程1 CAS 之前),线程2 也准备获取锁,复制了对象头到线程2 的锁记录空间中,但是在线程2 CAS 的时候,发现线程1 已经把对象头换了,线程2 的 CAS 失败,那么线程2 就尝试使用自旋锁来等待线程1 释放锁。
但是如果自旋的时间太长也不行,因为自旋是要消耗 CPU 的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1 还没有释放锁,或者线程1 还在执行,线程2 还在自旋等待,这时又有一个线程3 过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
*注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。
这几种锁的优缺点(偏向锁、轻量级锁、重量级锁)

四、Synchronized 思维导图(可下载)
Synchronized 思维导图下载链接:Synchronized_思维导图(全面).xmind.zip
大纲如下:







文章最后,给大家推荐一些受欢迎的技术博客链接:
- JAVA相关的深度技术博客链接
- Flink 相关技术博客链接
- Spark 核心技术链接
- 设计模式 —— 深度技术博客链接
- 机器学习 —— 深度技术博客链接
- Hadoop相关技术博客链接
- 超全干货--Flink思维导图,花了3周左右编写、校对
- 深入JAVA 的JVM核心原理解决线上各种故障【附案例】
- 请谈谈你对volatile的理解?--最近小李子与面试官的一场“硬核较量”
- 聊聊RPC通信,经常被问到的一道面试题。源码+笔记,包懂
- 深入聊聊Java 垃圾回收机制【附原理图及调优方法】
欢迎扫描下方的二维码或 搜索 公众号“大数据高级架构师”,我们会有更多、且及时的资料推送给您,欢迎多多交流!


>>
评论记录:
回复评论: