更多Java并发文章:https://www.cnblogs.com/hello-shf/category/1619780.html

一、简介

相信每一个java程序员对synchronized都不会太陌生,尤其是在大家关心的面试环节,不了解synchronize?不好意思,拜拜了您嘞。synchronized作为java一个重要的同步机制,在远古时代是被人嗤之以鼻的存在,因为在早期,synchronized属于重量级锁,即底层采用的是操作系统提供的Mutex lock实现的,为什么说他是重量级的锁呢,主要是线程间的切换需要操作系统从用户态切换到核心态,开销极其大。所以synchronized被人嗤之以鼻也就理所当然了,当然在java1.5之后呢,synchronized引入了偏向锁,轻量级锁,以减少对重量级锁的依赖(减少对重量级锁的使用是synchronized优化的终极目标),在此之后synchronized重新焕发心机,迎来了第一个春天。

二、预备知识

1,CAS

在学习synchronized之前,我们需要明白CAS(Compare and Swap)是什么鬼,CAS呢在不同的角度有很多我们常听到的名词:乐观锁,自旋锁。其实这个CAS在当前的各种中间件或者语言或者数据库中具有相当重要的地位。synchronized锁获取和撤销中正式使用的CAS自旋操作。

2,重入锁

什么叫重入锁呢?很简单,从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己的对象锁锁定的临界资源时,是可重入的即不需要再去获取锁。

三、对象头 - Mark Word

Mark World

Java中每一个对象都可作为锁。原因是每个对象的对象头都存在一个32bit的空间记录着对象的基础信息。默认记录对象的hashCode,分带年龄(GC的知识),所类型,锁标志位(谁在拿着这把锁)。。。
记录这些信息的区域叫做:Mark Word

线程ID即当前持有锁的线程信息。
锁标志位:01(默认),00(轻量级锁),10(重量级锁)

Monitor

monitor:监视器
你想想JVM怎么知道哪个对象的Mark Word状态?答案就是这个monitor,monitor是synchronized实现的另一个基础,任何一个Java对象都有一个monitor与之关联,当一个monitor被一个线程持有后,他将处于被锁定状态。值得注意的一点monitor只作用于重量级锁中。

四、synchronize锁升级过程

synchronized锁有四个状态:无锁,偏向锁,轻量级锁,重量级锁
synchronized锁升级的方向:无锁 >> 偏向锁 >> 轻量级锁 >> 重量级锁
性能开销从左到右依次增加。
锁只会升级不会降级。

1,偏向锁

大多数情况下,锁不存在多线程竞争,总是由同一个线程多次获得。
偏向锁的使用旨在于减少对轻量级锁的依赖,偏向锁的加锁和解锁需要使用CAS自旋。
偏向锁加锁过程:如果一个线程进入同步代码块(synchronized)获得了锁,那么锁就进入了偏向模式,此时Mark Word的结构也就变为偏向结构,当该线程再次进入同步块(请求锁时)将不再需要话费CAS操作来加锁或者获取锁,即获取锁的过程只需要检查Mark Word的锁标记为是否为偏向锁以及当前线程ID是否等于Mark Word中的threadID即可,这样就省去了大量有关锁申请的操作。如果当前线程从Mark Word获取的锁标志位为01(偏向锁)并且ThreadId=当前线程ID,则加锁成果。
偏向锁撤销过程:
偏向锁是用来一种竞争才释放锁的机制,所以当其他线程尝试竞争(CAS自旋)偏向锁时,持有偏向锁的线程才有可能会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码再执行),他会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,该锁会重新偏向竞争者,即Mark Word中ThreadID重新指向竞争者。如果当前线程依然存活,即竞争者会获取失败,则偏向锁会膨胀为轻量级锁。
关闭偏向锁:偏向锁在 Java 6 和 Java 7 里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用 JVM 参数来关闭延迟 -XX:BiasedLockingStartupDelay = 0。如果你确定自己应用程序里所有的锁通常情况下处于竞争状态,可以通过 JVM 参数关闭偏向锁 -XX:-UseBiasedLocking=false,那么默认会进入轻量级锁状态。
当前这种偏向模式不适合锁竞争比较激烈的多线程场合。

2,轻量级锁

轻量级锁加锁过程:前面说到偏向锁由轻量级锁升级而来,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁挣用的时候,尝试通过CAS自旋修改Mark Word中的ThreadID,如果替换失败,如果在一定次数内(自适应自旋机制)还是失败,偏向锁就会升级为轻量级锁,当前如前面所说,偏向锁要经历偏向锁撤销 -- 到达安全点 -- 膨胀为轻量级锁。安全点是重点。
轻量级锁膨胀过程:当持有轻量级锁的线程正在执行同步代码块(持有锁),此时又有线程来竞争锁,首先该线程依然会通过CAS自旋替换Mark Word中的ThreadID为本线程的ID,在一定次数内修改失败(当前锁被其他线程持有),轻量级锁会膨胀为重量级锁。成功则继续执行知道当前线程执行完成,释放轻量级锁。
轻量级锁比较适合线程交替执行的场景。

3,重量级锁

