原子性,可见性与有序性

在多线程中,线程同步的时候一般需要考虑原子性,可见性与有序性

原子性

原子性定义:一个操作或者多个操作在执行过程中要么全部执行完成,要么全部都不执行,不存在执行一部分的情况。

以我们在Java代码中经常用到的自增操作i++为例,i++实际上并不是一步操作,而是首先对i的值加一,然后将结果再赋值给i。在单线程中不会存在问题,但如果在多线程中我们考虑这样一个情况:i是一个共享变量,初始值为0,假设线程一以执行到某一步正好进行自增操作i++,刚好对i进行了加一但是还没将值重新赋给i,此时当前线程被cpu挂起,而另一个线程二开始执行,刚好也对i进行了一个赋值操作i=10;,等线程一重新执行后会将i自增后的值1赋给i,此时相当于覆盖了线程二的赋值操作。此时将会产生线程不安全的情况。

可见性

多个线程同时访问一个共享的变量的时候,每个线程的工作内存有这个变量的一个拷贝,变量本身还是保存在共享内存(堆)中。所以并不是每一次一个线程修改了值后其他线程都可以立即取到修改后的值。可见性是指当其他的线程访问同一个变量时,当一个线程修改了这个变量的值,其他线程也能够立即看得到修改的值。

有序性

有序性是指程序的执行严格按照我们写的代码的顺序进行执行。

指令重排

一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许对指令进行优化,即调整实际指令运行的顺序。指令重排不会对单线程的程序造成任何不利的影响,但是多线程环境下将会产生一些影响。指令重排的前提条件是指令调整后不会影响单线程程序的执行:

int i = 2; //statement 1
int j = 1;//statement 2 int k = i*j;//statement 3

在上面的代码中对于语句1和语句2相互之间没有任何依赖,所以可能发生指令重排,但是语句3和语句1,2都有关系,所以语句3一定是在语句1和语句2之后执行的。所以单线程情况下是绝对不会出现问题的。但是对于多线程可能就发生只是初始化了语句1或者语句2就执行语句3了。

要保证在多线程下线程安全,这三大性质都是必须要保证的,而一旦其中一项无法保证那么不是线程安全的。前面的synchronized关键字就是实现了这三大特性的。

volatile

虽然已经有了synchronized关键字保证了线程安全需要的三大特性,但是在JDK1.8优化synchronized之前,synchronized关键字都是一个重量级的锁,对程序的效率有着比较大的影响。在java中还有一个synchronized关键字的轻量级的实现-volatile关键字。volatile关键字是在JDK1.5之后重新被重用的一个关键字,它可以保证上诉三大特性中的有序性和可见性,但是不能保证原子性,所以它实际上是线程不安全的。

保证可见性

出现可见性的原因在于私有栈帧中的值和公共堆中的共享值不同得问题。

当一个线程在修改普通变量时,其他线程不能立刻看到修改后的值,如果此时有其他线程读取该变量的值,实际上读到的是没有修改的值。

volatile关键字作用在于当要使用时强制从主内存中读取值,保证每次读取的都是公共内存中的值。

防止指令重排

内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。 这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。

volatile关键字功能的实现既是通过内存屏障完成的,当使用volatile关键字修饰的变量进行读写是便会加上内存屏障来保证设计变量的操作顺序执行,需要注意的是其和synchronized关键字的同步是不一样的。

为什么不是原子性的?

实际上volatile关键字保证的事所有线程从主存中取到的值是最新的,但是多个线程修改了改变量的值并不会通知其他线程,除非其他线程再次从主存中取值。

在以上阶段中,比如存在两个线程且两个线程都已经加载了变量count的值,这时线程一将count修改为10,线程二将值修改为20,但是两个线程之间并不知道对方都改了值,而最终写到主存的值也是后写入的那一个,即始终都一个线程修改的值被覆盖,所以其并不是原子性的。在涉及到多线程操作共享变量是还是应该加锁进行操作。

