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

在有关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秒钟之后自动终止。

你可能感兴趣的:(java,读书笔记)