这篇笔记是我《没内鬼》系列第二篇,其实我计划是把设计模式和多线程并发分为两个系列,统一叫《一块儿学系列》来系统的介绍 相关的知识,可是想到这篇笔记去年就写成了,一直不发心也痒痒,因此整理一番就发出来,但愿你们指正~java
另外推荐我上一篇爆文
:没内鬼,来点干货!SQL优化和诊断web
一块儿学习,一块儿进步!面试
volatile关键字是在通常面试中常常问到的一个点,你们对它的回答莫过于两点:设计模式
那为了更有底气,那我们就来深刻看看吧缓存
我们在聊volatile关键字的时候,首先须要了解JMM内存模型,它自己是一种抽象的概念并不真实存在,草图以下:安全
JMM内存模型规定了线程的工做机理:即全部的共享变量都存储在主内存,若是线程须要使用,则拿到主内存的副本,而后操做一番,再放到主内存里面去
微信
这个能够引起一个思考,这是否是就是多线程并发状况下线程不安全的根源?假如全部线程都操做主内存的数据,是否是就不会有线程不安全的问题,随即引起下面的问题多线程
关于这个问题,我感受过于硬核,我只能简单的想象假如没有JMM,全部线程能够直接操做主内存的数据会怎么样
并发
因此我想面对这样的场景,前辈们才模仿CPU解决缓存一致性的思路肯定了JMM模型(能力不足,纯属猜想)app
❝在多处理器系统中,每一个处理器都有本身的高速缓存,而他们又共享同一主存
❞
咱们来看一段代码:
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();
复制代码
虽然在代码中只有一行,编译出的字节码指令能够用以下三行表示
因为步骤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就是对象锁,在普通方法,静态方法,同步块时锁的对象分别是:
类型 | 代码示例 | 锁住的对象 |
---|---|---|
普通方法 | 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("答案清楚了吗"); } } // 输出结果 // 真的有所谓的类锁? // 间隔几秒左右 // 答案清楚了吗 复制代码
因此实际上所谓的类锁,彻底就是当前类的Class对象,因此不要被误导,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 } 复制代码
同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit
命令释放锁,目的是为了不异常状况就没法释放锁
以前你们都说千万不要用synchronized,效率太差啦,可是Hotspot团队对synchronized进行许多优化,提供了三种状态的锁:偏向锁、轻量级锁、重量级锁,这样一来synchronized性能就有了极大的提升
偏向锁:就是锁偏向某一个线程。主要是为了处理同一个线程屡次获取同一个锁的状况,好比锁重入或者一个线程频繁操做同一个线程安全的容器,可是一旦出现线程之间竞争同一个锁,偏向锁就会撤销,升级为轻量级锁
轻量级锁:是基于CAS操做实现的。线程使用CAS尝试获取锁失败后,进行一段时间的忙等,也就是所谓的自旋操做。尝试一段时间仍没法获取锁才会升级为重量级锁
重量级锁:是基于底层操做系统实现的,每次获取锁失败都会直接让线程挂起,这会带来用户态
和内核态
的切换,性能开销比较大
打一个比方:你们在排队打饭,你有一个专属通道,叫作帅哥美女专属通道,只有你一我的能够自由的同行,这就叫偏向锁
忽然有一天,我来了,我也自夸帅哥,因此我盯上了你的通道,可是你还在打饭,而后我就抢过去和你一块儿打饭,可是这样效率比较低,因此阿姨没问个人时候,我就玩会手机等你,这就叫轻量级锁
忽然还有一天,我饿到不行,什么帅哥美女通通滚蛋,就我一我的先打饭,全部阿姨为我服务,给我服务完了再轮到大家,这就叫重量级锁
这也就是上文提到的System.out.println()为什么会影响内存可见性的缘由了
字节码获取方法:
用法: 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日一天哦