轻量级锁因为竞争激烈,会膨胀为重量级锁,一旦锁膨胀为重量级锁,线程切换将不是通过CAS自旋竞争来切换线程,而是未持有锁的竞争者将进入阻塞态。线程的状态切换都是操作系统底层的mutex lock来实现,而这个操作将意味着实现线程之间的切换需要从用户态转为核心态,这个成本是非常高的。
详细的锁升级过程如下图所示:

模拟一下以上过程,假设有两个线程,线程A和线程B
1,当线程A首先进入同步代码块
1)检查锁状态:判断锁标志位是否为01,如果是即偏向锁状态
2)检查偏向状态:Mark Word中的ThreadID是否为当前线程
是:当前线程即线程A进入偏向锁,执行同步代码块。
否:进入偏向锁竞争
2,模拟偏向锁竞争
假设线程A当前持有偏向锁,此时,线程B进入同步代码块
1)线程B同样经过1中的1)-- 2)但是Mark Word中的ThreadID == 线程A的ThreadID,即线程B获取失败
3)CAS自旋:线程B进入CAS自旋,尝试去替换ThreadID(CAS自旋采用的是自适应自旋)
成功:获取到偏向锁,执行同步代码块。
失败:在一定次数内还是失败,偏向锁膨胀为轻量级锁
3,偏向锁升级为轻量级锁
接着以上过程
1)线程B自旋替换ThreadID失败,当前持有偏向锁的线程A开始执行偏向锁撤销(等待竞争才释放的机制)
2)线程A到达安全点 ,虚拟机暂停原持有偏向锁的线程即线程A
3)虚拟机检查Mark Word中ThreadID指向的线程(线程A)状态
不活动状态:已退出同步代码块,表示线程A已退出竞争,线程B获取到偏向锁
活动状态:未退出同步代码块,锁膨胀为轻量级锁。
4,轻量级锁竞争及膨胀过程
接着以上过程线程A膨胀为轻量级锁
1)拷贝Mark Word到线程A的线程栈中,修改锁标志位为00,修改ThreadID指向当前线程即线程A。线程A被唤醒,从安全点继续执行。
2)线程B开始进入同步代码块,线程B发现锁标志位为00,拷贝对象头中的Mark Word到自己的线程栈。
3)线程B自旋修改Mark Word中的ThreadID
成功:执行同步代码块
失败:轻量级锁膨胀为重量级锁,标志位被修改为 10,指针指向monitor。
5,重量级锁竞争
synchronized膨胀为重量级锁之后,线程调度将依赖于操作系统底层的monitor
竞争不到锁的线程将进入阻塞状态,线程切换将会导致操作系统内核由用户态到核心态的转变(关于这个知识可以参考操作系统进程和线程调度的知识)。

五、synchronized优化

关于synchronized的使用,度娘一下一大把,在此就不在赘述。

1,锁粒度优化 —— 应用层优化

synchronized作用域:
修饰静态方法:锁是当前对象的 Class 对象,即类锁。
修饰非静态方法:锁是当前实例对象,即对象锁。
修饰代码块:锁是 Synchonized 括号里配置的对象(不要用Test.class这样等同于类锁)。
从上至下,锁粒度是递减的,其实最推荐使用的还是修饰同步代码块,这样尽量减少线程持有锁的时间。如果你用的是类锁,一旦锁膨胀为重量级锁,而类本身生命周期可以简单地理解为=进程,锁又不会被及时的GC掉,1.6之后对synchronize所做的偏向锁,轻量级锁优化等于没做。
锁粗化:
原则上我们需要将锁的粒度尽量的减小,以减少锁持有的时间。任何事情过度的追求等于浪费,如果对一个对象反复的加锁解锁,也是很浪费时间的,所以当出现这种场景,尽量的需要合并同步代码块,减少频繁加锁和解锁的资源浪费。

2,自适应自旋锁 —— 实现层优化

常规的自旋我们一般会这么写
while(true){...}
无限制的自旋是对CPU资源的极度浪费,JVM为了节省资源的浪费即更加的智能化,采用了自旋自适应锁,即自旋的次数不再是无限制或者固定次数,将由前一次在同一个锁上的自旋时间及锁的拥有者的状态来确定。

3,锁消除 —— JVM编译层优化

锁消除即删除不必要的加锁操作。JIT编译期,根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必加锁。
比如如下代码:

 public void add(String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}

JVM会傻到用stringBuffer吗?不会的,在编译器就给你把stringBuffer方法上的synchronized给优化掉了。

  如有错误的地方还请留言指正。
  原创不易,转载请注明原文地址:https://www.cnblogs.com/hello-shf/p/12091591.html

  参考文献:

  https://www.infoq.cn/article/java-se-16-synchronized/
  https://www.cnblogs.com/paddix/p/5405678.html
  https://blog.csdn.net/baidu_38083619/article/details/82527461

