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

 

posted @ 2016-12-22 16:50  Jim'sBlog  阅读(3127)  评论(0编辑  收藏  举报