Java多线程之内存可见性和原子性:Synchronized和Volatile的比较

在说明Java多线程内存可见性之前,先来简单了解一下Java内存模型。

(1)Java所有变量都存储在主内存中
     (2)每个线程都有自己独立的工作内存,里面保存该线程的使用到的变量副本(该副本就是主内存中该变量的一份拷贝)

(1)线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接在主内存中读写
   (2)不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
线程1对共享变量的修改,要想被线程2及时看到,必须经过如下2个过程:

(1)把工作内存1中更新过的共享变量刷新到主内存中
   (2)将主内存中最新的共享变量的值更新到工作内存2中

可见性与原子性

可见性:一个线程对共享变量的修改,更够及时的被其他线程看到
   原子性:即不可再分了,不能分为多步操作。比如赋值或者return。比如"a = 1;"和 "returna;"这样的操作都具有原子性。类似"a +=b"这样的操作不具有原子性,在某些JVM中"a +=b"可能要经过这样三个步骤:
① 取出a和b
② 计算a+b
③ 将计算结果写入内存


(1)Synchronized:保证可见性和原子性
   Synchronized能够实现原子性和可见性;在Java内存模型中,synchronized规定,线程在加锁→先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。

(2)Volatile:保证可见性,但不保证操作的原子性
   Volatile实现内存可见性是通过store和load指令完成的;也就是对volatile变量执行写操作时,会在写操作后加入一条store指令,即强迫线程将最新的值刷新到主内存中;而在读操作时,会加入一条load指令,即强迫从主内存中读入变量的值。但volatile不保证volatile变量的原子性,例如:

(3)Synchronized和Volatile的比较
    1)Synchronized保证内存可见性和操作的原子性

加锁----清空内存----在主存中拷贝最新副本----执行+修改--------刷回主存-------释放锁

2)Volatile只能保证内存可见性

          a.每次读取的时候都会CAS

         b.每次写完都会store回主存

3)Volatile不需要加锁(忙等待,做自旋),比Synchronized更轻量级,并不会阻塞线程(volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。)
    4)volatile标记的变量不会被编译器优化(通过volatile保证了有序性),而synchronized标记的变量可以被编译器优化(如编译器重排序的优化). (synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性)所以根据before-happen原则,也可以保证有序性。

5)volatile是变量修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符。
     volatile本质是在告诉JVM当前变量在寄存器中的值是不确定的,使用前,需要先从主存中读取,因此可以实现可见性。而对n=n+1,n++等操作时,volatile关键字将失效,不能起到像synchronized一样的线程同步(原子性)的效果。


 Java  Synchronize 和 Lock 的区别与用法

    java为此也提供了2种锁机制,synchronized和lock。

一、synchronized和lock的用法区别
 
synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
 
lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
 
用法区别比较简单,这里不赘述了,如果不懂的可以看看Java基本语法。

二、synchronized和lock性能区别
 
synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。

但是到了Java1.6,发生了变化。synchronized在语义上很清晰,可以进行很多优化,有适应自旋,锁消除(消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。),锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。

三、synchronized和lock用途区别
 
synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面几种需求的时候。

*(当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。注意:synchronized不提供中断)
 
1.某个线程在等待一个锁的控制权的这段时间需要中断
2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程(所谓notify也就是notify等待这个加锁对象的锁池(entryset)里面的线程)
3.具有公平锁功能,每个到来的线程都将排队等候
 
先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。

比如:如果A、B2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制,

第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此)(也就是说,B在我的锁池里,B如果想要中断自己不想等待这把锁,那么需要经过我的响应,比如做一些引用计数的操作,才能中断);第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。
 
这里来做个试验,首先搞一个Buffer类,它有读操作和写操作,为了不读到脏数据,写和读都需要加锁,我们先用synchronized原语来加锁

我们期待“读”这个线程能退出等待锁,可是事与愿违,一旦读这个线程发现自己得不到锁,就一直开始等待了,就算它等死,也得不到锁,因为写线程要21亿秒才能完成 T_T ,即使我们中断它,它都不来响应下,看来真的要等死了。这个时候,ReentrantLock给了一种机制让我们来响应中断,让“读”能伸能屈,勇敢放弃对这个锁的等待。我们来改写Buffer这个类,就叫BufferInterruptibly吧,可中断缓存。

这次“读”线程接收到了lock.lockInterruptibly()中断,并且有效处理了这个“异常”。

 

至于第二种情况,ReentrantLock可以与Condition的配合使用,Condition为ReentrantLock锁的等待和释放提供控制逻辑。
 

Lock lock = new ReentrantLock();
Condition cond = lock.newCondition();

例如,使用ReentrantLock加锁之后,可以通过它自身的Condition.await()方法释放该锁,线程在此等待Condition.signal()方法,然后继续执行下去。await方法需要放在while循环中,因此,在不同线程之间实现并发控制,还需要一个volatile的变量,boolean是原子性的变量。

调用spillDone.await()时可以释放spillLock锁,线程进入阻塞状态,而等待其他线程的spillDone.signal()操作时,就会唤醒线程,重新持有spillLock锁。
 
这里可以看出,利用lock可以使我们多线程交互变得方便,而使用synchronized则无法做到这点。


 
最后呢,ReentrantLock这个类还提供了2种竞争锁的机制:公平锁(先来后到原则,估计就是一个队列性质)和非公平锁(不分先后,估计就是一个类似于set)。