Java程序员必精通之—synchronized的更多相关文章

  1. Java程序员必会Synchronized底层原理剖析

    synchronized作为Java程序员最常用同步工具,很多人却对它的用法和实现原理一知半解,以至于还有不少人认为synchronized是重量级锁,性能较差,尽量少用. 但不可否认的是synchr ...

  2. Java程序员必学知识点

    JVM无论什么级别的Java从业者,JVM都是进阶时必须迈过的坎.不管是工作还是面试中,JVM都是必考题.如果不懂JVM的话,薪酬会非常吃亏(近70%的面试者挂在JVM上了) 详细介绍了JVM有关于线 ...

  3. 【转】java架构师之路:JAVA程序员必看的15本书的电子版下载地址

    作为Java程序员来说,最痛苦的事情莫过于可以选择的范围太广,可以读的书太多,往往容易无所适从.我想就我自己读过的技术书籍中挑选出来一些,按照学习的先后顺序,推荐给大家,特别是那些想不断提高自己技术水 ...

  4. Java架构师之路:JAVA程序员必看的15本书

    作为Java程序员来说,最痛苦的事情莫过于可以选择的范围太广,可以读的书太多,往往容易无所适从.我想就我自己读过的技术书籍中挑选出来一些,按照学习的先后顺序,推荐给大家,特别是那些想不断提高自己技术水 ...

  5. JAVA程序员必看的15本书-JAVA自学书籍推荐

    作为Java程序员来说,最痛苦的事情莫过于可以选择的范围太广,可以读的书太多,往往容易无所适从.我想就我自己读过的技术书籍中挑选出来一些,按照学习的先后顺序,推荐给大家,特别是那些想不断提高自己技术水 ...

  6. Java程序员必须知道的10个调试技巧

    调试可以帮助识别和解决应用程序缺陷,在本文中,将使用大家常用的的开发工具Eclipse来调试Java应用程序. 但这里介绍的调试方法基本都是通用的,也适用于NetBeans IDE,我们会把重点放在运 ...

  7. Java程序员必了解的JVM原理以及虚拟机的运行过程

    JVM概念 虚拟机:指以软件的方式模拟具有完整硬件,VM概念 虚拟机:指以软件的方式模拟具有完整硬件系统功能.运行在一个完全隔离环境中的完整计算机系统 ,是物理机的软件实现.常用的虚拟机有VMWare ...

  8. Java程序员必知的8大排序算法

    8种排序之间的关系 直接插入排序 (1)基本思想:在要排序的一组数中,假设前面(n-1)[n>=2] 个数已经是排 好顺序的,现在要把第n个数插到前面的有序数中,使得这n个数 也是排好顺序的.如 ...

  9. java程序员必知的8大排序

    先来看看8种排序之间的关系: 1,  直接插入排序 (1)基本思想:在要排序的一组数中,假设前面(n-1) [n>=2] 个数已经是排 好顺序的,现在要把第n个数插到前面的有序数中,使得这n个数 ...

随机推荐

  1. BZOJ 3331 (Tarjan缩点+树上差分)

    题面 传送门 分析 用Tarjan求出割点,对点-双连通分量(v-DCC)进行缩点,图会变成一棵树 注意v-DCC的缩点和e-DCC不同,因为一个割点可能属于多个v-DCC 设图中共有p个割点和t个v ...

  2. python学习三十八天常用内置函数分类汇总

    python给我们提供丰富的内置函数,不用去写函数体,直接调用就可以运行,很方便快速给我提供开发所需要的函数. 1,查内存地址 id() 变量的内存地址 id() 2,输入输出 input()  pr ...

  3. C# 引用类型的深度拷贝帮助类

    using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Lin ...

  4. XILINX FPGA 开发板 XC3S250E 核心板 学习板+12模块

    北京太速科技有限公司为广大合作单位特设海外代购业务,主要包括各类板卡.相机.传感器.仪器仪表.专用芯片等.代购业务仅收取基本的手续费. 北京太速科技有限公司在线客服:QQ:448468544 淘宝网站 ...

  5. Linux就该这么学11学习笔记

    参考链接:https://i.cnblogs.com/EditPosts.aspx?opt=1 文件传输协议 一般来讲,人们将计算机联网的首要目的就是获取资料,而文件传输是一种非常重要的获取资料的方式 ...

  6. Prometheus + Grafana

    Prometheus ubuntu安装prometheus非常简单: apt update apt install prometheus systemctl enable prometheus sys ...

  7. MYSQL5.7版本sql_mode=only_full_group_by问题,重启有效的方法

    1./etc/mysql/mysql.conf.d/mysqld.cnf 或者my.cnf   总之就是mysql的配置文件 2.查看当前的sql模式 select @@sql_mode; 3.添加语 ...

  8. xblock架构,链接与加载

    首先,xblock是一个模块,edx即edx-platform,在github.com/edx/edx-platform中,此平台有一个专门加载xblock的模块,定义了所有的接口,可以逐一将模块中的 ...

  9. JavaScript的三大组成部分

    JavaScript是一种属于网络的脚本语言,已经被广泛用于Web应用开发,常用来为网页添加各式各样的动态功能,为用户提供更流畅美观的浏览效果.通常JavaScript脚本是通过嵌入在HTML中来实现 ...

  10. ltp-ddt smp_basic

    SMP_S_FUNC_DUAL_CORE source functions.sh; cmd="stress-ng --matrix 4 -t 10s --perf --matrix-size ...