详解java并发编程(2) --Synchronized与Volatile区别

1 Synchronized

在多线程并发中synchronized一直是元老级别的角色。利用synchronized来实现同步具体有一下三种表现形式:

  1. 对于普通的同步方法,锁是当前实例对象。
  2. 对于静态同步方法,锁是当前类的class对象。
  3. 对于同步方法块,锁是synchronized括号里配置的对象。

当一个代码,方法或者类被synchronized修饰以后。当一个线程试图访问同步代码块的时候,它首先必须得到锁,退出或抛出异常的时候必须释放锁。那么这样做有什么好处呢?

它主要确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量的可见性和排他性。

1.1 如何实现排他性

如下图所示,一个普通的方法会有一个左右摆动的开关,可以连接到任意一个线程,如果该方法代码不是原子性的,可能会出现一个线程并没有将方法代码执行完毕就链接到另一个线程中去。而被synchronized修饰的方法,链接到一个线程后,除非这个线程将方法执行完毕或者抛出异常,开关才会链接至别的线程。就这样将一个并行的操作变了穿行操作。(同一时间保证只有一个线程在执行方法代码)

 int i = 1;
  public synchronized void increment(){
    i++;
  }

在前面并发基础及锁的原理中我们介绍过i++并不是原子操作,所有当多个线程同时操作i++的时候可能会出现多线程并发问题。而上诉代码块中i++是在synchronized修饰的方法中。其中一个线程进入该方法首先获得当前实例对象的锁,当另一个线程试图执行该方法的时候,由于前一个线程并没有执行完毕释放掉锁,所以该线程挂起等待锁的释放。

通过加锁的方式我们实现了将i++非原子操作的方法变成了原子操作的方法。从而实现了排他性。

1.2 如何实现可见性

因为在java内存模型中规定:在执行被synchronized修饰的代码时,线程首先获取锁→清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将工作内存中更改后的共享变量的值刷新到主内存中→释放互斥锁。

这里有一个细节需要注意: 当一个线程A将最新的共享变量刷新到主内存的时候,会导致缓存在其他线程B的工作内存的这个共享变量失效。
当线程B下一次去访问这个变量的时候,会发现,工作缓存的这个变量已经失效。会强制从主内存中重新读取这个共享变量

2 Volatile

当声明共享变量为volatile后,对这个变量的读/写将会很特别。volatile可以说是java虚拟机提供的最轻量级的同步机制。他只能能只能保证变量的可见性与读/写的原子性。要理解volatile确实是不容易的,接下来我们进入深入的分析!

2.1 volatile的特性

下面有两个示例代码:

public class VolatileTest1 {
  volatile long a = 0L;          //使用volatile声明64位的long型变量

  public void set(long b) {
    a = b;               //单个volatile变量的写
  }

  public void increment() {
    a++;                //复合(多个)volatile变量的读/写
  }

  public long get() {
    return a;              //的那个volatile变量的读
  }
}
public class VolatileTest2 {
  long a = 0L;                //64位的普通long型变量

  public synchronized void set(long b) {   //单个普通变量的写使用同步锁
    a = b;
  }

  public void increment() {          //普通方法调用
    long tmp = get();            //调用以同步的读方法
    tmp += 1;                //普通的写操作
    set(tmp);                //调用以同步的写方法
  }

  public synchronized long get() {      //单个普通变量的读使用同步锁
    return a;
  }
}

上述两个示例代码所带来的的执行效果是相同的。

可以看到被volatile修饰的变量读与写操作是原子性的。如前面所述,被Synchronized修饰的变量每次写操作完成后,会强制将工作内存中缓存的共享变量强制刷新到主内存中。所以保证了volatile修饰变量的可见性。

从上述示例代码中我们也能看出,即便读与写是原子性,但是依旧不能保证 a++;是原子操作。这也是很多人对volatile字段理解困难的原因所在。

简而言之,volatile变量自身具有下列特征。

  1. 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  2. 原子性:对任意单个volat变量的读 / 写具有原子性,但类似volatile++这种复合操作不具有原子性。

在这里楼主插一个之前遇到的面试题:请问对于double和long类型的读写是原子性的吗?double和long类型是64位的,在一些32位的处理器上,可能会把一个64位的long/double型变量的写操作才分为两个32位的写操作来执行。座椅此时对这个64位变量的写操作将不具有原子性。但是如果被volatile修饰的话,写64位的double和long的操作依旧是原子操作。

2.2 volatile的禁止重排序

除了前面内存可见性中讲到的volatile关键字可以保证变量修改的可见性之外,还有另一个重要的作用:在JDK1.5之后,可以使用volatile变量禁止指令重排序。

volatile关键字通过提供“内存屏障”的方式来防止指令被重排序,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:

  1. 在每个volatile写操作的前面插入一个StoreStore屏障。
  2. 在每个volatile写操作的后面插入一个StoreLoad屏障。
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障。
  4. 在每个volatile读操作的后面插入一个LoadStore屏障