synchronized和volatile关键字的比较

  1. volatile关键字并不是同步操作,其在多线程访问下不会进行阻塞,而synchronized关键字会发阻塞。
  2. volatile关键字能保证有序性和可见性,但不能保证原子性。synchronized三种特性都能保证,所以synchronized是线程同步的,而volatile不是。
  3. volatile是线程同步的轻量级实现,所以效率较之synchronized要高。

等待通知机制

在多线程程序中可能会存在多个程序相互配合完成一项功能,这是就需要线程之间进行通信,在一个线程的工作完成后通知后续线程工作。通常情况下我们可以在一个线程中进行一个while循环操作,设置一个标志flag,当该线程的前置线程完成后修改flag,后面的while得到这个标志后知道自身需要开始工作了,跳出循环。但是这种方法的劣势在于while循环使得该线程一个需要处于运行中,同时当多个线程相互之间都需要进行通信时会使得程序变得极其复杂。为了解决这个问题,有人提出了一种等待通知机制。

等待通知机制是利用JDK中提供的API中的wait()notify/notifyAll()方法来进行实现(实际上Lock类中的方法也能实现),wait()方法是使得当前线程进入等待队列中,notify/notifyAll()是将等待的线程唤醒。

等待方

  1. 获取对象锁
  2. 如果条件不满足,调用对象的wait方法,被通知后依然要检查条件是否满足
  3. 条件满足以后,才能执行相关的业务逻辑
Synchronized(对象){
While(条件不满足){
对象.wait()
}
// do your working
}

通知方

  1. 获得对象的锁;
  2. 改变条件;
  3. 通知所有等待在对象的线程
Synchronized(对象){
业务逻辑处理,改变条件
对象.notify/notifyAll
}

实例

