一、Synchronized

对于多线程不安全(当数据共享(临界资源),而多线程同时访问并改变该数据时,就会不安全),JAVA提供的锁有两个,一个是synchronized关键字,另外一个就是lock类。JDK1.6之前,synchronized是一个重量级锁,在使用非自旋锁(互斥锁)时,“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。JDK1.6之后,synchronized进行了很大的优化,加入了偏置锁、轻量级锁、自旋锁等,大大提高了synchronized的性能。

JAVA锁综述:

  • 悲观锁:关系型数据库的读锁,读都要加锁
  • 乐观锁:读的时候不加锁,但是需要有版本号,例如Redis,SVN
  • 共享锁(读锁)和排它锁(写锁)是悲观锁的不同的实现,它俩都属于悲观锁的范畴。
   例如ReentrantReadWriteLock中包含读锁和写锁,其中读锁是可以多线程共享的,即共享锁,而写锁是排他锁,在更改时候不允许其他线程操作。读写锁其实是一把锁,所以会有同一时刻不允许读写锁共存的规定。之所以要细分读锁和写锁也是为了提高效率,将读和写分离,对比ReentrantLock就可以发现,无论并发读还是写,它总会先锁住全部再说。再如MySql中的行锁和表锁,行锁就会造成死锁(因为锁的范围小),表锁就不会造成死锁。
造成死锁原因:
1.A读资源,然后B也来读资源,然后A先提交update写锁,等B卸读锁,然后B非但没有卸下读锁,反而也提交update写锁,等A卸读锁,形成死锁
2.A对第一行进行update,此时B对第二行也进行update,A执行完后还继续要对第二行update,所以等B释放,B执行完后还继续要对第一行update,所以等A释放,死锁了
  • 阻塞锁:多个线程同时调用同一个方法的时候,所有线程都被排队处理了。让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。
  • 非阻塞锁:多个线程同时调用一个方法的时候,当某一个线程最先获取到锁,这时其他线程判断没拿到锁,这时就直接返回,只有当最先获取到锁的线程释放,其他线程才能进来,在它释放之前其它线程都会获取失败。

1.Synchronized 的作用主要有三个(原子性、可见性、有序性):

 - 确保线程互斥的访问同步代码- 保证共享变量的修改能够及时可见- 有效解决重排序问题

2.Synchronized可以把任何一个非null对象作为"锁",在HotSpot JVM实现中,锁有个专门的名字:对象监视器(Object Monitor)。从语法上讲,Synchronized 总共有三种用法:

 - 当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this);- 当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁;- 当synchronized作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例;

3.线程五大状态

  • 新建状态start:新建线程对象,并没有调用start()方法之前
  • 就绪状态waiting:调用start()方法之后线程就进入就绪状态,但是并不是说只要调用start()方法线程就马上变为当前线程,在变为当前线程之前都是为就绪状态。值得一提的是,线程在睡眠和挂起中恢复的时候也会进入就绪状态哦。
  • 运行状态running:线程被设置为当前线程,开始执行run()方法。就是线程进入运行状态
  • 阻塞状态blocking:线程被暂停,比如说调用sleep()方法后线程就进入阻塞状态
  • 死亡状态dead:线程执行结束

二、Synchronized底层实现

1.先了解(Java对象头)概念:
我们都知道对象是存放在堆内存中的,大致可以分为三个部分,分别是对象头、实例变量和填充字节
一图了解:

Epoch:偏向锁时间戳

2.对象监视器(monitor)
重量级锁对应的锁标志位是10,存储了指向重量级监视器锁的指针,在Hotspot中,对象的监视器(monitor)锁对象由ObjectMonitor对象实现(C++),其跟同步相关的数据结构如下:

线程(synchronized修饰的方法(代码块))在获取锁的几个状态的转换:

  • 1.当多个线程同时访问该方法,那么这些线程会先被放进_EntryList队列,此时线程处于blocking状态
  • 2.当一个线程获取到了实例对象的监视器(monitor)锁,那么就可以进入running状态,执行方法,此时,ObjectMonitor对象的_owner指向当前线程,_count加1表示当前对象锁被一个线程获取
  • 3.当running状态的线程调用wait()方法,那么当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,_count减1,同时线程进入_WaitSet队列,直到有线程调用notify()方法唤醒该线程,则该线程重新获取monitor对象进入_Owner区
  • 4.如果当前线程执行完毕,那么也释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,_count减1

3.monitor结构及配合重量锁的流程图


4.Synchronized修饰的代码块/方法如何获取monitor对象
(1)Synchronized修饰代码块:

Synchronized代码块同步在需要同步的代码块开始的位置插入monitorentry指令,在同步结束的位置或者异常出现的位置插入monitorexit指令;JVM要保证monitorentry和monitorexit都是成对出现的,任何对象都有一个monitor与之对应,当这个对象的monitor被持有以后,它将处于锁定状态。

例如,同步代码块如下:

public class SyncCodeBlock {public int i;public void syncTask(){synchronized (this){i++;}}
}