总结来说:

  1. volatile写操作之前的操作不会被编译器重排序到写操作之后。
  2. volatile读之后的操作不会被编译器重排序到volatile读操作之前。
  3. 第一个是volatile读操作,第二个是volatile写操作,不能重排序

2.3 volatile的使用场景

1.状态标志

用volatile修饰的boolean 变量来作为while循环的的判断条件:当这个变量被其他线程修改的时候能保证while循环能立即读到。

2.一次性安全发布

初始化对象的正确步骤为:

  1. 1、分配对象的内存空间
  2. 2、初始化对象
  3. 3、设置引用指向刚分配的内存地址

然而由于重排序机制,可能导致2、3步骤重排序,导致初始化对象的步骤变为 1-3-2。
著名的双重检查锁定存在的问题就是因为初始化对象的重排序,引用所指向的对象可能还没有完成初始化,而仅仅是指向了一个空的内存地址。

3.独立观察

这是第一种使用场景的引用。例如一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。

4.开销较低的读-写锁策略

前面我们介绍过,因为 ++x 实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值有可能会丢失。但是被volatile修饰变量的读 / 写却是原子操作。所以当共享变量被volatile修饰之后,我们只需要在复合操作的方法上加上synchronized比直接用synchronized修饰该变量效率高的多。

2.4 volatile总结

相对于synchronized块的代码锁,volatile应该是提供了一个轻量级的针对共享变量的锁,当我们在多个线程间使用共享变量进行通信的时候需要考虑将共享变量用volatile来修饰。

volatile是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操作,也就不会执行线程阻塞,因此volatilei变量是一种比synchronized关键字更轻量级的同步机制。

3 synchronized和volatile的区别

1、 volatile不会进行加锁操作:

volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。

2、volatile变量作用类似于同步变量读写操作:

从内存可见性的角度看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。

3、volatile不如synchronized安全:

在代码中如果过度依赖volatile变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更难以理解。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它。一般来说,用同步机制会更安全些。

4、volatile无法同时保证内存可见性和原则性:

加锁机制(即同步机制)既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性,原因是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count++”、“count = count+1”。

