volatile和synchronized

2018-01-28  本文已影响0人  JD大魔王

1. volatile


Java代码如下:

instance = new Singleton();//instance是volatile变量

转换成汇编代码:

0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0,(%esp);

有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,lock前缀的指令在多核处理器下会引发了两件事情。

  • 将当前处理器缓存行的数据会写回到系统内存。
  • 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。

处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完之后不知道何时会写到内存,如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。


多线程CPU处理机制
  • Lock前缀指令会引起处理器缓存回写到内存。
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

volatile变量具有 synchronized 的可见性特性,但是不具备原子性。这就是说线程能够自动发现 volatile 变量的最新值。只能在有限的一些情形下使用 volatile 。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

  • 对变量的写操作不依赖于当前值。
  • 该变量没有包含在具有其他变量的不变式中。

反例:volatile变量不能用于约束条件中,下面是一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是小于或等于上界。

public class NumberRange {  
    private volatile int lower;
    private volatile int upper; 
    public int getLower() { return lower; }  
    public int getUpper() { return upper; }  
  
    public void setLower(int value) {   
        if (value > upper)   
            throw new IllegalArgumentException(...);  
        lower = value;  
    }  
  
    public void setUpper(int value) {   
        if (value < lower)   
            throw new IllegalArgumentException(...);  
        upper = value;  
    }  
}

将 lower 和 upper 字段定义为 volatile 类型不能够充分实现类的线程安全;而仍然需要使用同步——使 setLower()和 setUpper() 操作原子化。否则,如果凑巧两个线程在同一时间使用不一致的值执行 setLower 和 setUpper 的话,则会使范围处于不一致的状态。例如,如果初始状态是(0, 5),同一时间内,线程 A 调用setLower(4) 并且线程 B 调用setUpper(3),显然这两个操作交叉存入的值是不符合条件的,那么两个线程都会通过用于保护不变式的检查,使得最后的范围值是(4, 3) —— 一个无效值

状态标志:也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

volatile boolean isShutdown;  
...    
public void shutdown() {   
    isShutdown = true;   
}  
  
public void doWork() {   
    while (!isShutdown) {   
        // do stuff  
    }  
}

线程1执行doWork()的过程中,可能有另外的线程2调用了shutdown,所以boolean变量必须是volatile。
这种类型的状态标记的一个公共特性是:通常只有一种状态转换;shutdownRequested 标志从false 转换为true,然后程序停止。

开销较低的“读-写锁”策略:如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。
如下显示的线程安全的计数器,使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

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;  
  
    //读操作,没有synchronized,提高性能  
    public int getValue() {   
        return value;   
    }   
  
    //写操作,必须synchronized。因为x++不是原子操作  
    public synchronized int increment() {  
        return value++;  
    }  
}

使用锁进行所有变化的操作,使用 volatile 进行只读操作。
其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作。

2. synchronized


Java中的每一个对象都可以作为锁。具体表现为一下3中形式:

  • 对于普通的同步方法,锁是当先实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchronized括号里配置的对象。

示例代码:

public class SynchronizedTest2 {  
      
    // synchronized关键字修饰静态的方法 同步方法  
    public synchronized static void printNum1(){   
        for(int i = 0; i < 10; i++) {  
            System.out.println(Thread.currentThread().getName() + " " + i);  
        }  
    }  
      
    // synchronized关键字使用类锁   同步代码块  
    public static void printNum2(){   
        synchronized(SynchronizedTest2.class) {  
            for(int i = 0; i < 10; i++) {  
                System.out.println(Thread.currentThread().getName() + " " + i);  
            }  
        }  
    }  
      
    // synchronized关键字修饰 同步方法  这里使用的是对象锁  
    public synchronized void printNum3(){   
        for(int i = 0; i < 10; i++) {  
            System.out.println(Thread.currentThread().getName() + " " + i);  
        }  
    }  
      
