【Java面试】说说synchronized和volatile的区别

简介: 【Java面试】说说synchronized和volatile的区别

synchronized、volatile区别

  1. volatile主要应用在多个线程对实例变量更改的场合,刷新主内存共享变量的值从而使得各个 线程可
    以获得最新的值,线程读取变量的值需要从主存中读取;synchronized则是锁定当前变 量,只有当前线
    程可以访问该变量,其他线程被阻塞住。另外,synchronized还会创建一个内 存屏障,内存屏障指令保
    证了所有CPU操作结果都会直接刷到主存中(即释放锁前),从而保证 了操作的内存可见性,同时也使
    得先获得这个锁的线程的所有操作.
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
    volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞,比如多个线程争抢 synchronized锁对象时,会出现阻塞。
  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的 修改可见
    性和原子性,因为线程获得锁才能进入临界区,从而保证临界区中的所有语句全部得到 执行。
  4. volatile标记的变量不会被编译器优化,可以禁止进行指令重排;synchronized标记的变量 可以被编
    译器优化。

个人理解JMM: Java Memory Model(Java内存模型),根据并发过程中如何处理、可见性、原子性和有

序性这三个特性而建立的模型。

可见性: JMM提供了volatile变量定义、final、synchronized块来保证可见性。

原子性: 个人理解是如果执行,就执行完,synchronized块来保证。

有序性: 觉得有序是相对性的,根据从哪个线程观察,volatile和synchronized保证线程之间操作的有序

性。

指令重排: 处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证各个语句的执行顺序

同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不
会影响单个线程的执行,但是会影响到线程并发执行的正确性。

JMM处理过程: JMM是通过禁止特定类型的编译器重排序和处理器重排序来为程序员提供一致的内存可

见性保证。例如A线程具体什么时候刷新共享数据到主内存是不确定的,假设我们使用了同步原语

(synchronized,volatile和final),那么刷新的时间是确定的。

  • 每个线程都有一个自己的本地内存空间–虚拟机栈线程空间。线程执行时,先把变量从主内存读取
    到线程自己的本地内存空间,然后再对该变量进行操作
  • 对该变量操作完后,在某个时间再把变量刷新回主内存,所以线程A释放锁后会同步到主内存,线
    程B获取锁后会同步主内存数据,即“A线程释放锁–B线程获取锁”可以实现A,B线程之间的通信

    稍微解释下:
    假设本地内存A和B有主内存中共享变量x的副本,初始时这三个内存中的x值都为0。线程A在执行
    时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,
    线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。然后,
    线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。

建议了解下线程的生命状态,这里就不多做解释,面试的时候很有可能会被问到。

原子性

原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。及

时在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。我们先来看看哪些是原

子操作,哪些不是原子操作,有一个直观的印象:

int a = 10; //1
a++; //2
int b=a; //3
a = a+1; //4

上面这四个语句中只有第1个语句是原子操作,将10赋值给线程工作内存的变量a,而语句2(a++),实

际上包含了三个操作:1. 读取变量a的值;2:对a进行加一的操作;3.将计算后的值再赋值给变量a,而

这三个操作无法构成原子操作。对语句3,4的分析同理可得这两条语句不具备原子性。当然,java内存模

型中定义了8中操作都是原子的,不可再分的。

  1. lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态;
  2. unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可
    以被其他线程锁定
  3. read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便
    后面的load动作使用;
  4. load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中
    的变量副本
  5. use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚
    拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;
  6. assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变
    量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
  7. store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后
    的write操作使用;
  8. write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的
    变量中。

上面的这些指令操作是相当底层的,可以作为扩展知识面掌握下。那么如何理解这些指令了?比如,把一

个变量从主内存中复制到工作内存中就需要执行read,load操作,将工作内存同步到主内存中就需要执行

store,write操作。注意的是:java内存模型只是要求上述两个操作是顺序执行的并不是连续执行的。也

就是说read和load之间可以插入其他指令,store和writer可以插入其他指令。比如对主内存中的a,b进

行访问就可以出现这样的操作顺序:read a,read b, load b,load a。

由原子性变量操作read,load,use,assign,store,write,可以大致认为基本数据类型的访问读写具备原子

性(例外就是long和double的非原子性协定)

synchronized

上面一共有八条原子操作,其中六条可以满足基本数据类型的访问读写具备原子性,还剩下lock和

unlock两条原子操作。如果我们需要更大范围的原子性操作就可以使用lock和unlock原子操作。尽管

jvm没有把lock和unlock开放给我们使用,但jvm以更高层次的指令monitorenter和monitorexit指令开

放给我们使用,反应到java代码中就是—synchronized关键字,也就是说synchronized满足原子性。

volatile

我们先来看这样一个例子:

public class VolatileExample {
private static volatile int counter = 0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++)
counter++;
}
});
thread.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter);
}
}

开启10个线程,每个线程都自加10000次,如果不出现线程安全的问题最终的结果应该就是:10*10000

= 100000;可是运行多次都是小于100000的结果,问题在于 volatile并不能保证原子性,在前面说过

counter++这并不是一个原子操作,包含了三个步骤:1.读取变量counter的值;2.对counter加一;3.将

新值赋值给变量counter。如果线程A读取counter到工作内存后,其他线程对这个值已经做了自增操作

后,那么线程A的这个值自然而然就是一个过期的值,因此,总结果必然会是小于100000的。

如果让volatile保证原子性,必须符合以下两条规则:

  1. 运算结果并不依赖于变量的当前值,或者能够确保只有一个线程修改变量的值;
  2. 变量不需要与其他的状态变量共同参与不变约束

有序性

synchronized

synchronized语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。因

