Volatile可见性

比如现在我们有这样一段代码:线程等待另一个线程将数据装载完就输出success,可是最后程序一直卡在while循环里没有往下执行。

public class VolatileDemo {
private static boolean flag = false;
//private static volatile boolean flag = false; public static void main(String[] args) throws Exception{
new Thread(()->{
System.out.println("等待装载数据。。。。");
while(!flag){
}
System.out.println("====== SUCCESS =====");
}).start();
Thread.sleep(2000);
new Thread(()->{
System.out.println("开始装载");
flag = true;
System.out.println("装载完毕");
}).start();
}
}
/* 控制台输出
等待装载数据。。。。
开始装载
装载完毕
*/

造成这个问题出现的原因是jmm原子操作造成的。jmm内存模型就是java内存模型、准确的说是java线程内存模型。它和cpu缓存模型类似、是基于cpu缓存模型来建立的。
jmm一共有8种原子操作:
  read(读取):从主存读取数据
  load(载入):将内存数据读到工作内存
  use (使用):取出工作内存中的数据来计算
  assign(赋值):将计算好的值重新赋予到工作内存中
  store(存储):将工作内存数据写入主存
  write(写入):将store过去的变量值赋值给主内存中的变量
  lock(锁定):将主内存变量加锁,标识为线程独占状态
  unlock(解锁):将主存变量解锁,解锁后其他线程可以锁定该变量

  

可以看到线程1已经把变量副本加载到工作内存了,而线程2将计算后的值存到主存之后,却没有办法告诉线程1,所以就出现了线程安全问题。其实cpu与主存交互会经过"总线"这么一个概念,cpu为了解决这种数据不一致问题有两种方案:
总线加锁(性能太低)
  早期cpu是对总线加锁,lock住这个数据,这样其它线程就没法对它读或写,直到这个线程用完这个数据 unlock之后才能被其他线程操作。也就是说从read开始后直到write结束才释放锁。
MESI缓存一致性协议
  多个线程将同一个数据读取到各自的缓存区后,某个cpu修改了缓存的数据之后,会立马同步给主存,这都是汇编语言实现的。其他cpu通过总线嗅探机制(可以理解为监听)可以感知到数据的变化从而将自己缓存里的数据失效,从而去读取主存的值。所以mesi协议是从store开始加锁,锁的粒度更小,时间更短。实际上volatile就是这么实现可见性的。同时由于这中间过程中有store和write几步操作、还要让其他cpu缓存的数据置空都是要耗时的,可能这个过程中数据被别人改了,所以它是非原子操作的。

volatile与指令重排

指令重排
  指定重排只会发生在多线程情况下,单线程是不会出现指定重排的。所谓的指令重排就是JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行排序优化。但不会对有依赖关系的做重排序。比如:
  int a = 1;
  int b = 2;
  int c = a*c;
  a 和 b 没有任何关系,所以它们的顺序无所谓,但是 c 依赖于a、b。只能存在于a、b后面,不然就乱套了。
在一个变量被volatile修饰后会被禁止指令重排,JVM会为我们做两件事:
  1.在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。
  2.在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。

Synchronization原子性

  synchronized (a){} 锁住的就是()里面的对象,多个线程对同一个对象操作时,就会形成互斥效果,如果是操作两个不同的对象,那么就不会受synchronized影响

