没内鬼,来点干货!volatile和synchronized
题外话
这篇笔记是我《没内鬼》系列第二篇,其实我计划是把设计模式和多线程并发分为两个系列,统一叫《一起学系列》来系统的介绍
相关的知识,但是想到这篇笔记去年就写成了,一直不发心也痒痒,所以整理一番就发出来,希望大家指正~
另外推荐我上一篇爆文
:没内鬼,来点干货!SQL优化和诊断
一起学习,一起进步!
volatile关键字
volatile关键字是在一般面试中经常问到的一个点,大家对它的回答莫过于两点:
- 保证内存可见性
- 防止指令重排
那为了更有底气,那咱们就来深入看看吧
JMM内存模型
咱们在聊volatile关键字的时候,首先需要了解JMM内存模型,它本身是一种抽象的概念并不真实存在,草图如下:
JMM内存模型规定了线程的工作机理:即所有的共享变量都存储在主内存,如果线程需要使用,则拿到主内存的副本,然后操作一番,再放到主内存里面去
这个可以引发一个思考,**这是不是就是多线程并发情况下线程不安全的根源?**假如所有线程都操作主内存的数据,是不是就不会有线程不安全的问题,随即引发下面的问题
为什么需要JMM内存模型
关于这个问题,我感觉过于硬核,我只能简单的想象假如没有JMM,所有线程可以直接操作主内存的数据会怎么样
- 上文说过,JMM模型并不是真实存在的,它只是一种规范,这种规范反而可以统一开发者的行为,如果没有规范,可能Java所提倡的一次编译,处处运行就凉凉了
- 另外我们都知道CPU 时间片轮转机制(就是在极短的时间切换进程,让用户无感知的享受多个进程运行的效果),线程在执行时候其实也是轮着来,假如A线程正在操作一个金钱数据,操作到一半,轮给B线程了,B线程把金额给改了,A线程最后又以错误的数据去入库等等,那问题不就大了去了?
所以我想面对这样的场景,前辈们才模仿CPU解决缓存一致性的思路确定了JMM模型(能力不足,纯属猜测)
在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主存
volatile如何保证内存可见性
我们来看一段代码:
public class VolatileTest {
static volatile String key;
public static void main(String[] args){
key = "Happy Birthday To Me!";
}
}
通过对代码进行javap命令,获取其字节码,内容如下(可以忽略啦):
public class com.mine.juc.lock.VolatileTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#21 // java/lang/Object."<init>":()V
#2 = String #22 // Happy Birthday To Me!
#3 = Fieldref #4.#23 // com/mine/juc/lock/VolatileTest.key:Ljava/lang/String;
#4 = Class #24 // com/mine/juc/lock/VolatileTest
#5 = Class #25 // java/lang/Object
#6 = Utf8 key
#7 = Utf8 Ljava/lang/String;
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/mine/juc/lock/VolatileTest;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 SourceFile
#20 = Utf8 VolatileTest.java
#21 = NameAndType #8:#9 // "<init>":()V
#22 = Utf8 Happy Birthday To Me!
#23 = NameAndType #6:#7 // key:Ljava/lang/String;
#24 = Utf8 com/mine/juc/lock/VolatileTest
#25 = Utf8 java/lang/Object
{
static volatile java.lang.String key;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_VOLATILE
public com.mine.juc.lock.VolatileTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/mine/juc/lock/VolatileTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: ldc #2 // String Happy Birthday To Me!
2: putstatic #3 // Field key:Ljava/lang/String;
5: return
LineNumberTable:
line 16: 0
line 17: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 args [Ljava/lang/String;
}
SourceFile: "VolatileTest.java"
请大家注意这一段代码:
static volatile java.lang.String key;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_VOLATILE
可以看到,volatile关键字在编译的时候会主动为变量增加标识:ACC_VOLATILE
,再研究下去就过于硬核了(汇编指令),我可能硬不起来(手动狗头),以后我会再对它进行深入的研究,我们只用了解到,Java关键字volatile,是在编译阶段主动为变量增加了ACC_VOLATILE标识,以此保证了它的内存可见性
即然volatile可以保证内存可见性,那至少有一个场景我们是可以放心使用的,即:一写多读场景
另外,大家在验证volatile内存可见性的时候,不要使用 System.out.println() ,原因如下:
public void println() {
newLine();
}
/**
* 是不是赫然看到一个synchronized,具体原因见下文
*/
private void newLine() {
try {
synchronized (this) {
ensureOpen();
textOut.newLine();
textOut.flushBuffer();
charOut.flushBuffer();
if (autoFlush)
out.flush();
}
}
catch (InterruptedIOException x) {
Thread.currentThread().interrupt();
}
catch (IOException x) {
trouble = true;
}
}
为什么会有指令重排
为了优化程序性能,编译器和处理器会对Java编译后的字节码和机器指令进行重排序,在单线程情况下不会影响结果,然而在多线程情况下,可能会出现莫名其妙的问题,案例见下文
指令重排例子
运行这段代码我们可能会得到一个匪夷所思的结果:我们获得的单例对象是未初始化的。为什么会出现这种情况?因为指令重排
首先要明确一点,同步代码块中的代码也是能够被指令重排的。然后来看问题的关键
INSTANCE = new Singleton();
虽然在代码中只有一行,编译出的字节码指令可以用如下三行表示
- 1.为对象分配内存空间
- 2.初始化对象
- 3.将INSTANCE变量指向刚分配的内存地址
由于步骤2,3交换不会改变单线程环境下的执行结果,故而这种重排序是被允许的。也就是我们在初始化对象之前就把INSTANCE变量指向了该对象。而如果这时另一个线程刚好执行到代码所示的2处
if (INSTANCE == null)
那么这时候有意思的事情就发生了:虽然INSTANCE指向了一个未被初始化的对象,但是它确实不为null了,所以这个判断会返回false,之后它将return一个未被初始化的单例对象!
如下:
由于重排序是编译器和CPU自动进行的,如何禁止指令重排?
INSTANCE变量加个volatile关键字就行,这样编译器就会根据一定的规则禁止对volatile变量的读写操作重排序了。而编译出的字节码,也会在合适的地方插入内存屏障,比如volatile写操作之前和之后会分别插入一个StoreStore屏障和StoreLoad屏障,禁止CPU对指令的重排序越过这些屏障
即然保证了内存可见,为什么还是线程不安全?
volatile 关键字虽然保证了内存可见,但是问题来了,见代码:
index += 1;
这短短一行代码在字节码级别其实分为了多个步骤进行,如获取变量,赋值,计算等等,如CPU基本执行原理一般,真正执行的是一个个命令,分为很多步骤
volatile 关键字可以保证的是单个读取操作是具有原子性的(每次读取都是从主内存获取最新的值)
但是如 index += 1; 实质是三个步骤,三次行为,因此它无法保证整块代码的原子性
synchronize关键字
驳斥关于类锁的概念
首先驳斥一个关于类锁的概念,synchronize就是对象锁,在普通方法,静态方法,同步块时锁的对象分别是:
类型 | 代码示例 | 锁住的对象 |
---|---|---|
普通方法 | synchronized void test() { } | 当前对象 |
静态方法 | synchronized static void test() { } | 锁的是当前类的Class 对象 |
同步块 | void fun () { synchronized (this) {} } | 锁的是()中的对象 |
大家都同意在同步代码块中,锁住的是括号里的对象,那么见以下代码:
public class SynDemo {
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (SynDemo.class) {
System.out.println("真的有所谓的类锁?");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
Thread.sleep(500);
answer();
}
synchronized static void answer () {
System.out.println("答案清楚了吗");
}
}
// 输出结果
// 真的有所谓的类锁?
// 间隔2秒多左右
// 答案清楚了吗
所以实际上所谓的类锁,完全就是当前类的Class对象,所以不要被误导,synchronize就是对象锁
synchronize实现原理
JVM
是通过进入、退出对象监视器(Monitor
来实现对方法、同步块的同步的
具体实现是在编译之后在同步方法调用前加入一个 monitor.enter
指令,在退出方法和异常处插入 monitor.exit
的指令。
其本质就是对一个对象监视器 Monitor
进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的
而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit
之后才能尝试继续获取锁。
流程图如下:
代码例子:
public static void main(String[] args) {
synchronized (Synchronize.class){
System.out.println("Synchronize");
}
}
字节码:
public class com.crossoverjie.synchronize.Synchronize {
public com.crossoverjie.synchronize.Synchronize();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // class com/crossoverjie/synchronize/Synchronize
2: dup
3: astore_1
**4: monitorenter**
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String Synchronize
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
**14: monitorexit**
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
}
为什么会有两次monitorexit
同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit
命令释放锁,目的是为了避免异常情况就无法释放锁
synchronized锁的几种形式
之前大家都说千万不要用synchronized,效率太差啦,但是Hotspot团队对synchronized进行许多优化,提供了三种状态的锁:偏向锁、轻量级锁、重量级锁,这样一来synchronized性能就有了极大的提高
偏向锁:就是锁偏向某一个线程。主要是为了处理同一个线程多次获取同一个锁的情况,比如锁重入或者一个线程频繁操作同一个线程安全的容器,但是一旦出现线程之间竞争同一个锁,偏向锁就会撤销,升级为轻量级锁
轻量级锁:是基于CAS操作实现的。线程使用CAS尝试获取锁失败后,进行一段时间的忙等,也就是所谓的自旋操作。尝试一段时间仍无法获取锁才会升级为重量级锁
重量级锁:是基于底层操作系统实现的,每次获取锁失败都会直接让线程挂起,这会带来用户态
和内核态
的切换,性能开销比较大
打一个比方:大家在排队打饭,你有一个专属通道,叫做帅哥美女专属通道,只有你一个人可以自由的同行,这就叫偏向锁
突然有一天,我来了,我也自诩帅哥,所以我盯上了你的通道,但是你还在打饭,然后我就抢过去和你一起打饭,但是这样效率比较低,所以阿姨没问我的时候,我就玩会手机等你,这就叫轻量级锁
突然还有一天,我饿到不行,什么帅哥美女统统滚蛋,就我一个人先打饭,所有阿姨为我服务,给我服务完了再轮到你们,这就叫重量级锁
synchronized除了上锁还有什么作用
- 获得同步锁
- 清空工作内存
- 从主内存中拷贝对象副本到本地内存
- 执行代码
- 刷新主内存数据
- 释放同步锁
这也就是上文提到的System.out.println()为何会影响内存可见性的原因了
Tips
字节码获取方法:
用法: javap <options> <classes>
其中, 可能的选项包括:
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
最后
感谢以下博文及其作者:
最后的最后
文章中我留了一个小小的彩蛋,如果你能发现也证明你看的非常仔细啦
夏天到啦,加我微信,我来请你吃一根雪糕~ 仅限5.13日一天哦
没内鬼,来点干货!volatile和synchronized的更多相关文章
- Thread 学习记录 <1> -- volatile和synchronized
恐怕比较一下volatile和synchronized的不同是最容易解释清楚的.volatile是变量修饰符,而synchronized则作用于一段代码或方法:看如下三句get代码: int i1; ...
- 从JAVA看C#中volatile和synchronized关键字的作用
最近一直在想C#中 volatile关键字到底是用来干什么的?查了很多.NET的文章都是说用volatile修饰的变量可以让多线程同时修改,这是什么鬼... 然后查到了下面这篇JAVA中关于volat ...
- 关于volatile和synchronized
这个可能是最好的对比volatile和synchronized作用的文章了.volatile是一个变量修饰符,而synchronized是一个方法或块的修饰符.所以我们使用这两种关键字来指定三种简单的 ...
- 剑指Offer——线程同步volatile与synchronized详解
(转)Java面试--线程同步volatile与synchronized详解 0. 前言 面试时很可能遇到这样一个问题:使用volatile修饰int型变量i,多个线程同时进行i++操作,这样可以实现 ...
- 线程同步Volatile与Synchronized(一)
volatile 一.volatile修饰的变量具有内存可见性 volatile是变量修饰符,其修饰的变量具有内存可见性. 可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改 ...
- Java并发——线程同步Volatile与Synchronized详解
0. 前言 转载请注明出处:http://blog.csdn.net/seu_calvin/article/details/52370068 面试时很可能遇到这样一个问题:使用volatile修饰in ...
- volatile和synchronized到底啥区别?多图文讲解告诉你
你有一个思想,我有一个思想,我们交换后,一个人就有两个思想 If you can NOT explain it simply, you do NOT understand it well enough ...
- 从JMM透析volatile与synchronized原理,图文并茂
在面试.并发编程.一些开源框架中总是会遇到 volatile 与 synchronized .synchronized 如何保证并发安全?volatile 语义的内存可见性指的是什么?这其中又跟 JM ...
- volatile与synchronized的区别
1.锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility). 互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一 ...
随机推荐
- Spring系列.事务管理
Spring提供了一致的事务管理抽象.这个抽象是Spring最重要的抽象之一, 它有如下的优点: 为不同的事务API提供一致的编程模型,如JTA.JDBC.Hibernate和MyBatis数据库层 ...
- Redis的常用配置
1. 配置守护线程方式运行,修改damonize,使用yes启用守护线程,这样就可以后台运行了 damonize no 修改为 damonize yes 2. 手动指定redis的pid,可以通过pi ...
- jmeter组件中 测试计划,线程组,sampler等等
[测试计划] 这边用户定义的变量,定义整个测试中使用的重复值(全局变量),一般定义服务器的ip,端口号 [线程组] 关于,线程组,我简单聊聊,有不对的地方欢迎大家拨乱反正 线程数:你需要运行的线程 比 ...
- Linux下安装java环境
准备工作: linux环境 xshell6 1.在Windows本地www,oracle.com下载对应的linux系统的JDK安装包,我下载的是 2.下载下来后,通过xftp远程传输到linux服务 ...
- PHP开发环境搭建工具有哪些?
对于php开发小白来说搭建一个php运行环境就是一道坎! 因为要做php开发,搭建一个能够运行php网站的服务器环境是第一步,传统的php环境软件非常复杂,好在很多公司开发了一键搭建php安装环境,一 ...
- python之单元测试及unittest框架的使用
例题取用登录模块:代码如下 def login_check(username,password): ''' 登录校验的函数 :param username:账号 :param password: 密码 ...
- caffe的python接口学习(4)mnist实例手写数字识别
以下主要是摘抄denny博文的内容,更多内容大家去看原作者吧 一 数据准备 准备训练集和测试集图片的列表清单; 二 导入caffe库,设定文件路径 # -*- coding: utf-8 -*- im ...
- spring bean post processor
相关文章 Spring 整体架构 编译Spring5.2.0源码 Spring-资源加载 Spring 容器的初始化 Spring-AliasRegistry Spring 获取单例流程(一) Spr ...
- antd图标库按需加载的插件实现
前景概要 antd是阿里出品的一款基于antd的UI组件库,使用简单,功能丰富,被广泛应用在中台项目开发中,虽然也出现了彩蛋事故,但不能否认antd本身的优秀,而我们公司在实际工作中也大量使用antd ...
- C# 基于内容电影推荐项目(一)
从今天起,我将制作一个电影推荐项目,在此写下博客,记录每天的成果. 其实,从我发布 C# 爬取猫眼电影数据 这篇博客后, 我就已经开始制作电影推荐项目了,今天写下这篇博客,也是因为项目进度已经完成50 ...