Java 多线程 原子性 Java编程思想读书笔记

来源:互联网 发布:python twisted下载 编辑:程序博客网 时间:2024/06/16 07:17

在有关Java线程的讨论中,常有一个不正确的知识是“原子操作不需要进行同步控制”。原子操作是不能被线程调度机制中断的操作;一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前执行完成。但是注意:依赖于原子性是很棘手且很危险的。如果 你是一个并发专家,或者你得到来自这样的专家的帮助,你才应该使用原子性来代替同步。

Goetz测试:如果你可以编写用于现代微处理器的高性能JVM,那么就有资格去考虑是否可以避免同步。

原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。对于读取和写入除long和double之外的基本类型变量这样的操作,可以保证它们会被当作不可分(原子)的操作来操作内存。但JVM可以将64位(long和double变量)的读取和写入当作两个分享的32位操作来执行,这就产生了在一个读取和写入操作中间发生上下文切换,从而导致不同的任务可以看到不正确结果的可能性(有时被称为字撕裂)。但你定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作的)原子性(注意:在Java SE5之前,volatile一直未能正确地工作)。不同的JVM可以任意地提供更强的保证,但你不应该依赖于平台相关的特性。

原子操作可由线程机制来保证其不可中断,专家级的程序员可以利用这一点来编写无锁的代码,这些代码不需要被同步。但是即使是这样,它也是一种过于简化的机制。有时,甚至起来应该是安全的原子操作,实际上也可能不安全。尝试移除同步通常是一种表示不成熟优化的信号,并且将会给你招致大量的麻烦,而你却可能没有收获多少好处,甚至完成没有。

一个任务做出的修改,即使在不中断的意义上讲是原子性的,对其他任务也可能是不可视的(例如,修改只是暂时性存储在本地处理器的缓存中,而其他任务看不到这修改的结果),导致不同的任务对应用的状态有不同的视图。另一方面,同步机制应强制在处理器系统中,一个任务做出的修改必须在应用中是可视的。如果没有同步机制,那么修改时可视将无法确定。

volatile关键字还确保了应用中的可视性。如果将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作都可以看到这个修改,即使使用了本地缓存,情况也如此,volatile域会立即被写入主存中,而读取操作就发生在主存中。

一个任务所作的任何写入操作对这个任务来说都是可视的,如果它只需要在这个任务内部可视,那么你就不需要将其设置为volatile了。

当一个域的值依赖于它之前的值时(例如递增一个计数器),volatile就无法工作了。如果某个域的值受到其他域的限制,那么volatile就无法工作了,例如Range类的lower和upper边界就必须遵循lower<=upper的限制。

使用volatile而不是synchronized的唯一安全情况是类中只有一个可变的域。再次提醒,你的第一选择应该是使用synchronized关键字,这是最安全的方式,而尝试其他任何方式都是有风险的。

什么才属于原子操作呢?对域中的值做赋值和返回操作通常都是原子性。

注意:如i++;  i+=2; 等不是原子性的。

请看下面例子:

import java.util.concurrent.*;    public class AtomicityTest implements Runnable{      private  int i = 0;            public  int getValue(){          return i;      }            private synchronized void evenIncrement(){          i++;          i++;      }            public void run(){          while(true)              evenIncrement();      }        public static void main(String[] args) {          ExecutorService exec = Executors.newCachedThreadPool();          AtomicityTest at = new AtomicityTest();          exec.execute(at);                    while(true){              int val = at.getValue();              if(val % 2 != 0){                  System.out.println(val);                  System.exit(0);              }          }      }  } 

该程序会找到奇数并终止。尽管return i确实是原子性操作,但是缺少同步使得其数值可以在处于不稳定的中间状态时被读取,除此之外,由于i也不是volatile的,因此还存在可视性问题,getValue()和evenIncrement()必须是synchronized的。


原子类

Java SE5 引入了诸如AtomicInteger,AtomicLong,AtomicReference等特殊的原子性变量类,它们提供了下面形式的原子性条件更新操作:

boolean compareAndSet(int expect, int update);

这些类被调整为可以使用在某些现代处理器上的获得的,并且是在机器级别上的原子性,因此在使用它们时,通常不需要担心。对于常规编程来说,它们很少会派上用场,但是在涉及性能调优时,它们就大有用武之地了。例如,可以使用AtomicIntegerTest重写AtomicityTest:

import java.util.*;  import java.util.concurrent.*;  import java.util.concurrent.atomic.AtomicInteger;    public class AtomicIntegerTest implements Runnable{      private AtomicInteger  i = new AtomicInteger(0);      public int getValue(){          return i.get();      }      private void evenIncrement(){          i.addAndGet(2);      }      public void run(){          while(true){              evenIncrement();          }      }      public static void main(String[] args) {          new Timer().schedule(new TimerTask() {              public void run() {                  System.err.println("Aborting");                  System.exit(0);              }          }, 5000);                    ExecutorService exec = Executors.newCachedThreadPool();          AtomicIntegerTest ait = new AtomicIntegerTest();          exec.execute(ait);          while(true){              int val = ait.getValue();              if(val % 2 != 0){                  System.out.println(val);                  System.exit(0);              }          }      }  }
通过AtomicInteger消除了synchronized关键字。因为这个程序不会失败,所以添加了一个Timer,以便在5秒钟之后自动终止。

0 0
原创粉丝点击