对同步代码块编译后的class字节码文件反编译,结果如下(仅保留方法部分的反编译内容)

  public void syncTask();descriptor: ()Vflags: ACC_PUBLICCode:stack=3, locals=3, args_size=10: aload_01: dup2: astore_13: monitorenter  //注意此处,进入同步方法4: aload_05: dup6: getfield      #2             // Field i:I9: iconst_110: iadd11: putfield      #2            // Field i:I14: aload_115: monitorexit   //注意此处,退出同步方法16: goto          2419: astore_220: aload_121: monitorexit //注意此处,退出同步方法22: aload_223: athrow24: returnException table://省略其他字节码.......

(2)Synchronized修饰方法:

Synchronized方法同步不再是通过插入monitorentry和monitorexit指令实现,而是由方法调用指令来读取运行时常量池中的ACC_SYNCHRONIZED标志隐式实现的,如果方法表结构(method_info Structure)中的ACC_SYNCHRONIZED标志被设置,那么线程在执行方法前会先去获取对象的monitor对象,如果获取成功则执行方法代码,执行完毕后释放monitor对象,如果monitor对象已经被其它线程获取,那么当前线程被阻塞。

同步方法代码如下:

public class SyncMethod {public int i;public synchronized void syncTask(){i++;}
}

对同步方法编译后的class字节码反编译,结果如下(仅保留方法部分的反编译内容):

public synchronized void syncTask();descriptor: ()V//方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法flags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=3, locals=1, args_size=10: aload_01: dup2: getfield      #2                  // Field i:I5: iconst_16: iadd7: putfield      #2                  // Field i:I10: returnLineNumberTable:line 12: 0line 13: 10
}

可以看出方法开始和结束的地方都没有出现monitorentry和monitorexit指令,但是出现的ACC_SYNCHRONIZED标志位。

三、锁的优化(升级)

锁的状态:无锁状态、偏向锁状态(锁只被一个线程持有)、轻量级锁状态(不同线程交替持有锁)、重量级锁状态(多线程竞争锁)(级别从低到高)
1.无锁状态:不会锁住资源,多个线程只有一个能成功,其他重试
2.使用偏向锁:同一个线程执行同步资源时自动获取资源

最关键的判断A线程是否存在的c++源码分析。

  • 偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程
偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟-XX:BiasedLockingStartupDelay = 0如果你确定自己应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁-XX:-UseBiasedLocking=false,那么默认会进入轻量级锁状态。

  • 释放和撤销的区别

释放:对应的就是synchronized方法的退出或synchronized块的结束。
撤销:笼统的说就是多个线程竞争导致不能再使用偏向模式的时候。
这里强调:偏向锁的撤销(Revoke) 操作并不是将对象恢复到无锁可偏向的状态, 而是在偏向锁的获取过程中, 发现了竞争时, 直接将一个被偏向的对象“升级到” 被加了轻量级锁的状态

3.偏向锁->轻量级锁:多个线程竞争同步资源时,没有获取到资源的线程自旋等待锁释放

按照轻量级锁的定义,如果对象的锁级别升级为轻量级锁后,JVM将在线程A、线程B和之后请求获得对象Y操作的若干线程的当前栈帧中,添加一个锁记录空间(Lock record),并将对象头中的Mark Word复制到锁记录中。然后线程会持续尝试使用CAS原理将对象头中的Mark Word部分替换为指向本线程锁记录空间的指针。如果替换成功则当前线程获得这个对象的操作权;如果多次CAS持续失败,说明当前对象的多线程抢占现象很严重,这是对象锁升级为重量锁状态,并使用操作系统层面的Mutex Lock(互斥锁)技术进行实现。
示意图:

4.轻量级锁->重量级锁
重量级锁依赖于操作系统的互斥量(mutex) 实现, 该操作会导致进程从用户态与内核态之间的切换, 是一个开销较大的操作

  • 获取重量锁流程:
  • 释放重量锁流程:

5.三锁总结:

偏向锁中:MarkWord->threadID
轻量级锁:MarkWord->线程栈中的LockRecord地址
重量级锁:MarkWord->一个堆中的monitor对象的指针

四、锁的粗化

锁粗化
如果在一段代码中连续的对同一个对象反复加锁解锁,其实是相对耗费资源的,这种情况下可以适当放宽加锁的范围,减少性能消耗。
当 JIT 发现一系列连续的操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的时候,会将加锁同步的范围扩散到整个操作序列的外部。

//粗化前
for (int i = 0; i < 10000; i++) {synchronized(this) {do();}
}
//粗化后
synchronized(this) {for (int i = 0; i < 10000; i++) {do();}
}

五、锁的消除

为了保证数据的完整性,在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。

锁消除的依据是逃逸分析的数据支持

如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于程序员来说这还不清楚么?在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?虽然没有显示使用锁,但是在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法:

//在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,
//所以JVM可以大胆地将vector内部的加锁操作消除。
public void vectorTest(){Vector<String> vector = new Vector<String>();for(int i = 0 ; i < 10 ; i++){vector.add(i + "");}System.out.println(vector);
}