    public static void main(String[] args) {  
        // TODO Auto-generated method stub  
        final SynchronizedTest2 test = new SynchronizedTest2();  
        Thread t1 = new Thread(new Runnable() {  
            public void run() {  
                SynchronizedTest2.printNum1();  
            }  
        },"A");  
          
        Thread t2 = new Thread(new Runnable() {  
            public void run() {  
                SynchronizedTest2.printNum2();  
            }  
        },"B");  
          
        Thread t3 = new Thread(new Runnable() {  
            public void run() {  
                test.printNum3();  
            }  
        },"C");  
        t1.start();  
        t2.start();  
        t3.start();  
    }  
}  
public class Synchronized {

    public static void main(String[] args) {
        synchronized (Synchronized.class){//对Synchronized class对象进行加锁
            
        }
        m();//静态同步方法,对Synchronized class对象进行加锁
    }

    public synchronized static void m() {

    }
}

在Synchronized.class同级目录执行javap -v Synchronized.class,以下是相关部分的输出:

public static void main(java.lang.String[]);  
// 方法修饰符,表示:public staticflags: ACC_PUBLIC, ACC_STATIC  
Code:  
stack=2, locals=1, args_size=1  
0: ldc #1  // class com/murdock/books/multithread/book/Synchronized  
2: dup  
3: monitorenter  // monitorenter:监视器进入,获取锁  
4: monitorexit   // monitorexit:监视器退出,释放锁  
5: invokestatic  #16 // Method m:()V  
8: return  
public static synchronized void m();  
// 方法修饰符,表示: public static synchronized  
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED  
Code:  
stack=0, locals=0, args_size=0  
0: return 

下图描述了对象、对象的监视器、同步队列和执行线程之间的关系。



从图中可以看到,任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

下面我们直接使用对象锁的相关条件实现一个生产者和消费者案例:

public class SynchronizedTest3 {  
      
    private Object object = new Object();  
      
    private List<Integer> list = new ArrayList<Integer>();  
      
    private boolean flag = true;  
      
      
    // 这里我们使用object对象的锁,以及该锁的条件对象  
    // 生产者线程一次生产一个数据5  
    public void produce() throws InterruptedException {  
        synchronized(object) {  
            while(flag){  
                if (list.size() > 0){  
                    object.wait();  
                } else {  
                    list.add(5);  
                    System.out.println("生产者生产数据");  
                    object.notifyAll();  
                }  
            }  
        }  
          
    }  
      
    // 消费者线程每次消费一个数据  
    public void consume() throws InterruptedException {  
        synchronized(object){  
            while(flag){  
                if (list.size() <= 0) {  
                    object.wait();  
                } else {  
                    System.out.println(list.remove(0));  
                    System.out.println("消费者消费数据");  
                    object.notifyAll();  
                }  
            }  
        }  
    }  
      
      
      
    public boolean isFlag() {  
        return flag;  
    }  
  
    public void setFlag(boolean flag) {  
        this.flag = flag;  
    }  
  
    public static void main(String[] args) {  
        // TODO Auto-generated method stub  
        final SynchronizedTest3 test = new SynchronizedTest3();  
        Thread t1 = new Thread(new Runnable() {  
            public void run() {  
                try {  
                    test.produce();  
                } catch (InterruptedException e) {  
                    // TODO Auto-generated catch block  
                    e.printStackTrace();  
                }  
            }  
        });  
          
        Thread t2 = new Thread(new Runnable() {  
            public void run() {  
                try {  
                    test.consume();  
                } catch (InterruptedException e) {  
                    // TODO Auto-generated catch block  
                    e.printStackTrace();  
                }  
            }  
        });  
          
        t1.start();  
        t2.start();  
          
        try {  
            Thread.sleep(10);  
        } catch (InterruptedException e) {  
            // TODO Auto-generated catch block  
            e.printStackTrace();  
        }  
        test.setFlag(false);  
    }  
  
}  

下图描述了上述示例的全过程。


等待通知机制
上一篇 下一篇

猜你喜欢

热点阅读