java面试题之volatile和synchronized的使用方法和区别

我们先来看一下Java 内存模型中的可见性、原子性和有序性。

可见性:

可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。

 

原子性:

原子是世界上的最小单位,具有不可分割性。synchronized块之间的操作就具备原子性。volatile关键字定义的变量就可以做到这一点,Java还有两个关键字能实现可见性,即synchronized和final。

 

有序性:

如果在本线程内观察,所有的操作都是有序的:如果在一个线程中观察另外一个线程,所有的线程操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性

 

首先来看一下jvm的内存模型

 

JVM中存在一个主存区(Main Memory或Java Heap Memory),Java中所有变量都是存在主存中的,对于所有线程进行共享,而每个线程又存在自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。

 

我们来模拟一下2个线程在内存中去修改一个变量的过程,线程1对共享变量的修改要想被线程2及时看到,必须要经过如下两个步骤:

1)把工作内存1中更新过的共享变量刷新到主内存中。

2)将主内存中最新的共享变量的值更新到工作内存2中。

1.首先线程1和线程2都会从主存中拷贝一份变量X的副本到自己的工作内存中。

2.线程1在自己的工作内存中修改变量X的副本。

3.线程1修改自己工作内存中的副本X后,把修改后的X同步到主存中。

4.主存中X的值更新成功后,通知其他线程同步X的值,当然这里我们只有2个线程,那么主存的X的值被线程1修改后会同步给线程2。这样就保证了变量在多个线程中的可见性。

在java中可以实现可见性的两个关键字 

 1)使用关键字synchronized

 2)使用关键字volatile

 

Synchronized能够实现多线程的原子性(同步)和可见性。

JVM关于Synchronized的两条规定:

1)线程解锁前,必须把共享变量的最新值刷新到主内存中。

2)线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁和解锁需要同一把锁)。

 

Synchronized执行互斥代码的过程

1)获得互斥锁

2)清空工作内存

3)从主内存拷贝变量的最新副本到工作内存

4)执行代码

5)将更改后的共享变量的值刷新到主内存

6)释放互斥锁

 

volatile可以保证变量的可见性,但是不能保证复合操作的原子性

关注公众号,一起学java

volatile如何实现内存可见性?

深入来说:通过加入内存屏障和禁止重排序优化来实现的。

1)对volatile变量执行写操作时,会在写操作后加入一条store屏障指令。

2)对volatile变量执行读操作时,会在读操作后加入一条load屏障指令。

 

通俗地讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存,这样任何时刻,不同的线程总能看到该变量的最新值。

 

线程写volatile变量的过程:

1)改变线程工作内存中volatile变量副本的值。

2)将改变后的副本的值从工作内存刷新到主内存。

 

线程读volatile变量的过程:

1)从主内存中读取volatile变量的最新值到线程的工作内存中。

2)从工作内存中读取volatile变量的副本。

 

volatile不能保证volatile变量复合操作的原子性

对于下面的一段程序的使用volatile和synchronized

private int number = 0;              

number++;//不是原子操作                   

1读取number的值                      

2将number的值加1                    

3写入最新的number的值  

//加入synchronized,变为原子操作   

synchronized(thhis){ 

        number++; 

}

//变为volatile变量,无法保证原子性

private volatile int number = 0;

 

volatile变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。

出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile 变量而不是锁。当使用 volatile 变量而非锁时,某些习惯用法(idiom)更加易于编码和阅读。此外,volatile 变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于锁的性能优势。

 

volatile适合的使用场景

只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

(1)对变量的写入操作不依赖其当前值

(2)该变量没有包含在具有其他变量的不变式中。

第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由(读取-修改-写入)操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使x 的值在操作期间保持不变,而 volatile 变量无法实现这点。(然而,如果只从单个线程写入,那么可以忽略第一个条件。)

 

总结:

1)volatile比synchronized更轻量级。

2)volatile没有synchronized使用的广泛。

3)volatile不需要加锁,比synchronized更轻量级,不会阻塞线程。

4)从内存可见性角度看,volatile读相当于加锁,volatile写相当于解锁。

5)synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。

6)volatile本身不保证获取和设置操作的原子性,仅仅保持修改的可见性。但是java的内存模型保证声明为volatile的long和double变量的get和set操作是原子的。

 

注意:

对64位(long、double)变量的读写可能不是原子操作

Java内存模型允许JVM将没有被volatile修饰的64位数据类型的读写操作划分为两次32位的读写操作来运行。

 

导致问题:有可能会出现读取到半个变量的情况。

解决方法:加volatile关键字。

 

一个问题:即使没有保证可见性的措施,很多时候共享变量依然能够在主内存和工作内存间得到及时的更新?

 

        答:一般只有在短时间内高并发的情况下才会出现变量得不到及时更新的情况,因为CPU在执行时会很快地刷新缓存,所以一般情况下很难看到这种问题。慢了不就不会刷新了。CPU运算快的话,在分配的时间片内就能完成所有工作:工作内从1->主内存->工作内存2,这样一来就保证了数据的可见性。在这个过程中,假如线程没有在规定时间内完成工作,然后这个线程就释放CPU,分配给其它线程,该线程就需要等待CPU下次给该线程分配时间片,如果在这段时间内有别的线程访问共享变量,可见性就没法保证了。

关注公众号,一起学java


 

 

  • 11
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值