咱们都知道,随着祖国愈来愈繁荣昌盛,随着科技的进步,设备的更新换代,计算机体系结构、操做系统、编译程序都在不断地改革创新,但始终有一点是不变的(我对鸭血粉丝的热爱忠贞不渝):那就是下面三者的性能耗时:CPU < 内存 < I/Ohtml
但也正由于这些改变,也就在并发程序中出现了一些诡异的问题,而其中最昭著的三大问题就是:可见性、有序性、原子性。java
今天咱们就主要来学习一下三者中的可见性。编程
可见性 的定义是:一个线程对共享变量的修改,另一个线程可以马上看到。缓存
在单核时代,全部线程都在一个CPU上执行,因此一个线程的写,必定是对其它线程可见的。就比如,一个总经理下面就一个项目负责人。安全
此时,项目经理查看到任务G后,分配给员工A和员工B,那么这个任务的进度就能随时掌握在项目经理手中了;每一个员工都能从项目经理处得知最新的项目进度。bash
而在多核时代后,每一个CPU都有本身的缓存,这就出现了可见性问题。并发
此时,两个项目经理同时查看到任务G后,各自分配给本身下属员工,那么这个任务的进度就只能掌握在各自项目经理手中了,由于全部员工的工做进度并非汇报给同一个项目经理;那么,每一个员工只能得知本身项目组员工的工做进度,并不能得知其余项目组的工做进度。因此,当多个项目经理在作同一个任务时,就可能出现任务配比不均、任务进度拖延、任务重复进行等多种问题。app
总结上面的例子来说,就是由于进度的不及时更新,致使数据不是最新,致使决策失误。因此,咱们隐约能够看出,内存并不直接与Cpu打交道,而是经过高速缓存与Cpu打交道。ide
cpu <——> 高速缓存 <———> 内存
复制代码
经过一张图片来表示就是(多核):函数
下文咱们的阐述,若无特殊说明,都是基于多核的。
可见性问题都是由Cpu缓存不一致为并发编程带来,而其中的主要有下面三种状况:
线程交叉执行多数状况是因为线程切换致使的,例以下图中的线程A在执行过程当中切换到线程B执行完成后,再切换回线程A执行剩下的操做;此时线程B对变量的修改不能对线程A当即可见,这就致使了计算结果和理想结果不一致的状况。
例以下面这段代码
int a = 0; //行1
int b = 0; //行2
a = b + 10; //行3
b = a + 9; //行4
复制代码
若是行1和行2在编译的时候改变顺序,执行结果不会受到影响;
若是将行3和行4在变异的时候交换顺序,执行结果就会受到影响,由于b的值得不到预期的19;
由图知:因为编译时改变了执行顺序,致使结果不一致;而两个线程的交叉执行又致使线程改变后的结果也不是预期值,简直雪上加霜!
由于主线程对共享变量的修改没有及时更新,子线程中不能当即获得最新值,致使程序不能按照预期结果执行。
例以下面这段代码:
package com.itquan.service.share.resources.controller;
import java.time.LocalDateTime;
/**
* @author :mmzsblog
* @description:共享变量在线程间的可见性测试
*/
public class VisibilityDemo {
// 状态标识flag
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
System.out.println(LocalDateTime.now() + "主线程启动计数子线程");
new CountThread().start();
Thread.sleep(1000);
// 设置flag为false,使上面启动的子线程跳出while循环,结束运行
VisibilityDemo.flag = false;
System.out.println(LocalDateTime.now() + "主线程将状态标识flag被置为false了");
}
static class CountThread extends Thread {
@Override
public void run() {
System.out.println(LocalDateTime.now() + "计数子线程start计数");
int i = 0;
while (VisibilityDemo.flag) {
i++;
}
System.out.println(LocalDateTime.now() + "计数子线程end计数,运行结束:i的值是" + i);
}
}
}
复制代码
运行结果是:
从控制台的打印结果能够看出,由于主线程对flag的修改,对计数子线程没有当即可见,因此致使了计数子线程久久不能跳出while循环,结束子线程。
对于这种状况,固然不能忍,因此就引出了下一个问题:如何解决线程间不可见性
为了保证线程间可见性咱们通常有3种选择:
volatile
关键字能保证可见性,但也只能保证可见性,在此处就能保证flag的修改能当即被计数子线程获取到。
此时纠正上面例子出现的问题,只需在定义全局变量的时候加上volatile
关键字
// 状态标识flag
private static volatile boolean flag = true;
复制代码
将标识状态flag在定义的时候使用Atomic相关类来进行定义的话,就能很好的保证flag属性的可见性以及原子性。
此时纠正上面例子出现的问题,只需在定义全局变量的时候将变量定义成Atomic相关类
// 状态标识flag
private static AtomicBoolean flag = new AtomicBoolean(true);
复制代码
不过值得注意的一点是,此时原子类相关的方法设置新值和获得值的放的是有点变化,以下:
// 设置flag的值
VisibilityDemo.flag.set(false);
// 获取flag的值
VisibilityDemo.flag.get()
复制代码
此处咱们使用的是Java常见的synchronized关键字。
此时纠正上面例子出现的问题,只需在为计数操做i++
添加synchronized
关键字修饰
synchronized (this) {
i++;
}
复制代码
经过上面三种方式,咱们都能获得相似以下的指望结果:
然而,接下来咱们要对其中的volatile
和synchronized
关键字作一番较为详细的解释。欢迎关注公众号"Java学习之道",查看更多干货!
Java内存模型对volatile关键字定义了一些特殊的访问规则,当一个变量被volatile修饰后,它将具有两种特性,或者说volatile具备下列两层语义:
针对第一点,volatile保证了不一样线程对这个变量进行读取时的可见性,具体表现为:
附一张CPU缓存模型图:
综上所述:就是用volatile修饰的变量,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。
使用volatile没法保障线程安全,那么volatile的做用是什么呢?
其中之一:(对状态量进行标记,保证其它线程看到的状态量是最新值)
volatile关键字是Java虚拟机提供的最轻量级的同步机制,不少人因为对它理解不够(其实这里你想理解透的话能够看看happens-before原则),而每每更愿意使用synchronized来作同步。
synchronized关键字的做用域有二种:
1)是某个对象实例内,synchronized aMethod(){}
能够防止多个线程同时访问这个对象的synchronized方法。
若是一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法。
这时,不一样的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样能够同时访问相同类的另外一个对象实例中的synchronized方法。
由于当修饰非静态方法的时候,锁定的是当前实例对象。
2)是某个类的范围,synchronized static aStaticMethod{}
防止多个线程同时访问这个类中的synchronized static 方法。它能够对类的全部对象实例起做用。
由于当修饰静态方法的时候,锁定的是当前类的 Class 对象。
除了方法前用synchronized关键字,synchronized关键字还能够用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
用法是:
synchronized(this){
/*区块*/
}
复制代码
它的做用域是当前对象;
synchronized关键字是不能继承的,也就是说,基类的方法
synchronized f(){
// 具体操做
}
复制代码
在继承类中并不自动是
synchronized f(){
// 具体操做
}
复制代码
而是变成了
f(){
// 具体操做
}
复制代码
继承类须要你显式的指定它的某个方法为synchronized方法;
综上3点所述:synchronized关键字主要有如下这3种用法:
这三种用法就基本保证了共享变量在读取的时候,读取到的是最新的值。
线程解锁前,必须把共享变量的最新值刷新到主内存
线程加锁时,将清空工做内存中共享变量的值,从而是使用共享变量时,须要从主内存中从新读取最新的值(注意:加锁与解锁是同一把锁)
从上面的这两条规则也能够看出,这种方式保证了内存中的共享变量必定是最新值。
但咱们在使用synchronized保证可见性的时候也要注意如下几点:
以上内容就是我对并法中的可见性的一点理解与总结了,下期咱们接着叙述并发中的有序性。
欢迎关注公众号:Java学习之道
我的博客网站:www.mmzsblog.cn