public class User {
private int age = 30; public int getAge() {
return age;
} public void setAge(int age) {
this.age = age;
} /**
* 1、获取对象锁
* 2、如果条件不满足,调用对象的wait方法,被通知后依然要检查条件是否满足
* 3、条件满足以后,才能执行相关的业务逻辑
*/
public synchronized void waitAge(){
System.out.println("age is " + this.age);
while(this.age >= 20){
//条件不满足
try {
System.out.println("current thread is waiting");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//满足条件后执行
System.out.println("current thread" + Thread.currentThread().getName() + " age is " + this.age);
} /**
* 1、 获得对象的锁;
* 2、 改变条件;
* 3、 通知所有等待在对象的线程
*/
public synchronized void changeAge(){
//修改条件
this.age = new Random().nextInt(20);
System.out.println("inform all thread");
//这里使用notifyAll()是因为notify()方法无法指定唤醒某一个线程,notify()的唤醒是随机的
//notifyAll()唤醒所有等待线程
notifyAll();
} }

测试类:

public class WaitAndInform extends Thread{

    private static User user = new User();

    @Override
public void run() {
user.waitAge();
} public static void main(String[] args) throws InterruptedException {
for(int i=0;i<=4;i++){
new WaitAndInform().start();
}
Thread.sleep(1000);
//修改条件 唤醒其他线程
user.changeAge();
}
}

Java多线程之三volatile与等待通知机制示例的更多相关文章

  1. java 多线程:线程通信-等待通知机制wait和notify方法;(同步代码块synchronized和while循环相互嵌套的差异);管道通信:PipedInputStream;PipedOutputStream;PipedWriter; PipedReader

    1.等待通知机制: 等待通知机制的原理和厨师与服务员的关系很相似: 1,厨师做完一道菜的时间不确定,所以厨师将菜品放到"菜品传递台"上的时间不确定 2,服务员什么时候可以取到菜,必 ...

  2. 【Java并发基础】使用“等待—通知”机制优化死锁中占用且等待解决方案

    前言 在前篇介绍死锁的文章中,我们破坏等待占用且等待条件时,用了一个死循环来获取两个账本对象. // 一次性申请转出账户和转入账户,直到成功 while(!actr.apply(this, targe ...

  3. Java并发编程实战 05等待-通知机制和活跃性问题

    Java并发编程系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 Java并发编程实 ...

  4. java多线程系列(三)---等待通知机制

    等待通知机制 前言:本系列将从零开始讲解java多线程相关的技术,内容参考于<java多线程核心技术>与<java并发编程实战>等相关资料,希望站在巨人的肩膀上,再通过我的理解 ...

  5. 《java多线程编程核心技术》不使用等待通知机制 实现线程间通信的 疑问分析

    不使用等待通知机制 实现线程间通信的 疑问分析 2018年04月03日 17:15:08       ayf 阅读数:33 编辑 <java多线程编程核心技术>一书第三章开头,有如下案例: ...

  6. Java并发编程(04):线程间通信,等待/通知机制

    本文源码:GitHub·点这里 || GitEE·点这里 一.概念简介 1.线程通信 在操作系统中,线程是个独立的个体,但是在线程执行过程中,如果处理同一个业务逻辑,可能会产生资源争抢,导致并发问题, ...

  7. Java Concurrency - wait & notify, 等待通知机制

    生产者消费者问题是一个常见的多线程同步案例:一组生产者线程和一组消费者线程共享一个初始状态为空.大小为 N 的缓冲区.只有当缓冲区没满的时候,生产者才能把消息放入缓冲区,否则必须等待:只有缓冲区不空的 ...

  8. Java并发之等待/通知机制

    目录 1 前言 1.1 先来段代码放松一下 2 Object wait()/notify() 2.1 一段入门代码 2.2 问题三连击 a.为什么官方说wait() 要放在while里面? b.为什么 ...

  9. Java并发读书笔记:线程通信之等待通知机制

    目录 synchronized 与 volatile 等待/通知机制 等待 通知 面试常问的几个问题 sleep方法和wait方法的区别 关于放弃对象监视器 在并发编程中,保证线程同步,从而实现线程之 ...

随机推荐

  1. numpy.convolve函数用法

    函数numpy.convolve(a, v, mode=‘full’),这是numpy函数中的卷积函数库 参数: a:(N,)输入的一维数组 b:(M,)输入的第二个一维数组 mode:{‘full’ ...

  2. 异步async与await的简单探究

    在学习.net core的过程中,到处见到异步的使用,Task.async.await随处可见.有点疑惑,就去了解了下这个过程是怎样的. 下面是一段代码,去看看是怎么执行的吧. 一.看看异步执行的方式 ...

  3. W7500S2E串口转以太网

    概述 W7500S2E是一系列串口转以太网模块,支持TCP Server.TCP Client和UDP三种工作模式,串口波特率最高可达460,800bps,并提供配套的上位机配置软件,也可通过网页或A ...

  4. jquery学习总结12-24

    一.jquery操作类的相关方法 1.addClass()方法可以为DOM元素添加类,若添加多个类中间可以用空格连接 2.removeClass()方法可以为DOM元素删除类,若删除多个类中间可以用空 ...

  5. nginx server

    配置nginx 首先apt install nginx 然后安装php apt-get install php7.0-fpm php7.0-mysql php7.0-common php7.0-mbs ...

  6. JVM中的堆和栈

    基本概念: 基本数据类型:byte   short   int   long  char  float  double  boolean 引用数据类型:类类型.接口类型和数组 栈内存: 程序在栈内存中 ...

  7. 缓存为王-varnish

    2.varnish的软件清单 [root@centos69 ~]# rpm -ql varnish /etc/logrotate.d/varnish /etc/rc.d/init.d/varnish ...

  8. 【慕课网实战】五、以慕课网日志分析为例 进入大数据 Spark SQL 的世界

    提交Spark Application到环境中运行spark-submit \--name SQLContextApp \--class com.imooc.spark.SQLContextApp \ ...

  9. scrapy的入门使用(一)

    1. scrapy项目实现流程 创建一个scrapy项目:scrapy startproject mySpider 生成一个爬虫:scrapy genspider  提取数据:完善spider,使用x ...

  10. Ubuntu安装pyenv实现python多版本控制

    Ubuntu安装pyenv实现python多版本控制 git clone git://github.com/yyuu/pyenv.git ~/.pyenv echo 'export PYENV_ROO ...