zoukankan      html  css  js  c++  java
  • java中的volatile和synchronized

    关于volatile和同步相关的东西,网上有太多错误和解释不清的东西, 所以查阅相关书籍和文章后总结如下, 如果还是也存在不正确的内容,请一定要指出来, 以免误人子弟:)

    1. 原子性与可视性

        原子性是指操作不能被线程调度机制中断, 除long和double之外的所有基本类型的读或写操作都是原子操作,注意这里说的读写, 仅指如return i, i = 10, 对于像i++这种操作,包含了读,加1,写指令,所以不是原子操作。 对于long和double的读写,在64位JVM上会把它们当作两个32位来操作,所以不具备原子性。

        在定义long和double类型变量时,如果使用volatile来修饰,那么也可以获得原子性,除此以外,volatile与原子性没有直接关系。

        可视性,volatile的主要作用就是确保可视性,那么什么是可视性?
        在系统中(多处理器更加明显),对某一变量的修改有时会暂时保存在本地处理器的缓存中,还没有写入共享内存,这时候有另外一个线程读取变量在共享内存的值,那么这个修改对这个线程就是不可视的。
        Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值直接写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。 原子操作不一定就有可视性, 比如赋值,i = 10,  如果i没有被特别修饰, 那么因为缓存的原因, 它仍然可能是不可视的

        所以原子性和可视性是完全不同的两个概念

    2. volatile的应用场景
        详细可以参考java语言架构师Brain Geotz的文章
        Java 中volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 优点是所需的编码较少,并且运行时开销也较少, 不会引起线程阻塞。Volatile 变量具有 synchronized 的可视性特性,但是不具备原子特性。
        这就导致Volatile 变量可用于提供线程安全,但是应用场景非常有限,在一些经典java书里,基本都不推荐使用volatile替代synchronized来实现同步,因为风险较大, 很容易出错。
    Brain给出的使用volatile实现线程安全的条件:
        对变量的写操作不依赖于当前值。 (count++这种就不行了)
        该变量没有包含在具有其他变量的不变式中(Invariants,例如 “start <=end”)。

        我的理解是, 这两个条件都是因为volatile不能提供原子性导致的, 如果多线程执行的一个操作不是原子性的, 使用volatile时就一定要慎重。
        如果满足这两个条件, 多线程执行的操作是原子性的, 那就是可以使用,如:
    将 volatile 变量作为状态标志使用

    volatile boolean shutdownRequested;
    .
    ...
    public void shutdown() { shutdownRequested = true; } //不依赖当前值,原子操作
    public void doWork() { 
    
      while (!shutdownRequested) { 
      // do stuff
      }
    }

        文章中的其他几种模式, 也都差不多这个意思。

        还有一种情况,如果读操作远远超过写操作,可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。
        结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁”

    @ThreadSafe
    public class CheesyCounter {
    
      // Employs the cheap read-write lock trick
      
      // All mutative operations MUST be done with the 'this' lock held
    
      @GuardedBy("this") private volatile int value;
    
      public int getValue() { return value; } //使用volatile替代synchronized
    
      public synchronized int increment() {
    
      return value++;
    
    }

        然而,你可以在读操作中使用 volatile 确保当前值的可见性,因此可以使用锁进行所有变化的操作,使用 volatile 进行只读操作。其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作,因此当使用 volatile 保证读代码路径时,要比使用锁执行全部代码路径获得更高的共享度 —— 就像读-写操作一样。

    3. synchronized

    class AtomTest implements Runnable {
        private volatile int i = 0;
        public int getVal() {return i;}
    
        public synchronized void inc() {i++; i++;}
        
        @Override
        public void run() {
            while (true) {
                inc();
            }
        }
    }
    
    public class TestThread {
        
        public static void main(String[] args) throws InterruptedException {
    
            ExecutorService exec = Executors.newCachedThreadPool();
            AtomTest at = new AtomTest();
            exec.execute(at);
            while (true) {
                int val = at.getVal();
                if (val % 2 != 0) {
                    System.out.println(val);
                    System.exit(0);
                }
            }
        }
    }

        结果会输出奇数, 退出程序, 原因是getVal读到了inc的中间值。 这种情况只能在getVal方法前加synchronized
        在读取的时候也加锁, 这样在读的时候如果正在写, 那么等待, 所以就不会读到inc的中间值。


        关于synchronized值得注意的几个点:

        1) 所有对象都含有一个锁,当调用到synchronized(object)块时,先检测obj有没有加锁,如果有, 阻塞, 如果没有, 对object加锁, 执行完后释放锁。

        2) synchronized void f() {//...}    等价于  void f() { synchronized(this) {//...} },   在当前对象上加锁
        3) synchronized 提供原子性和可视性, 被它完全保护的变量不需要用volatile

        4) synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法。

        注意第一点, 非常重要:虽然sync块可以包裹一段代码,但是锁是加对象上,不是加在代码上,它的工作机制如下:

        对于synchronized(obj) {//...}: 先检测obj有没有加锁,如果有, 阻塞, 如果没有, 对obj加锁, 执行块中的代码,完毕后释放锁。这里只检测obj对象上的锁,不关注代码块里的代码或者对象。

        

        所以, 加锁的范围由obj决定,理解了这一点, 下面的很多种情况就会很容易理解:

          1. 当两个并发线程访问同一个对象object中的这个相同synchronized(this)同步代码块时,一个时间内针对该对象的操作只能有一个线程得到执行。另一个线程必须等待。

             - 如果同一对象已经加锁, 另一线程执行到sync块,检测到有锁挂起。

      2. 然而,另一个线程仍然可以访问该object中的synchronized(this)同步代码块。

             - 非sync代码,不关注对象是否加锁

      3. 当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对该object中所有其它synchronized(this)同步代码块的访问将被阻塞。

             - synchronized(this) 只是关注this对象, 只要this已加锁,执行到同一对象中不同方法的sync块时,也会阻塞。

      4. 不同的对象实例的synchronized(this)方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法。

             - 可以同时访问不同对象中的sync块, 原因很简单, 因为synchronized(this),关注的是this对象,不同对象的this是不一样的。

          5.  同理,也可以对其他对象加锁,

              1) 对于类中的成员对象: private Integer i = new Integer(0);   synchronized(i) {//...}    i创建在堆上,每个对象有一个i,所以效果与synchronized(this)一样。

              2) 对于静态成员对象: private static Integer i = new Integer(0);  synchronized(i) {//...}: i创建在静态区,属于类, 所以效果与synchronized(ClassName.class)一样。

      6.对类对象加锁时,对该类的所有对象都起作用:synchronized(ClassName.class) {//...}

    最后我在测试代码时,发现对于private Integer i = 0;   synchronized(i) {//...}

    锁的效果是全局的,推测可能是Integer对0进程打包时,自动生成的这个对象可能在常量区。 后来查了下资料才发现并非如此:

    Integer实现中有一个IntegerCache类,它包含一个静态的Integer数组,在类加载时就将-128 到 127 的Integer对象创建了,并保存在cache数组中,一旦程序调用valueOf 方法,如果i的值是在-128 到 127 之间就直接在cache缓存数组中去取Integer对象。 所以这里的i引用的是整个全局数组里值, 所以锁也是全局的了。。。

    如果改成private Integer i = 300,  然后加锁就只在本对象有效了。 原因是i不在缓存范围,所以创建在了堆上。

    refer  http://blog.csdn.net/xiaohai0504/article/details/6885137

  • 相关阅读:
    [HNOI2007]最小矩形覆盖
    Java实现第十届蓝桥杯质数
    Redo current损坏
    [学习笔记]计算几何
    delete noprompt archivelog 报错ORA-00245,RMAN-08132
    [学习笔记]CDQ分治
    Java实现第九届蓝桥杯耐摔指数
    RAC RMAN 备份 RMAN-03009 ORA-19504 ORA-27040 RMAN-06012 channel c3 not allocated 错误分析
    [学习笔记]树套树
    RMAN-03002、RMAN-06059
  • 原文地址:https://www.cnblogs.com/hushpa/p/6211915.html
Copyright © 2011-2022 走看看