同步访问共享的可变数据(synchronized与volatile关键字)

synchronized 关键字可以保证同一时刻,只有一个线程可以执行某一个方法,或是某一个代码块。

它包含两个特征:1、互斥 2、可见。即同步不仅可以阻止一个线程看到对象处于不一致的状态中,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。

java语言规范保证读或者写一个变量时原子的,除非这个变量的类型为long或者double。

读取一个非long或double类型的变量,可以保证返回的值是某个线程保存在该变量中的,即使多线程在没有同步的情况下并发的修改这个变量也是如此。

      虽然语言规范保证了线程在读取原子数据的时候,不会看到任意的数值,但是它并不保证一个线程写入的值对于另一个线程是可见的。为了在线程之间进行可靠通信,也为了互斥访问,同步是必要的。

public class StopThread {
    private static boolean stopRequested = false;

    public static synchronized boolean isStopRequested() {
        return stopRequested;
    }

    public static synchronized void setStopRequested(boolean stopRequested) {
        StopThread.stopRequested = stopRequested;
    }

    public static void main(String[] args) {
        try {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int i = 0;
                    while (!isStopRequested()) {
                        System.out.println(i++);
                    }
                }
            }).start();

            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        setStopRequested(true);
    }
}

 

 上面的synchronized关键字是需要的,如果没有同步的话,这个程序永远不会终止:因为不能保证后台线程何时"看到"主线程对stopRequested的值所做的改变,后台线程永远在循环。

注意:读写方法都要被同步,否则同步就不会起作用。

stopRequested即使没有被同步也是原子的,这些同步方法是为了它的 通信效果而不是为了互斥访问。

 

volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。

 

     锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。

 

    Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。

public class StopThread2 {
    private static volatile boolean stopRequested = false;

    public static boolean isStopRequested() {
        return stopRequested;
    }

    public static void setStopRequested(boolean stopRequested) {
        StopThread2.stopRequested = stopRequested;
    }

    public static void main(String[] args) {
        try {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int i = 0;
                    while (!isStopRequested()) {
                        System.out.println(i++);
                    }
                }
            }).start();

            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        setStopRequested(true);
    }
}

 

   单独使用 volatile 还不足以实现计数器,问题在于操作符(++)不是原子的,例如

private static volatile int nextSerialNumber = 0;
public static int generaterSerialNumber(){
       return nextSerialNumber ++;
}

    它在nextSerialNumber域中执行两个操作:首先它读取值,然后写回一个新值,相当于原来的值再加上1。如果第二个线程在第一个线程读取旧值和写回新值期间读取这个域,第二个线程就会与第一个线程看到同一值,并返回相同的序列号,这个程序会计算出错误结果。

修正generaterSerialNumber的方法的一种方法是:在它的声明中去掉volatile增加synchronized修饰符。这样可以确保多个调用不会交叉存取,确保每个调用都会看到之前所有调用的效果。

最好的修正方法是:使用类AtomicLong

private static final AtomicLong nextSerialNumber = new AtomicLong();
public static long generaterSerialNumber(){
       return nextSerialNumber.getAndIncrement();
}

 

    简而言之,多个线程共享可变数据的时候,每个读或写数据的线程都必须执行同步。如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。如果需要线程之间的交互通信,而不需要互斥,volatile修饰符就是一种可以接受的形式,但需要正确的使用。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章