public class SynchronizedDemo {
public static void main(String[] args) {
SynchronizedDemo s = new SynchronizedDemo();
Integer a = 1;
Integer b = 2; new Thread(()->{
s.sync(a);
}).start();
new Thread(()->{
s.sync(b);
//s.sync(a);
}).start();
} public void sync(Integer a){
synchronized (a){
System.out.println("线程:"+Thread.currentThread().getName()+" 获取到变量"+a);
try {
Thread.sleep(8000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

在jdk中,Synchronization同步是基于Monitor对象实现的,它里面主要有两个指令:
  monitorenter: 插入到同步代码块的开始位置
  monitorexit: 插入到同步代码块结束的位置
它们对应着JMM模型8大原子操作的lock与unlock,lock获取锁后把对象加载到工作内存,数据操作完之后重新赋值到主内存,最后unlock解锁。JVM需要保证每一个monitorenter都有一个monitorexit与之对应。为了保证在方法异常时,monitorenter和monitorexit指令也能正常配对执行,编译器会自动产生一个异常处理器,它的目的就是用来执行 异常的monitorexit指令。Monitor(监视器锁)是依赖操作系统的Mutex Lock(互斥锁)实现的,需要向内核申请资源,此时cpu将由用户态转换为内核态,它是一个低性能重量级锁。

jdk1.6对Synchronization的优化

  jdk1.6之后就对这个synchronized锁进行了各种优化,如适应性自旋锁、轻量级锁和偏向锁,并默认开启偏向锁。从 无锁—>偏向锁—>轻量级锁—>重量级锁 ,锁升级的这个过程是不可逆的。被加锁的对象 jvm中为它定义了一种对应的数据结构,通过判断数据结构的对象头就知道目前是什么锁状态。例如通过倒数第三个bit的值 0/1 就知道目前是无锁还是偏向锁了。

三种锁的区别

  偏向锁:仅有一个线程进入临界区(主要用于不存在锁竞争,而是一个线程多次获得锁时,为的使线程获取锁使用最小的代价(因为只需要修改获取锁的线程id就好了))
  轻量级锁:多个线程交替进入临界区(当其他线程尝试竞争偏向锁时,会升级为轻量锁)
  重量级锁:多个线程同时进入临界区

锁的升级过程

1. 无锁:此时还没有线程获取所得资源

  

2. 获取偏向锁:第一个线程获取到锁就会将前面的23个bit位修改为自己线程的id,将无锁升级为偏向锁。

  

3. 升级轻量锁:此时另一个线程尝试获取锁,发现锁里的线程id并不是自己的,就会释放锁,将对象头重的Mark Word替换为指向锁记录的指针,将其升级为轻量锁。

  

4. 若刚才将对象头重的Mark Word替换为指向锁记录的指针失败,则会自旋(循环等待)来获取锁,此时若有另一个线程同时竞争,锁会升级为重量级锁。

   

ReentrantLock

  ReentrantLock和Synchronization一样是并发编程的核心,Synchronization是sun公司开发,而ReentrantLock是一个叫Doug Lea的人写出来的。它控制锁的状态是通过AQS(队列同步器)来实现的,主要用到了2点技术点。

1. volatile关键字
  在AQS中定义一个volatile修饰的int变量state,有线程获取到锁之后state就加一,其他线程发现锁被占用之后就会进入等待队列。线程释放锁之后state就会减一,然后唤醒队列中的其他线程。
2. CAS(比较替换算法)
  我们知道volatile不是线程安全的,那么如何保证只有一个线程对state在操作呢?其实就用到了CAS算法,它是一个无锁算法是乐观锁的体现。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。只有A==V的时候才把V的值修改成B,否则不做任何操作。源码调用了Unsafe类的原子方法,都是被native修饰的,整个比较并替换的操作是一个原子操作。

ReentrantLock和Synchronization比较

  ReentrantLock和synchronized在低并发的时候性能差距不大,高并发时ReentrantLock性能要稍微高一些。虽然sync做了优化但是在竞争激烈的时候还是会从偏向锁升级为重量级锁,是用户态切换到内核态的一个过程 比较消耗资源,lock有利用CAS自旋操作来实现锁则会稍微好一点。

Volatile可见性 与 Synchronization原子性的优化的更多相关文章

  1. JUC 并发编程--05, Volatile关键字特性: 可见性, 不保证原子性,禁止指令重排, 代码证明过程. CAS了解么 , ABA怎么解决, 手写自旋锁和死锁

    问: 了解volatile关键字么? 答: 他是java 的关键字, 保证可见性, 不保证原子性, 禁止指令重排 问: 你说的这三个特性, 能写代码证明么? 答: .... 问: 听说过 CAS么 他 ...

  2. volatile可见性的一些认识

    一.前言 volatile的关键词的使用在JVM内存模型中已是老生常谈了,这篇文章主要结合自己对可见性的一些认识和一些直观的例子来谈谈volatile.文章正文大致分为三部分,首先会介绍一下happe ...

  3. volatile可见性的一些认识和论证

    一.前言 volatile的关键词的使用在JVM内存模型中已是老生常谈了,这篇文章主要结合自己对可见性的一些认识和一些直观的例子来谈谈volatile.文章正文大致分为三部分,首先会介绍一下happe ...

  4. Volatile可见性分析(一)

    JUC(java.util.concurrent) 进程和线程 进程:后台运行的程序(我们打开的一个软件,就是进程) 线程:轻量级的进程,并且一个进程包含多个线程(同在一个软件内,同时运行窗口,就是线 ...

  5. Java并发编程-volatile可见性的介绍

    要学习好Java的多线程,就一定得对volatile关键字的作用机制了熟于胸.最近博主看了大量关于volatile的相关博客,对其有了一点初步的理解和认识,下面通过自己的话叙述整理一遍. 有什么用? ...

  6. volatile可见性案例-黑马

    volatile可见性案例-黑马 package com.mozq.demo.demo; class Task implements Runnable{ //public boolean flag = ...

  7. volatile 关键字 和 i++ 原子性

    package com.mozq.multithread; /** * 深入理解Java虚拟机 volatile 关键字 和 i++ 原子性. */ public class VolatileTest ...

  8. volatile可见性和指令重排

    volatile关键字的2个作用 1.线程的可见性 2.防止指令重排 什么是线程的可见性? 线程的可见性 就是一个线程对一个变量进行更改操作 其他线程获取会获得最新的值. 线程在执行的行 操作主线程的 ...

  9. volatile(防止编译器对代码进行优化)

    adj.易变的:无定性的:无常性的:可能急剧波动的 网络挥发性:挥发性的:不稳定的 volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了.

随机推荐

  1. Zjoi2011看电影(movie)

    第一步,打表找规律,发现自己的表连3的小样例都过不去,还不如自己手模,自己手跑了5以下的样例,然后发现毫无规律可言…… 第二步,想出一种错误做法,首先n>k必零,人比座都多……然后粘一下图: 基 ...

  2. 关于开发APP接口版本不兼容的问题

    关于 APP接口版本兼容的问题. iOS和android 要不断开发新版本,很多服务端开发都是在以前接口的逻辑上进行修改. 新的APP和接口开发后,接口如何兼容老的APP? 有的公司 每次发布完APP ...

  3. js中的事件委托技术

    1.什么是事件委托:通俗的讲,时间就是onclick,onmouseover,onmouseout,等就是事件,委托呢,就是让别人来做,这个时间本来是加在某些元素上的,然而你却加到别人身上来做,完成这 ...

  4. laravel查询构造器DB还是ORM,这两者有什么区别,各该用在什么场景中

    解答一: 我们所有操作都是走的orm,因为操作简单 直观明了 好维护,性能是低一些 但还没有多致命,真有并发需要优化了 用DB也不一定能解决问题.还是要了解orm每个方法的意思,不然你可能一不小心就会 ...

  5. koa2环境搭建

    npm install -g koa-generator koa2 ssy-koa2 cd ssy-koa2 npm install

  6. JxBrowser开启调试模式,JxBrowser debug

    原文: 一.问题描述 像一般的浏览器都带了调试功能,按F12就能打开,在JxBrowser中如何开启调试模式了. 二.解决方法 以下代码就能开启调试模式: import com.teamdev.jxb ...

  7. np.repeat()

    np.repeat()用于将numpy数组重复. numpy.repeat(a, repeats, axis=None); 参数: axis=0,沿着y轴复制,实际上增加了行数axis=1,沿着x轴复 ...

  8. flask 学习(二)

    安装了flask扩展 以及flask-bootstrap 默认情况下,flask在template文件夹寻找模板. flask 加载的是Jinja2模板,该模板引擎在flask中由函数render_t ...

  9. GCC 9.2 2019年8月12日 出炉啦

    GNU 2019-08-12 发布了 GCC 9.2https://gcc.gnu.org/onlinedocs/9.2.0/ 有详细的说明 MinGW 上可用的 GCC 9.2 版本下载地址 [ m ...

  10. JWT With NetCore WebApi

    1 什么是JWT? JWT是一种用于双方之间传递安全信息的简洁的.URL安全的表述性声明规范.JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象 ...