这2种机制的意思从字面上也能了解个大概:即对于多线程来说,公平锁会依赖线程进来的顺序,后进来的线程后获得锁。而非公平锁的意思就是后进来的锁也可以和前边等待锁的线程同时竞争锁资源。对于效率来讲,当然是非公平锁效率更高,因为公平锁还要判断是不是线程队列的第一个才会让线程获得锁。

这两种排队策略供我们自己选择。


总结

(1)synchronized与volatile的比较

1)volatile比synchronized更轻量级。

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

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

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

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

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

volatile的long和double变量的get和set操作是原子的。

(2)补充1

共享数据的访问权限都必须定义为private。一般是考虑安全性,对数据提供保护,可以通过set()方法赋值,再

通过get()方法取值,这就是java封装的思想。

Java中对共享数据操作的并发控制是采用加锁技术

Java中没有提供检测与避免死锁的专门机制,但应用程序员可以采用某些策略防止死锁的发生。

final也可以保证内存可见性。

(3)补充2

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

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

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

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

        解决方法:加volatile关键字。

       不过现在也有很多系统实现将没有使用volatile的64位long\double的变量也设置为原子操作

(4)一个问题

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

          是的,不过要看当前线程并发量

答:一般只有在短时间内高并发的情况下才会出现变量得不到及时更新的情况,因为CPU在执行时会很快地刷新

缓存,所以一般情况下很难看到这种问题。

慢了不就不会刷新了。。。CPU运算快的话,在分配的时间片内就能完成所有工作:工作内从1->主内存->工作

内存2,然后这个线程就释放CPU时间片,这样一来就保证了数据的可见性。如果是慢了话CPU强行剥夺该线的资

源,分配给其它线程,该线程就需要等待CPU下次给该线程分配时间片,如果在这段时间内有别的线程访问共享变

量,可见性就没法保证了。

选择比较

除非需要使用 ReentrantLock 的高级功能(中断、公平锁)否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

多线程:synchronize、volatile、Lock 的区别与用法相关推荐

  1. 深入研究 Java Synchronize 和 Lock 的区别与用法

    在分布式开发中,锁是线程控制的重要途径.Java为此也提供了2种锁机制,synchronized和lock.做为Java爱好者,自然少不了对比一下这2种机制,也能从中学到些分布式开发需要注意的地方. ...

  2. Synchronize和Lock 的区别与用法

    synchronized和lock的用法区别 synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象. lock ...

  3. synchronize和lock的区别 synchionzie与volatile的区别

    synchronized与Lock的区别 https://www.cnblogs.com/iyyy/p/7993788.html Lock和synchronized和volatile的区别和使用 ht ...

  4. synchronize与lock的区别

    https://blog.csdn.net/Maxiao1204/article/details/85065510

  5. 关于synchronize与lock的区别

  6. Java之多线程里面的锁理解以及synchronized与Lock的区别

    一.宏观的说下锁的分类 1)锁分为乐观锁.悲观锁 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改.因此对于同一个数据的并发操作,悲观锁采取加锁的形式.悲观的认为,不 ...

  7. 题目:三个售票员 卖出 30张票 || 多线程编程的企业级套路+模板||synchronized与Lock的区别

    package com.dym.juc;//资源类 class Ticket {private int number =30;public synchronized void saleTicket() ...

  8. synchronized与Lock的区别与使用

    原文链接 https://blog.csdn.net/u012403290/article/details/64910926 ###引言: 昨天在学习别人分享的面试经验时,看到Lock的使用.想起自己 ...

  9. java并发,同步synchronize和lock锁的使用方法和注意,死锁案例分析

    1.什么是线程安全问题 多个线程同时共享同一个全局变量或者静态变量的时候,某个线程的写操作,可能会影响到其他线程操作这个变量.所有线程读一个变量不会产生线程安全问题. 实际场景就是火车站买票问题:剩余 ...

最新文章

  1. 内存错误 处理 [CAlayer release]
  2. 安利 10 个 Intellij IDEA 实用插件
  3. windows上报错:Could not find a version that satisfies the requirement torch==0.4.1
  4. 各个大厂的机器学习平台概述
  5. UNICODE字符集
  6. 科研工作者结合实验与计算机模拟,理论物理前沿重点实验室
  7. VSCode 代码风格统一设置eslint + stylelint
  8. Python——文件操作3——文件修改
  9. CSS的50个代码片段
  10. H.264 视频编码器的研究与分析
  11. python物业管理系统_小型物业管理系统的设计与实现研究背景及意义
  12. 计算机属于附属常用工具吗,计算机常用工具软件试题.doc
  13. Java基础入门笔记
  14. ORACLE莫明其妙出错!
  15. vue 组件开发 ---- rui-vue-poster 海报制作
  16. 编程基础 - 线索二叉树 (Threaded Binary Tree)
  17. TextView和EditText的gettext()方法
  18. [C语言编程练习][17]假如有n个台阶,一次只能上1个台阶或2个台阶,请问走到第n个台阶有几种走法?
  19. 用户界面设计10原则 (转)
  20. 服务器异常卡顿 无法重装系统,电脑重装系统经常卡死怎么解决

热门文章

  1. JVM必备指南(转)
  2. 阅读准备-构建redis容器
  3. pthread-win32 semaphore信号量总结
  4. 现代化权限管理解决方案平台推动商业模式的演进
  5. 修改代码的艺术----- 2.2 高层测试 2.3 测试覆盖
  6. WebRTC的优缺点
  7. C/C++内存分配方式与存储区
  8. ACE_Select_Reactor 一 ——入门
  9. Linux下profile和bashrc四种的区别
  10. Flask实战2问答平台--导航条