以上所述是小编给大家介绍的Synchronized与Volatile区别详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • Java多线程并发编程 Synchronized关键字

    synchronized 关键字解析 同步锁依赖于对象,每个对象都有一个同步锁. 现有一成员变量 Test,当线程 A 调用 Test 的 synchronized 方法,线程 A 获得 Test 的同步锁,同时,线程 B 也去调用 Test 的 synchronized 方法,此时线程 B 无法获得 Test 的同步锁,必须等待线程 A 释放 Test 的同步锁才能获得从而执行对应方法的代码. 综上,正确使用 synchronized 关键字可确保原子性. synchronized 关键字的特

  • 详解Java利用同步块synchronized()保证并发安全

    本文实例为大家分享了Java利用同步块synchronized()保证并发安全的具体代码,供大家参考,具体内容如下 package day10; /** * 同步块 * 有效地缩小同步范围 * 可以在保证并发安全的同时尽可能提高并发效率 * * 实例:模拟两个人同时进店买衣服,为提高效率 * 只在试衣服阶段进行同步排队过程,其他阶段无需排队. * @author kaixu * */ public class SyncDemo2 { public static void main(String[

  • Java 并发编程学习笔记之Synchronized底层优化

    一.重量级锁 上篇文章中向大家介绍了Synchronized的用法及其实现的原理.现在我们应该知道,Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的.但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的.而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因.因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为"重量级锁"

  • Java 并发编程学习笔记之Synchronized简介

    一.Synchronized的基本使用 Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法.Synchronized的作用主要有三个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题.从语法上讲,Synchronized总共有三种用法: (1)修饰普通方法 (2)修饰静态方法 (3)修饰代码块 接下来我就通过几个例子程序来说明一下这三种使用方式(为了便于比较,三段代码除了Synchronized的使用方式不同以外,

  • 聊聊Java并发中的Synchronized

    1 引言 在多线程并发编程中Synchronized一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,有些情况下它并不那么重了,本文详细介绍了Java SE1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程. 2 术语定义 术语 英文 说明 CAS Compare and Swap 比较并设置.用于在硬件层面上提供原子性操作.在 Intel 处理器中,比较并交换通过指令cmpxch

  • Java并发 synchronized锁住的内容解析

    synchronized用在方法上锁住的是什么? 锁住的是当前对象的当前方法,会使得其他线程访问该对象的synchronized方法或者代码块阻塞,但并不会阻塞非synchronized方法. 脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量或者全局静态变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过的.注意这里 局部变量是不存在脏读的情况 public class ThreadDomain13 {

  • 详解java并发编程(2) --Synchronized与Volatile区别

    1 Synchronized 在多线程并发中synchronized一直是元老级别的角色.利用synchronized来实现同步具体有一下三种表现形式: 对于普通的同步方法,锁是当前实例对象. 对于静态同步方法,锁是当前类的class对象. 对于同步方法块,锁是synchronized括号里配置的对象. 当一个代码,方法或者类被synchronized修饰以后.当一个线程试图访问同步代码块的时候,它首先必须得到锁,退出或抛出异常的时候必须释放锁.那么这样做有什么好处呢? 它主要确保多个线程在同一

  • 详解Java并发编程基础之volatile

    目录 一.volatile的定义和实现原理 1.Java并发模型采用的方式 2.volatile的定义 3.volatile的底层实现原理 二.volatile的内存语义 1.volatile的特性 2.volatile写-读建立的happens-before关系 3.volatile的写/读内存语义 三.volatile内存语义的实现 1.volatile重排序规则 2.内存屏障 3.内存屏障示例 四.volatile与死循环问题 五.volatile对于复合操作非原子性问题 一.volati

  • 详解Java并发编程之原子类

    目录 原子数组 AtomicIntegerArray 原子更新器 AtomicIntegerFieldUpdater 原子累加器 LongAdder 原子数组 原子数组有AtomicIntegerArray.AtomicLongArray.AtomicReferenceArray,主要是用来对数组中的某个元素进行原子操作.三个类的方法基本类似,这里只介绍一下AtomicIntegerArray的方法. AtomicIntegerArray 两个构造方法,第一个构造方法传入数组长度初始化一个所有值

  • 详解Java并发编程之内置锁(synchronized)

    简介 synchronized在JDK5.0的早期版本中是重量级锁,效率很低,但从JDK6.0开始,JDK在关键字synchronized上做了大量的优化,如偏向锁.轻量级锁等,使它的效率有了很大的提升. synchronized的作用是实现线程间的同步,当多个线程都需要访问共享代码区域时,对共享代码区域进行加锁,使得每一次只能有一个线程访问共享代码区域,从而保证线程间的安全性. 因为没有显式的加锁和解锁过程,所以称之为隐式锁,也叫作内置锁.监视器锁. 如下实例,在没有使用synchronize

  • 详解Java函数式编程和lambda表达式

    为什么要使用函数式编程 函数式编程更多时候是一种编程的思维方式,是种方法论.函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做.说白了,函数式编程是基于某种语法或调用API去进行编程.例如,我们现在需要从一组数字中,找出最小的那个数字,若使用用命令式编程实现这个需求的话,那么所编写的代码如下: public static void main(String[] args) { int[] nums = new int[]{1, 2, 3, 4, 5,

  • 详解JAVA 函数式编程

    1.函数式接口 1.1概念: java中有且只有一个抽象方法的接口. 1.2格式: 修饰符 interface 接口名称 { public abstract 返回值类型 方法名称(可选参数信息); // 其他非抽象方法内容 } //或者 public interface MyFunctionalInterface { void myMethod(); } 1.3@FunctionalInterface注解: 与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解

  • 详解Java网络编程

    一.网络编程 1.1.概述 1.计算机网络是通过传输介质.通信设施和网络通信协议,把分散在不同地点的计算机设备互连起来,实现资源共享和数据传输的系统.网络编程就就是编写程序使联网的两个(或多个)设备(例如计算机)之间进行数据传输.Java语言对网络编程提供了良好的支持,通过其提供的接口我们可以很方便地进行网络编程. 2.Java是 Internet 上的语言,它从语言级上提供了对网络应用程 序的支持,程序员能够很容易开发常见的网络应用程序. 3.Java提供的网络类库,可以实现无痛的网络连接,联

  • 详解Java事件编程的使用

    Java事件编程 当前在线网店很多,很涉及商品管理和销售的问题,比如: 一,在商品库存管理的商品增加时,我们主要业务时编辑保持商品信息, 同时因商品增加而附带有一些"非主要业务",如: 1,应商品的库存数量等更新, 2,热销产品的推广处理等 二,在商品产生订单时,我们的主要业务(对买家而言)是建立订单业务, 同时因产生订单而附带有一些不是买家关心的"非主要业务",如: 1,库存和已售数量的更新 2,发货的准备处理事宜 3,物流的处理事宜 非主要业务我们可以让程序使用

  • 详解Java中的sleep()和wait()的区别

    详解Java中的sleep()和wait()的区别 对于sleep()方法,我们首先要知道该方法是属于Thread类中的.而wait()方法,则是属于Object类中的. sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态. 在调用sleep()方法的过程中,线程不会释放对象锁. 而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象

  • 详解Java中Comparable和Comparator接口的区别

    详解Java中Comparable和Comparator接口的区别 本文要来详细分析一下Java中Comparable和Comparator接口的区别,两者都有比较的功能,那么究竟有什么区别呢,感兴趣的Java开发者继续看下去吧. Comparable 简介 Comparable 是排序接口. 若一个类实现了Comparable接口,就意味着"该类支持排序".  即然实现Comparable接口的类支持排序,假设现在存在"实现Comparable接口的类的对象的List列表(

随机推荐