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

  • 相关阅读:
    《Machine Learning in Action》—— 白话贝叶斯,“恰瓜群众”应该恰好瓜还是恰坏瓜
    《Machine Learning in Action》—— 女同学问Taoye,KNN应该怎么玩才能通关
    《Machine Learning in Action》—— Taoye给你讲讲决策树到底是支什么“鬼”
    深度学习炼丹术 —— Taoye不讲码德,又水文了,居然写感知器这么简单的内容
    《Machine Learning in Action》—— 浅谈线性回归的那些事
    《Machine Learning in Action》—— 懂的都懂,不懂的也能懂。非线性支持向量机
    《Machine Learning in Action》—— hao朋友,快来玩啊,决策树呦
    《Machine Learning in Action》—— 剖析支持向量机,优化SMO
    《Machine Learning in Action》—— 剖析支持向量机,单手狂撕线性SVM
    JVM 字节码指令
  • 原文地址:https://www.cnblogs.com/hushpa/p/6211915.html
Copyright © 2011-2022 走看看