java架构之路(多线程)synchronized详解以及锁的膨胀升级过程
上几次博客,我们把volatile基本都说完了,剩下的还有我们的synchronized,还有我们的AQS,这次博客我来说一下synchronized的使用和原理。
synchronized是jvm内部的一把隐式锁,一切的加锁和解锁过程是由jvm虚拟机来控制的,不需要我们认为的干预,我们大致从了解锁,到synchronized的使用,到锁的膨胀升级过程三个角度来说一下synchronized。
锁的分类
java中我们听到很多的锁,什么显示锁,隐式锁,公平锁,重入锁等等,下面我来总结一张图来供大家学习使用。
这次博客我们主要来说我们的隐示锁,就是我们的无锁到重量级锁。
synchronized的使用
我们先来看一段简单的代码
public class SynchronizedTest { private static Object object = new Object(); public static void main(String[] args) {
synchronized (object){
System.out.println("只有我拿到锁啦");
}
}
}
就这样synchronized就可以使用了,这样是每次去拿全局对象的object去锁住后续的代码段。我们来看一下汇编指令码
public static void main(java.lang.String[]);
Code:
: getstatic # // Field object:Ljava/lang/Object;
: dup
: astore_1
: monitorenter
: getstatic # // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc # // String 只有我拿到锁啦
: invokevirtual # // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: aload_1
: monitorexit
: goto
: astore_2
: aload_1
: monitorexit
: aload_2
: athrow
: return
Exception table:
from to target type
any
any
明显看到了两个很重要的方法monitorenter和monitorexit两个方法,也就是说我们的synchronized方法加锁是基于monitorenter加锁和monitorexit解锁来操作的
我们得知是由monitorenter来控制加锁和monitorexit解锁的,我们完全可以这样来操作。上次我们说过一个unsafe类。
public class SynchronizedTest { private static Object obj = new Object(); public void lockMethod(){
UnsafeInstance.reflectGetUnsafe().monitorEnter(obj);
} public void unLockMethod(){
UnsafeInstance.reflectGetUnsafe().monitorExit(obj);
}
}
就是我们上次说的unsafe那个类给我们提供了加锁和解锁的方法,这样就是实现夸方法的加锁和解锁了,但是超级不建议这样的使用,后面的AQS回去说别的方式。越过虚拟机直接操作底层的,我们一般是不建议这样来做的。
我们还可以将synchronized锁放置在方法上。例如
public class SynchronizedTest { private static Object object = new Object(); public static synchronized void lockMethod() {
System.out.println("只有我拿到锁啦");
}
}
这样加锁是加在了this当前类对象上的。如果不加static,锁是加在类对象上的,需要注意我们用的spring的bean作用域
并且我们的synchronized是一个可重入锁,在jvm源码中有一个数值来记录加锁和解锁的次数,所以我们是可以多次套用synchronized的
public void lockMethod(){
synchronized(obj){
synchronized(obj){
System.out.println("我没报错");
}
}
}
synchronized到底锁了什么
还是拿上个每次加锁的时候会在对象头内记录我们的加锁信息,我们这里来说一下对象头里面都放置了什么吧。
以32位JVM内部存储结构为例
锁状态 |
25 bit |
4bit |
1bit |
2bit |
||
锁标志位 |
||||||
是否是偏向锁 |
||||||
23bit |
2bit |
|||||
GC标记 |
空 |
11 |
||||
重量级锁 |
指向重量级锁Monitor的指针(依赖Mutex操作系统的互斥) |
10 |
||||
轻量级锁 |
指向线程栈中锁记录的指针 pointer to Lock Record |
00 |
||||
偏向锁 |
线程ID |
Epoch |
对象分代年龄 |
1 |
01 |
|
无锁 |
对象的hashCode |
对象分代年龄 |
0 |
01 |
由此看出对象一直是有一个位置来记录我们的锁信息的。说到这我们就可以来看一下我们锁的膨胀升级过程了。
锁的膨胀升级
我们说过了对象头的内容,接下来可以说说我们的锁内部是如何升级上锁的了。从无锁到重量级锁的一个升级过程,我们来边画图,边详细看一下。
无锁状态:
开始时应该这样的,线程A和线程B要去争抢锁对象,但还未开始争抢,锁对象的对象头是无锁的状态也就是25bit位存的hashCode,4bit位存的对象的分代年龄,1bit位记录是否为偏向锁,2bit位记录状态,优先看最后2bit位,是01,所以说我们的对象可能无锁或者偏向锁状态的,继续前移一个位置,有1bit专门记录是否为偏向锁的,1代表是偏向锁,0代表无锁,刚刚开始的时候一定是一个无锁的状态,这个不需要多做解释,系统不同内部bit位存的东西可能有略微差异,但关键信息是一致的。
偏向锁:
这时线程开始占有锁对象,比如线程A得到了锁对象。
就会变成这样的,线程A拿到锁对象,将我们的偏向锁标志位改为1,并且将原有的hashCode的位置变为23bit位存放线程A的线程ID(用CAS算法得到的线程A的ID),2bit位存epoch,偏向锁是永远不会被释放的。
接下来,线程B也开始运行,线程B也希望得到这把锁啊,于是线程B会检查23bit位存的是不是自己的线程ID,因为被线程A已经持有了,一定锁的23bit位一定不是线程B的线程ID了
然后线程B也会不甘示弱啊,会尝试修改一次23bit位的对象头存储,如果说这时恰好线程A释放了锁,可以修改成功,然后线程B就可以持有该偏向锁了。如果修改失败,开始升级锁。自己无法修改,线程B只能找“大哥”了,线程B会通知虚拟机撤销偏向锁,然后虚拟机会撤销偏向锁,并告知线程A到达安全点进行等待。线程A到达了安全点,会再次判断线程是否已经退出了同步块,如果退出了,将23bit位置空,这时锁不需要升级,线程B可以直接进行使用了,还是将23bit的null改为线程B的线程ID就可以了。
轻量级锁:如果线程B没有拿到锁,我们就会升级到轻量级锁,首先会在线程A和线程B都开辟一块LockRecord空间,然后把锁对象复制一份到自己的LockRecord空间下,并且开辟一块owner空间留作执行锁使用,并且锁对象的前30bit位合并,等待线程A和线程B来修改指向自己的线程,假如线程A修改成功,则锁对象头的前30bit位会存线程A的LockRecord的内存地址,并且线程A的owner也会存一份锁对象的内存地址,形成一个双向指向的形式。而线程B修改失败,则进入一个自旋状态,就是持续来修改锁对象。
重量级锁:如果说线程B多次自旋以后还是迟迟没有拿到锁,他会继续上告,告知虚拟机,我多次自旋还是没有拿到锁,这时我们的线程B会由用户态切换到内核态,申请一个互斥量,并且将锁对象的前30bit指向我们的互斥量地址,并且进入睡眠状态,然后我们的线程A继续运行知道完成时,当线程A想要释放锁资源时,发现原来锁的前30bit位并不是指向自己了,这时线程A释放锁,并且去唤醒那些处于睡眠状态的线程,锁升级到重量级锁。
逃逸分析
很简单的一个问题,实例对象存在哪里?到底是堆还是栈?问题我先不回答,我们先看一段代码。
public class Test { public static void main(String[] args) throws InterruptedException {
System.out.println("开始");
for (int i = 0; i < 500000; i++) {
createCar();
}
System.out.println("结束");
Thread.sleep(10000000);
} private static void createCar() {
Car car = new Car();
}
}
就是我们运行一个创建对象的方法,一次性创建50万个Car对象,然后我们让我们的线程进行深度的睡眠,两个打印是为了知道我们的对象已经开始创建了和已经创建完成了。我们来运行一下。
然后运行jmap -histo命令来查看我们的线程
我们可以看到,car对象并没有产生50万个,别说会被GC掉对象,在运行之前我已经加了GC日志的参数-XX:+PrintGCDetails,控制台没有打印任何GC日志的。那么为什么会这样呢?我们来看一下我们的代码,由createCar代码创建了car对象,但car对象并没有被其它的方法或者线程去调用,虚拟机会认为你这对象可能只是一个实例化,并没有进行使用,这时虚拟机会给予你一个优化,就是对于可能没有使用的对象进行一次逃逸,也就是我们说到的逃逸分析。我们加入 -XX:DoEscapeAnalysis参数再看一次。
这也就是关闭了我们的逃逸分析,虚拟机就会真的为我们创建了50万个对象。也就是说开启了逃逸分析有一部分对象只是创建了线程栈上,当线程栈结束,对象也被销毁,上面的问题也就有答案了,实例对象可能存在堆上,也可能存在栈上。
java架构之路(多线程)synchronized详解以及锁的膨胀升级过程的更多相关文章
- JAVA多线程synchronized详解
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 当两个并发线程访问同一个对象object中的这个synchronized(this)同 ...
- java架构之路(多线程)AQS之ReetrantLock显示锁的使用和底层源码解读
说完了我们的synchronized,这次我们来说说我们的显示锁ReetrantLock. 上期回顾: 上次博客我们主要说了锁的分类,synchronized的使用,和synchronized隐式锁的 ...
- Java synchronized 详解
Java synchronized 详解 Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 1.当两个并发线程访问同一个对象object ...
- Java线程创建形式 Thread构造详解 多线程中篇(五)
Thread作为线程的抽象,Thread的实例用于描述线程,对线程的操纵,就是对Thread实例对象的管理与控制. 创建一个线程这个问题,也就转换为如何构造一个正确的Thread对象. 构造方法列表 ...
- Java多线程——多线程方法详解
本系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多线程的深入剖 ...
- Java多线程之synchronized详解
目录 synchronized简介 同步的原理 对象头与锁的实现 锁的优化与升级 Monitor Record 锁的对比 synchronized简介 synchronized关键字,一般称之为&qu ...
- java架构之路(多线程)JMM和volatile关键字(二)
貌似两个多月没写博客,不知道年前这段时间都去忙了什么. 好久以前写过一次和volatile相关的博客,感觉没写的那么深入吧,这次我们继续说我们的volatile关键字. 复习: 先来简单的复习一遍以前 ...
- 牛客网 Java 工程师能力评估 20 题 - 详解
牛客网 Java 工程师能力评估 20 题 - 详解 不知在看博客的你是否知道 牛客网,不知道就太落后了,分享给你 : 牛客网 此 20 题,绝对不只是 20 题! 免责声明:本博客为学习笔记,如有侵 ...
- Java AtomicInteger类的使用方法详解_java - JAVA
文章来源:嗨学网 敏而好学论坛www.piaodoo.com 欢迎大家相互学习 首先看两段代码,一段是Integer的,一段是AtomicInteger的,为以下: public class Samp ...
随机推荐
- 程序中打开IE浏览器并访问指定地址
最简单的方法 Process.Start("iexplore.exe"); //直接打开IE浏览器(打开默认首页) Process.Start(" ...
- POJ 2488 深搜dfs、
题意:模拟国际象棋中马的走棋方式,其实和中国象棋的马走的方式其实是一样的,马可以从给定的方格棋盘中任意点开始,问是否能遍历全部格子,能的话输出字典序最小的走棋方式,否则输出impossible 思路: ...
- urlencode()与urldecode()
urlencode()函数原理就是首先把中文字符转换为十六进制,然后在每个字符前面加一个标识符%. urldecode()函数与urlencode()函数原理相反,用于解码已编码的 URL 字符串,其 ...
- Spring Boot Admin-应用健康监控后台管理
Spring Boot Admin 用于监控基于 Spring Boot 的应用,它是在 Spring Boot Actuator 的基础上提供简洁的可视化 WEB UI. 1. 什么是Spring ...
- 在js中arguments对象的理解
一.在函数调用的时候,浏览器每次都会传递进两个隐式参数 函数的上下文对象this 封装实参的对象arguments 二.arguments 对象 arguments 对象实际上是所在函数的一个内置类数 ...
- Spring Boot实战之单元测试
Spring Boot实战之单元测试 本文介绍使用Spring测试框架提供的MockMvc对象,对Restful API进行单元测试 Spring测试框架提供MockMvc对象,可以在不需要客户端-服 ...
- 转 最近5年183个Java面试问题列表及答案[最全]
Java 面试随着时间的改变而改变.在过去的日子里,当你知道 String 和 StringBuilder 的区别(String 类型和 StringBuffer 类型的主要性能区别其实在于 Stri ...
- Linux 内核usb_bulk_msg 接口
usb_bulk_msg 创建一个 USB 块 urb 并且发送它到特定的设备, 接着在返回到调用者之 前等待完成. 它定义为: int usb_bulk_msg(struct usb_device ...
- 【15.93%】【codeforces 672D】Robin Hood
time limit per test1 second memory limit per test256 megabytes inputstandard input outputstandard ou ...
- c#中索引器
https://zhidao.baidu.com/question/59675980.html 不是必要的..相当于数学中的一个函数