此,synchronized语义就要求线程在访问读写共享变量时只能“串行”执行,因此synchronized具有有序

性。

volatile

在java内存模型中说过,为了性能优化,编译器和处理器会进行指令重排序;也就是说java程序天然的

有序性可以总结为:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,

所有的操作都是无序的。在单例模式的实现上有一种双重检验锁定的方式(Double-checked

Locking)。代码如下:

public class Singleton {
private Singleton() { }
private volatile static Singleton instance;
public Singleton getInstance(){
if(instance==null){
synchronized (Singleton.class){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}

这里为什么要加volatile了?我们先来分析一下不加volatile的情况,有问题的语句是这条:

instance = new Singleton();

这条语句实际上包含了三个操作:1.分配对象的内存空间;2.初始化对象;3.设置instance指向刚分配的

内存地址。但由于存在重排序的问题,可能有以下的执行顺序:

线程A:1:分配对象的内存空间-》3:设置instance指向内存空间 --》线程B抢占-》判断instance是否为null(是)-》线程B初次访问对象-》线程A抢占-》2:初始化对象-》4:线程A初次访问对象

如果2和3进行了重排序的话,线程B进行判断if(instance==null)时就会为true,而实际上这个instance并

没有初始化成功,显而易见对线程B来说之后的操作就会是错得。而用volatile修饰的话就可以禁止2和3

操作重排序,从而避免这种情况。volatile包含禁止指令重排序的语义,其具有有序性。

可见性

可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。通过之前对synchronzed

内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变

量同步到主内存中。从而,synchronized具有可见性。同样的在volatile分析中,会通过在指令中添加

lock指令,以实现内存可见性。因此, volatile具有可见性

总结

通过这篇文章,主要是比较了synchronized和volatile在三条性质:原子性,可见性,以及有序性的情

况,归纳如下:

synchronized: 具有原子性,有序性和可见性;

volatile:具有有序性和可见性


相关文章
|
9天前
|
Java
java基础(4)public class 和class的区别及注意事项
本文讲解了Java中`public class`与`class`的区别和注意事项。一个Java源文件中只能有一个`public class`,并且`public class`的类名必须与文件名相同。此外,可以有多个非`public`类。每个类都可以包含一个`main`方法,作为程序的入口点。文章还强调了编译Java文件生成`.class`文件的过程,以及如何使用`java`命令运行编译后的类。
15 3
java基础(4)public class 和class的区别及注意事项
|
4天前
|
Java
java中面向过程和面向对象区别?
java中面向过程和面向对象区别?
13 4
ly~
|
7天前
|
安全 Java 大数据
php跟java有什么区别
PHP 和 Java 是两种常用编程语言,各有特色。PHP 语法简洁灵活,适用于快速开发中小型网站,尤其在 Web 脚本和数据库交互中表现出色。Java 则语法严谨,强类型特性使其在企业级应用、移动开发及大数据处理中更受欢迎,具备高稳定性和安全性。通过优化,PHP 性能可提升,而 Java 在大规模应用中表现更佳。总体而言,PHP 开发效率高但维护性稍差,Java 则更注重代码质量和安全性。
ly~
16 5
|
13天前
|
Java 大数据 API
Java 流(Stream)、文件(File)和IO的区别
Java中的流(Stream)、文件(File)和输入/输出(I/O)是处理数据的关键概念。`File`类用于基本文件操作,如创建、删除和检查文件;流则提供了数据读写的抽象机制,适用于文件、内存和网络等多种数据源;I/O涵盖更广泛的输入输出操作,包括文件I/O、网络通信等,并支持异常处理和缓冲等功能。实际开发中,这三者常结合使用,以实现高效的数据处理。例如,`File`用于管理文件路径,`Stream`用于读写数据,I/O则处理复杂的输入输出需求。
|
17天前
|
机器学习/深度学习 人工智能 安全
python和Java的区别以及特性
Python:适合快速开发、易于维护、学习成本低、灵活高效。如果你需要快速上手,写脚本、数据处理、做点机器学习,Python就是你的首选。 Java:适合大型项目、企业级应用,性能要求较高的场景。它类型安全、跨平台能力强,而且有丰富的生态,适合更复杂和规模化的开发。
18 3
|
22天前
|
存储 安全 Java
Java并发编程之深入理解Synchronized关键字
在Java的并发编程领域,synchronized关键字扮演着守护者的角色。它确保了多个线程访问共享资源时的同步性和安全性。本文将通过浅显易懂的语言和实例,带你一步步了解synchronized的神秘面纱,从基本使用到底层原理,再到它的优化技巧,让你在编写高效安全的多线程代码时更加得心应手。
|
24天前
|
缓存 Java 编译器
JAVA并发编程synchronized全能王的原理
本文详细介绍了Java并发编程中的三大特性:原子性、可见性和有序性,并探讨了多线程环境下可能出现的安全问题。文章通过示例解释了指令重排、可见性及原子性问题,并介绍了`synchronized`如何全面解决这些问题。最后,通过一个多窗口售票示例展示了`synchronized`的具体应用。
|
2月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
2月前
|
XML 存储 JSON
【IO面试题 六】、 除了Java自带的序列化之外,你还了解哪些序列化工具?
除了Java自带的序列化,常见的序列化工具还包括JSON(如jackson、gson、fastjson)、Protobuf、Thrift和Avro,各具特点,适用于不同的应用场景和性能需求。
|
2月前
|
Java
【Java基础面试三十七】、说一说Java的异常机制
这篇文章介绍了Java异常机制的三个主要方面:异常处理(使用try、catch、finally语句)、抛出异常(使用throw和throws关键字)、以及异常跟踪栈(异常传播和程序终止时的栈信息输出)。
下一篇
无影云桌面