参考文献:
https://blog.csdn.net/lengxiao1993/article/details/81568130
https://juejin.im/post/5d96db806fb9a04e0f30f0eb
https://www.cnblogs.com/paddix/p/5405678.html
https://blog.csdn.net/tongdanping/article/details/79647337#2%E3%80%81%E9%94%81%E7%B2%97%E5%8C%96
https://www.cnblogs.com/webor2006/p/11442551.html
https://juejin.im/post/5b4eec7df265da0fa00a118f#heading-10

Synchronized关键字和锁升级相关推荐

  1. Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/tongdanping/article/ ...

  2. 多线程高并发 底层锁机制与优化的最佳实践——各种锁的分类 || synchronized 关键字 倒底锁的是什么东西?|| CAS与ABA问题||锁优化||轻量级锁一定比重量级锁的性能高吗

    多线程高并发 底层锁机制与优化的最佳实践 各种锁的分类 加上synchronized 关键字,共享资源就不会出错 synchronized 关键字 倒底锁的是什么东西? synchronized 锁的 ...

  3. 锁 synchronized 关键字

    本篇接着上篇 线程安全,可以先看一下上篇~~ 锁 锁 是什么? 锁的特点: 锁的基本操作: 锁的使用 synchronized 关键字 - 监视器锁 理解 synchronized 的具体使用: sy ...

  4. 【Java】多线程SynchronizedVolatile、锁升级过程 - 预习+第一天笔记

    预习 1.什么是线程 基本概念 我们先从线程的基本概念开始,给大家复习一下,不知道有多少同学是基础不太好,说什么是线程都不知道的,如果这样的话,花时间去补初级内容的课. 什么是叫一个进程? 什么叫一个 ...

  5. synchronized关键字实现同步

    synchronized关键字的使用 Java语言提供了synchronized关键字,可以给方法或代码块进行加锁,从而实现同步. synchronized关键字取的锁都是对象锁,而不是把代码块或方法 ...

  6. Java多线程基础-6:线程安全问题及解决措施,synchronized关键字与volatile关键字

    线程安全问题是多线程编程中最典型的一类问题之一.如果多线程环境下代码运行的结果是符合我们预期的,即该结果正是在单线程环境中应该出现的结果,则说这个程序是线程安全的. 通俗来说,线程不安全指的就是某一代 ...

  7. java synchronized关键字锁和锁类型、锁升级过程讲解

    概述 synchronized是java的一个关键字,用于对方法或者代码块添加一个同步锁,以实现操作的原子性,保证线程安全性,但是却会带来一些性能上的损耗. 这个关键字添加的是可重入锁,也就是同一个线 ...

  8. 从分布式锁角度理解Java的synchronized关键字

    分布式锁 分布式锁就以zookeeper为例,zookeeper是一个分布式系统的协调器,我们将其理解为一个文件系统,可以在zookeeper服务器中创建或删除文件夹或文件.设D为一个数据系统,不具备 ...

  9. 在c#中用mutex类实现线程的互斥_面试官经常问的synchronized实现原理和锁升级过程,你真的了解吗...

    本篇文章主要从字节码和JVM底层来分析synchronized实现原理和锁升级过程,其中涉及到了简单认识字节码.对象内部结构以及ObjectMonitor等知识点. 阅读本文之前,如果大家对synch ...

最新文章

  1. Kafka 顺序消费方案
  2. Linux下往移动硬盘拷贝数据步骤方式
  3. 基于Kaggle数据的词袋模型文本分类教程
  4. Python函数参数中的冒号与箭头
  5. 第二章:09流程控制[3for]
  6. mysql第三方工具binlog_mysql 开发进阶篇系列 33 工具篇(mysqlbinlog日志管理工具)
  7. node mysql查询回调_nodejs 数据库查询回调问题
  8. mysql time 5分钟_MySQL 使用 PV 和 PVC 每天5分钟玩转 Docker 容器技术(154)
  9. linux安装配置nginx
  10. 关于员工技术及培训所想
  11. 如何使用 ggplot2 ?
  12. SIR模型的应用(2) - Influence maximization in social networks based on TOPSIS(3)
  13. 张尧学等人获奖,理由不充分
  14. 卫星导航信号结构变化的过去,现在和未来
  15. 【loj6184】无心行挽(虚树+倍增)
  16. 华为网络专家的求学之路的第四步
  17. 色彩搭配及设计金字塔的总结
  18. python请输入一个三位数输出该三位数的逆序数_编写程序,从键盘输入一个三位数,求出其逆序数并输出,例如输入123,输出321。...
  19. OpenGL学习笔记 - 计算机图形学和现代图形API
  20. 使用拦截器或者AOP实现权限管理(OA系统中实现权限控制)

热门文章

  1. js判断是微信、QQ内置浏览器打开页面
  2. How to Pronounce BEAUTIFUL
  3. 【bzoj2038】[国家集训队2010]小Z的袜子 莫队
  4. [深入浅出WP8.1(Runtime)]Socket编程之UDP协议
  5. UITableViewCell 设置
  6. Google 联合 Plaxo 对 OpenID 进行改进
  7. Python进阶_wxpy学习:用微信监控你的程序
  8. android 服务的应用,在Activity中实现背景音乐播放
  9. 解救小易——网易笔试
  10. 【Python】for 循环倒叙遍历